C言語編 第43章 バッファリング

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

バッファリングとは

バッファリングとは、データを一旦どこかに蓄えておき、あるタイミングでまとめて処理する方法のことを指します。

例えば、通信分野であれば、送信するデータがある程度溜まってから、まとめて送信した方が、効率が高まります。 送信作業を行うためには、送り主や宛先の情報の付加など、様々な準備が必要になるため、ごく小さなデータを頻繁に送るよりも、 ある程度まとめて送った方が効率的な訳です。

このように、バッファリングの手法によって、より効率的な処理が行える場面はいくつもあります。 この章では、C言語の入出力処理におけるバッファリングについて見ていきます。

C言語の入出力処理においては、以下の3つのタイプが存在することになっています。

まず、フルバッファリング(完全バッファリング)は、与えられたバッファ領域を一杯まで使い切ろうとします。 入出力されるデータは、すべてバッファ領域に蓄えられます。

2つ目はラインバッファリング(行バッファリング)で、これは1行単位でバッファリングを行う方法です。 データはバッファ領域に蓄えられますが、行の終わりを表す文字(=改行)が現れると、それまでに蓄えられていたデータが実際に入出力処理に回されます。

3つ目は、バッファリング無しというパターンです。 この場合、発生した入出力要求はただちに処理されます。

実際にどれが使われているかは環境によって異なりますが、標準エラーに関してはバッファリング無しになっているのが一般的です。 標準エラーがバッファリングされていると、エラーが発生したタイミングですぐに有益なログを書き出すことができなくなってしまうためです。

入出力処理というものは、実はかなりの処理時間を喰っています。 ですから、1文字の読み書きを頻繁に行うよりも、バッファリングしておいて、1度にまとめて行う方が効率的です。 そのため、標準入力や標準出力はバッファリングされている環境が多いです。

バッファリングの設定変更

バッファリングのデフォルト設定は環境によって異なりますが、後から変更することは可能です。 これには、setbuf関数(⇒リファレンス)、 あるいは setvbuf関数(⇒リファレンス)を使います。
ただし実際には、setvbuf関数の方だけ使えば良いはずです。

int setvbuf(FILE* stream, char* buf, int mode, size_t size);

第1引数は、対象のストリームを指定します。
第2引数は、バッファ領域として使用する配列のアドレスか、NULL のいずれかを渡します。 アドレスを渡す場合には、BUFSIZマクロ(⇒リファレンス)で表される値以上のサイズを持った配列を用意しなければなりません。 NULL を指定した場合には、関数内部で自動的に確保されます。
第3引数は、バッファリングのタイプを指定します。 これは _IOFBF(⇒リファレンス)、 _IOLBF(⇒リファレンス)、 _IONBF(⇒リファレンス) のいずれかを指定します。 それぞれ、フルバッファリング、ラインバッファリング、バッファリング無しを表しています。
第4引数は、バッファ領域の大きさを指定します。 第2引数に配列のアドレスを指定したのなら、そのサイズを指定し、NULL を指定したのなら、自動的に確保させるサイズを指定します。 いずれにしても、BUFSIZマクロの値以上のサイズが必要です。
戻り値は、成功時には 0、失敗時には 0以外の値です。

それでは、実際に試してみます。 ただし、バッファリングは環境依存の処理なので、このプログラムが全ての環境で意図通りに動作する保証はありません。

#include <stdio.h>

int main(void)
{
	char buf[80];
	char stdoutBuf[BUFSIZ];


	/* ラインバッファリングに変更 */
	setvbuf( stdout, stdoutBuf, _IOLBF, sizeof(stdoutBuf) );

	printf( "文字列を入力して下さい" ); /* 改行なし */
	fgets( buf, sizeof(buf), stdin );
	printf( "入力内容:%s\n", buf );


	/* バッファリング無しに変更 */
	setvbuf( stdout, NULL, _IONBF, BUFSIZ );

	printf( "文字列を入力して下さい" ); /* 改行なし */
	fgets( buf, sizeof(buf), stdin );
	printf( "入力内容:%s\n", buf );

	return 0;
}

実行結果

Hello
文字列を入力して下さい入力内容:Hello

文字列を入力して下さいHello
入力内容:Hello

fgets関数(⇒リファレンス)の手前にある printf関数(⇒リファレンス)には、 改行文字が含まれていません。 そのため、ラインバッファリングを行っているときには、"文字列を入力して下さい" という文字列は、バッファに蓄えられているだけであり、 出力処理は実行されていません。
そのせいで実行結果のように、キーボードから入力した "Hello" の文字の方が先に画面に表示され、 その後で "文字列を入力して下さい" が表示されています。
入力内容を出力する方の printf関数には、改行文字が含まれているので、このタイミングでまとめて出力処理が実行されています。

一方、バッファリング無しにした方は、"文字列を入力して下さい" が先に表示され、その後に入力中の内容が、 最後に入力内容が表示されています。


このようなバッファリング方法を切り替える処理は、基本的にはほとんど使うことは無いと思われます。 この章で取り上げているのは、知識としてバッファリング処理という存在は知っておくべきだからです。

フラッシュ

ラインバッファリングが行われているとき、改行はしたくないけれど、内容は確実に出力したいというケースもあることでしょう。 そのような場合には、バッファの中身を強制的に吐き出させるフラッシュという操作を行います。 (フラッシュは「光」のことではありません。ここでのフラッシュは「押し流す」とか「噴出」というような意味です。 なお、「光」は flash、ここでのフラッシュは flush です)。

フラッシュを行うには、fflush関数(⇒リファレンス)を使用します。

int fflush(FILE* stream);

引数には、対象のストリームを指定します。 NULL を指定した場合には、現在オープンされている全てのストリームが対象になります。
戻り値は、成功したら 0 、失敗すると EOF(⇒リファレンス) が返されます。

ただし、fflush関数が確実に動作するのは、出力用のストリームだけです。 よく、

fflush( stdin );

このように、標準入力をフラッシュしようとするプログラムを見かけますが、これが正常に動作するかどうかは環境に依存します

では、試してみます。

#include <stdio.h>

int main(void)
{
	char buf[80];
	char stdoutBuf[BUFSIZ];


	/* ラインバッファリングに変更 */
	setvbuf( stdout, stdoutBuf, _IOLBF, sizeof(stdoutBuf) );

	printf( "文字列を入力して下さい" ); /* 改行なし */
	fflush( stdout );
	fgets( buf, sizeof(buf), stdin );
	printf( "入力内容:%s\n", buf );

	return 0;
}

実行結果

文字列を入力して下さいHello
入力内容:Hello

ラインバッファリングを行っており、改行無しで printf関数を呼び出しています。 この場合、前の項でのサンプルプログラムのように、まだ出力が行われませんが、 今回は fflush関数を呼び出すことによって、強制的に書き出しています。

なお、このサンプルプログラムは VisualC++ で試すと、実行結果が壊れているかも知れません。 これは、Windows ではラインバッファリングに変更しても、実はフルバッファリングになってしまう環境があるためです。 フルバッファリングの場合、改行を引き金としてバッファから書き出されるということはないので、最後にもう1度 fflush関数を呼び出さないと、 最後の printf関数の内容が表示されないかも知れません。
ただし、フルバッファリングであっても、対象がファイルであれば fclose関数(⇒リファレンス)でクローズされるときに、 バッファの中身は自動的に書き出されます。

入力を押し戻す

ungetc関数(⇒リファレンス)を使って、 文字をストリームへ押し戻すことができます。 ただし、入力ストリームだけです。

押し戻すというのは要するに、getc関数(⇒リファレンス)などで入力ストリームから受け取った文字を、 ストリームへ返却するということです。 押し戻した後に、再び getc関数などの入力関数を呼び出すと、押し戻した文字が取得できます。

入力ストリームがバッファリングされているかどうか分からないため、何文字まで押し戻せるかは規定されていませんが、 少なくとも 1文字だけは必ず押し戻せます

この関数は、ある文字が現れたら(あるいは、ある文字でない文字が現れたら)、他の処理を行わなければならないときに利用できます。 実際に文字を読み取って(入力ストリームから受け取って)みないことには、次にどんな文字が現れるのか分からない訳ですから、 読み取ってから判断するしかありません。 すると、今読み取った文字は、今はいらないからキャンセルしたいというケースも出てきます。 この「読み過ぎをキャンセルする」という目的で、ungetc関数が利用できます

このような使い方が頻繁に行われるのは、字句解析という分野です。 これは例えば、C言語で書かれたソースコードをコンパイルする過程の中にも現れるもので、 例えば、「a = b + c;」という文を「a」「=」「b」「+」「c」「;」のように分割するような作業を指します。

字句解析の話まで踏み込んでいくと難しくなるので、やめておきますが、ungetc関数の代表的な利用場面は、そういうところにあります。 ただし、別に ungetc関数を利用しなくとも、読み過ぎた文字をどこかの変数に退避させておくという手段での解決も図れる訳ですから、 必須というものでもありません。

一応、動作だけ確認して終わりにします。

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

int main(void)
{
	FILE* fp;
	int c;


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


	/* 1文字目を読み込む */
	c = fgetc(fp);
	printf( "%c", c );

	/* 2文字目を読み込む */
	c = fgetc(fp);
	printf( "%c", c );

	/* 2文字目を押し戻す */
	ungetc( c, fp );

	/* 押し戻した文字が読み込まれる */
	c = fgetc(fp);
	printf( "%c", c );


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

	return 0;
}

入力ファイル (test.txt)

abc

実行結果

abb


練習問題

問題@ 自分の使っている開発環境でのバッファリングの事情や、 コンピュータ関係でのバッファリングという考え方全般について、調べてみて下さい。


解答ページはこちら

参考リンク

更新履歴

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

'2010/6/11 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ