C言語編 第48章 理解の定着・小休止D

この章の概要

この章の概要です。

理解の定着・小休止D

さて、この章ではこれまでに見てきた内容の理解を再確認しましょう。 また、1章丸ごとを割くほどでも無い細かい部分について、少し触れていきます。

今回は、以下の範囲が対象です。 ファイル操作と、文字の扱いがテーマとなります。

ストリーム

ストリームは、「経路」という意味合いがあり、データが流れる経路を指しています。

ストリームには、標準入力(stdin)、標準出力(stdout)、標準エラー(stderr) という種類があります。 これ以外にも存在することもありますが、それは環境依存です。
一般に、標準入力はキーボード、標準出力と標準エラーは画面へ結び付けられていますが、 リダイレクトという機能を使って、接続先を変更することも可能です。

データの入力や出力を、ストリームを介して行うことによって、実際にどんな機器やファイルとやり取りしているのかを気にする必要がなくなります。 これによって、機器やファイルの性質の違いを考慮することなく、まったく同じ方法でプログラムを書くことができるようになっています。

テキストファイルの書き出し

テキストファイルへ、文字列を書き込む例を挙げます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;

	fp = fopen( "hello.txt", "w" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	fputs( "Hello, World\n", fp );

	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果



出力ファイル (hello.txt)

Hello, World

どんなタイプのファイル操作でも、fopen関数(⇒リファレンス)でオープンし、 fclose関数(⇒リファレンス)でクローズすることに変わりはありません。

fopen関数の第2引数によるオープンモードの指定には、様々なバリエーションがありますが、 テキストファイルに対する通常の書き込みには、"w" を指定し、追記書き込みの場合には、"a" を指定します。
追記書き込みは、既にファイルに書き込まれているデータを壊すことなく、末尾にデータを追加で書き込んでいくというものです。 追記書き込み以外の方法で書き込みを行う場合、既に書き込まれているデータは破棄されます。

また、テキストファイルへの書き出しのためには、fputs関数(⇒リファレンス)の他にも、 fprintf関数(⇒リファレンス)や fputc関数(⇒リファレンス)、 putc関数(⇒リファレンス)が使えます。
fputc関数と putc関数は、後者がマクロとして実装されている可能性があるということ以外に違いはありません。 通常は、余計なトラブルを避けるためにも、fputc関数の方を選んだ方がいいでしょう。

なお、これらの関数は実引数で、FILE*型の変数を渡すことによって、そのファイルを対象に出力処理を行うというものです。 FILE*型変数の代わりに、stdout を渡せば、標準出力を対象とすることができます。

テキストファイルの読み込み

テキストファイルを読み込む際には、fopen関数の第2引数に "r" を指定します。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	char buf[80];

	fp = fopen( "hello.txt", "r" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	while( 1 ){
		if( fgets( buf, sizeof(buf), fp ) == NULL ){
			if( feof( fp ) ){
				break;
			}
			else{
				fputs( "エラーが発生しました。\n", stderr );
				exit( EXIT_FAILURE );
			}
		}

		fputs( buf, stdout );
	}
	
	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果

Hello, World

入力ファイル (hello.txt)

Hello, World

テキストファイルからの読み込みには、fgets関数(⇒リファレンス)の他、 fscanf関数(⇒リファレンス)、 fgetc関数(⇒リファレンス)、 getc関数(⇒リファレンス) が使えます。
fgetc関数と getc関数は、後者がマクロとして実装されている可能性があるということ以外に違いはありません。 通常は、余計なトラブルを避けるためにも、fgetc関数の方を選んだ方がいいでしょう。

ファイルの中身を全て読み込む場合、どうにかしてファイルの末尾を検出する必要があります。 上記のサンプルプログラムでは、fgets関数の戻り値と、feof関数(⇒リファレンス)の助けを借りて、 これを実現しています。 つまり、次のように考えられます。

fgets関数の場合はこれで良いですが、他の関数を使う場合には、多少異なることもあります。 例えば、fgetc関数は、ファイルの終端に達するか、エラーが発生すると EOF(⇒リファレンス)を返します。

いずれにしても、ファイルの終端に達しているかは、feof関数で調べられますし、 エラーの有無は ferror関数(⇒リファレンス)で調べることができます。

バイナリファイルの書き出し

バイナリファイルへの書き出しには、fwrite関数(⇒リファレンス)を使います。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	int num = 900;
	double d = 7.85;
	char str[] = "xyzxyz";


	fp = fopen( "test.bin", "wb" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}


	fwrite( &num, sizeof(num), 1, fp );
	fwrite( &d, sizeof(d), 1, fp );
	fwrite( str, sizeof(char), sizeof(str), fp );


	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)


実行結果(test.bin のテキスト表現)

????ffffff@xyzxyz

バイナリファイルを扱う際には、fopen関数の第2引数に "b" を含むオープンモードを指定します。

バイナリファイルの読み込み

バイナリファイルの読み込みには、fread関数(⇒リファレンス)を使います。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	int num;
	double d;
	char str[7];


	fp = fopen( "test.bin", "rb" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}


	fread( &num, sizeof(num), 1, fp );
	fread( &d, sizeof(d), 1, fp );
	fread( str, sizeof(char), sizeof(str), fp );


	printf( "%d\n", num );
	printf( "%f\n", d );
	printf( "%s\n", str );


	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)

900
7.850000
xyzxyz

バイナリデータは、テキストデータとは異なり、改行文字に対して特別な扱いを行いません。 改行文字を含んだファイルを読み込んでも、それを改行だとみなすことはありません。

改行文字の表現は、環境によって異なり、C言語ではこれを '\n' という文字で表現することで差異を吸収しています。 例えば、Windows環境での改行文字は CR と LF という2つの文字の組み合わせで表現されます。 つまり、2Byte 必要としますが、他の環境では CR だけであったり、LF だけであったりします。 これが、環境ごとの差異ということです。

シーク

ファイルの読み書きを行うとき、ファイル内のどの辺りを対象にしているかは、ファイルポジションという値で管理されています。 ファイルの読み書きを行うと、ファイルポジションが自動的に移動します。

注意が必要なのは、ファイルポジションは読み書きを行う前に移動していることです。 そのため、例えば fgetc関数 がファイルの最後の1文字を読み取った直後に、feof関数を呼び出しても、まだ 0 を返します。 次回の fgetc関数の呼び出しのときに EOF が返却され、feof関数も 0以外を返すようになります。


ファイルポジションを強制的に移動させる操作を、シークと呼びます。 シークは、fseek関数(⇒リファレンス)で行えます。 また、現在のファイルポジションは、ftell関数(⇒リファレンス)で取得できます。

しかし、テキストファイルに対するシークは制限が厳しくなっており、以下の操作しか保証されません。

  1. 第3引数を SEEK_SET(⇒リファレンス) にし、第2引数に 0L を指定⇒ファイルの先頭へ移動
  2. 第3引数を SEEK_CUR(⇒リファレンス) にし、第2引数に 0L を指定⇒現在位置のまま
  3. 第3引数を SEEK_END(⇒リファレンス) にし、第2引数に 0L を指定⇒ファイルの末尾へ移動
  4. 第3引数を SEEK_SET にし、第2引数に ftell関数の戻り値を指定⇒ftell関数が返した位置へ移動

バイナリファイルの場合には、このような制限はありませんが、1点だけ注意が必要です。 バイナリファイルに対して、fseek関数を次のように使ったときの動作は保証されません。

これは、ファイルサイズを調べるためによく使われている以下のような関数が、実は環境依存な処理であるということです。

long GetFileSize(FILE* fp)
{
	long fpos_save, size;

	/* 現在のファイルポジションを保存 */
	fpos_save = ftell( fp );

	/* ファイルの末尾まで移動して、その位置を調べる */
	fseek( fp, 0, SEEK_END );
	size = ftell(fp);

	/* ファイルポジションを元に戻す */
	fseek( fp, fpos_save, SEEK_SET );

	return size;
}


また、fseek関数や ftell関数の引数は、long int型であり、巨大なファイルを扱うには大きさが不足する可能性があります。 そのような場合には、fsetpos関数(⇒リファレンス)や、 fgetpos関数(⇒リファレンス)を使った方が良いでしょう。

#include <stdio.h>
#include <stdlib.h>

#define FILE_NAME  "hello.txt"

void putFileLine(FILE* fp);

int main(void)
{
	FILE* fp;
	fpos_t pos;


	fp = fopen( "test.txt", "r" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}


	putFileLine( fp );     /* 1行目 */
	fgetpos( fp, &pos );   /* ファイルポジションを保存 */
	putFileLine( fp );     /* 2行目 */
	putFileLine( fp );     /* 3行目 */
	fsetpos( fp, &pos );   /* 保存しておいた位置へ復帰 */
	putFileLine( fp );     /* 2行目 */


	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

/*
	ファイルから1行読み取って、標準出力へ出力。
	引数
		fp:		ファイルポインタ。
*/
void putFileLine(FILE* fp)
{
	char buf[80];

	fgets( buf, sizeof(buf), fp );
	fputs( buf, stdout );
}

入力ファイル(test.txt)

1行目
2行目
3行目
4行目
5行目

実行結果(標準出力)

1行目
2行目
3行目
2行目

バッファリング

バッファリングとは、データを一旦どこかに蓄えておき、あるタイミングでまとめて処理する方法のことを指します。 C言語での入出力処理においても、バッファリングが使われています(使われない環境も無い訳ではありませんが)

setvbuf関数(⇒リファレンス)を使えば、バッファリングの方法を変更できますが、 環境への依存性の高い分野でもあるので、変更できるということを知っている程度で十分だとは思います。

バッファリングされているために、標準出力への出力が思ったタイミングで行われないことがあるかも知れません。 バッファリングされている場合に、任意のタイミングで出力を実行するためには、fflush関数(⇒リファレンス)を使用します。
なお、fflush関数を stdin(標準入力)に対して行うプログラムを見かけますが、これが正常に動作するかどうかは環境に依存します。

また、標準エラーストリームに関しては、ほとんどの場合はバッファリングが無効になっています。 これは、バッファリングしてしまうと、エラーが発生したタイミングですぐに有益なログを書き出すことができなくなってしまうためです。

ファイルシステム

ファイルに対する幾つかの操作は、標準関数として用意されています。

操作 方法
ファイルの新規作成 fopen関数(⇒リファレンス)の "w"オープンモードで開き、何も書き込まずに閉じる。
ファイルの削除 remove関数(⇒リファレンス
ファイルのコピー バイナリモードでコピー元とコピー先のファイルを開き、1Byteずつ fread関数(⇒リファレンス)と fwrite関数(⇒リファレンス)を使って複写する。
ファイルの移動 rename関数(⇒リファレンス)が事実上、移動と同じことをしている。
ファイルの名前変更 rename関数(⇒リファレンス
ファイルが存在しているか調べる fopen関数(⇒リファレンス)の "r"オープンモードでオープンできるか試行する。
ファイルサイズを調べる バイナリモードで開き、fseek関数(⇒リファレンス)と ftell関数(⇒リファレンス)を駆使して調べる。。

エンディアン

2Byte以上の大きさのデータが、メモリ上にどのような順番で並ぶのかは、 エンディアンバイトオーダー)というもので規定されています。

0x00000384 という 4Byte のデータを、逆の順番で「0x84 0x03 0x00 0x00」のように並べる方式は、リトルエンディアン方式と呼ばれます。 Windows環境では一般的に、この方式が使われています。

一方、そのままの順番で「0x00 0x00 0x03 0x84」のように並べる方式は、ビッグエンディアン方式と呼ばれます。

コマンドライン

プログラムを実行する際に、コマンドライン引数を渡すことができます。 Windows環境であれば、次のような方法が使えます。

  1. コマンドプロンプトから実行することにし、そのときに引数を渡す。
  2. 実行ファイルのショートカットを作成し、ショートカット側のプロパティにある「リンク先」のところに引数を記述。ショートカット側を実行する。
  3. VisualC++ のプロジェクト設定を利用する。

3つ目の方法は、プログラムを作成している段階に限った話ですから、完成品を誰かが実行するときには使えません。 通常は1つ目の方法を採ることになるでしょう。

コマンドライン引数を受け取るプログラムは、2つの引数を持った形式の main関数を使って作成する必要があります。 次のプログラムは、コマンドライン引数の内容を標準出力へ出力しています。

#include <stdio.h>

int main(int argc, char *argv[])
{
	int i;

	for( i = 1; i < argc; ++i ){
		puts( argv[i] );
	}

	return 0;
}

コマンドライン引数

test message

実行結果

test
message

main関数の仮引数argc には、コマンドライン引数の個数が格納されています。 argv の方は、argv[0] にプログラムの名前が格納されており、argv[1] 以降に、渡されたコマンドライン引数が順番通りに格納されています。


また、コマンドライン引数を持つプログラムを実行する際、

myprogram < test.txt

このように指定すると、標準入力の代わりに test.txt の内容を使うようになります。 これを、標準入力をリダイレクトすると言います。 同様のことを標準出力に対して行う場合は、

myprogram > test.txt

となります。

マルチバイト文字

マルチバイト文字は、1文字を表現するために必要なデータサイズが可変になっています。 Shift_JIS は、代表的なマルチバイト文字コードです。

Windows環境でのC言語プログラムでは、日本語を扱うために Shift_JIS を用いることが一般的になっています。 しかし、C言語そのものとしては、ASCIIコードを使って文字を表現します。
それでもある程度うまくいくのは、Shift_JIS が ASCII に対する互換性を持っているためです。 つまり、ASCII で表現できる文字は、Shift_JIS でもまったく同じ表現が通用するようになっています。

マルチバイト文字で表現された日本語の文字列に対して、 strlen関数(⇒リファレンス)を使っても、 思ったような値は返ってきません。 日本語には、1文字を 2Byte で表現する文字が含まれていますが、strlen関数は ASCII しか想定していないため、 1文字は 1Byte である前提で文字数を数えるためです。

本当に日本語としての文字数を数えるには、次のように複雑なプログラムが必要になります。

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main(void)
{
	const char str[] = "日本語を使うテスト";
	int char_count;
	int i;

	setlocale( LC_CTYPE, "" );

	i = 0;
	char_count = 0;
	while( str[i] != '\0' ){
		
		/* &str[i] 以降に何文字あるか調べる */
		/* MB_CUR_MAX はマルチバイト文字1文字分の最大サイズであり、それ以上は調べない */
		/* マルチバイト文字における1文字分だけ、変数i を進める */
		i += mblen( &str[i], MB_CUR_MAX );
		
		/* 上で、マルチバイト文字における1文字が調べられた */
		char_count++;
	}

	printf( "length: %d\n", char_count );

	return 0;
}

実行結果

length: 9

ワイド文字

ワイド文字は、1文字を表現するために必要なデータサイズが、1Byte以上の固定サイズになっています。 UTF-16 は、代表的なワイド文字コードです。

C言語では、ほとんどサポートの無いマルチバイト文字とは違い、ワイド文字は幾つかの標準関数が用意されています。 ASCII を対象とする str〜系の関数に対して、ワイド文字版は wcs〜 という名称の関数が用意されています。 例えば、strcpy関数(⇒リファレンス)に対応するワイド文字版は wcscpy関数(⇒リファレンス)です。

通常の文字を char型で表現するのに対し、ワイド文字は wchar_t型で表現します。 char型は必ず 1Byte なので、ワイド文字では容量不足になり得るためです。 ただし、wchar_t型は、単に他の型を typedef して作られているだけであって、予約語になっている訳ではありません。 例えば、ワイド文字を表現するために 2Byte必要で、かつ、short型の大きさが 2Byte の環境であれば、

typedef short int wchar_t;

のように定義されていることになります。

wchar_t型を、ワイド文字以外の表現のために使うのは間違っています。 マルチバイト文字の方は、char型(あるいは、その配列)で扱うのが正しいです。
なお、ワイド文字とマルチバイト文字の相互変換は、mbtowc関数(⇒リファレンス) や wctomb関数(⇒リファレンス)で行えます。
また、文字列であれば、mbstowcs関数(⇒リファレンス)、 wcstombs関数(⇒リファレンス)を使います。


練習問題

まとめとして、多めに練習問題を用意しました。★の数は難易度を表します。

問題@ 絶対パスと相対パスの意味を説明して下さい。[★]

問題A プログラムの処理結果を、標準出力ではなく、ファイルへ出力することの利点は何か説明して下さい。 [★]

問題B 環境変数MY_TOOL_DIR に記述されたパスにある tool.bin というファイルをオープンし、クローズするだけのプログラムを作成して下さい。 MY_TOOL_DIR の値の内容は各自で任意に設定して構いません。[★★]

問題C マルチバイト文字とワイド文字の違いを説明して下さい。[★]

問題D Shift_JIS を使用する環境において、0x5C問題が起こる理由と、その対策を説明して下さい。 [★]

問題E ワイド文字列 "abcde" のサイズが終端文字も含めて 24Byte であるとき、ワイド文字の '\0' は何バイトですか? [★]

問題F ファイルの名前が wchar_t型の文字列として与えられたとき、その名前のファイルをオープンし、クローズするだけのプログラムを作成して下さい。[★★]

問題G 文字列の中から、文字列を探す strstr関数(⇒リファレンス)という標準関数が存在します。 この関数は、第1引数に指定した文字列中から、第2引数で指定した文字列と一致する部分を探し、見つかれば、先頭のアドレスを返し、見つからなければ NULL を返します。
また、この関数のワイド文字列版に wcsstr関数(⇒リファレンス)があります。 単に、char*型から wchar_t*型に変わっただけの標準関数ですが、これと同じことをする関数を自作して下さい。[★★]

問題H C言語のソースファイルやヘッダファイルを読み込んで、コメント部分を除去した結果を出力するプログラムを作成して下さい。 読み込むファイルの名前は、コマンドライン引数から受け取るようにして下さい。[★★★]

問題I コマンドライン引数から、ファイルパスを2つ指定し、1つ目のファイルの内容を、2つ目のファイルへ追記するプログラムを作成して下さい。 例えば、

myprogram in.txt out.txt

としたとき、in.txt の内容を、out.txt の末尾へ追記します。 [★★★]

問題J コマンドライン引数から、ファイルパスと、任意の文字列を入力し、ファイル内検索を行うプログラムを作成して下さい。

例えば、

myprogram test.txt Hello

としたとき、test.txt の内容から、"Hello" という文字列を探します。 1行の文字数は 80文字以内であることが保証されているものとします。 発見できたら、その行数を標準出力へ出力させて下さい。 なお、同じ文字列が、ファイル内に複数存在する可能性も考慮して下さい。 [★★★]

問題K バッファリングの意味と、その価値、問題点を説明して下さい。 [★]

問題L 小さめで単色のビットマップファイルを用意し、その内容をバイナリエディタで確認して下さい。 色を変えて再度確認し、内容がどう変化するか調べて下さい。 その結果から、どんなことが分かりますか? [★]

問題M 問題Lの結果を踏まえ、赤色の単色画像を読み込んで、青色の単色画像に変換して出力するプログラムを作成して下さい。 [★★]

問題N コマンドライン引数から指定したファイルを読み込み、バイナリエディタのように整形して出力するプログラムを作成して下さい。 出力先は標準出力として、リダイレクトによってファイルへも書き出せることを確認して下さい。[★★★]


解答ページはこちら

参考リンク

更新履歴

'2015/8/29 flose関数の戻り値もチェックするようにした。

'2014/1/25 setlocale関数の LC_CTYPE の指定を修正。

'2010/11/20 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ