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

先頭へ戻る

この章の概要

この章の概要です。


バッファリングとは

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

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

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

例えば、出力を考えてみます。 printf関数puts関数などを使って出力を行うとき、もしバッファリングが行われていたら、即座に画面などの具体的な場所にデータを送らず、一旦、バッファに、出力すべきデータを蓄えておくだけにします。そして、何らかのタイミングをもって、バッファに蓄えられているデータを、画面などの具体的な場所へ送ります。

入力の場合も考えてみます。例えば、fgets関数で標準入力からの入力を受け取るとします。fgets関数は、1行分のデータを求めている訳ですが、もしバッファリングが行われていたら、バッファにすでに入力データが蓄えられていないかどうかを確認します。そこに十分なデータがあれば、そこからデータを持ってきます。バッファに十分なデータがないのなら、現実の入力装置(キーボードなど)からの入力を待ち受けます。

C言語では、ストリームをバッファリングするかどうかに関して、3つの方針のいずれかを取るようになっています。

1つ目は、フルバッファリング(完全バッファリング)です。 この方針では、そのストリームで行われるすべての入出力が、バッファを経由して行われます。 そして、バッファが一杯になった段階で、バッファに蓄えられているデータを放出するように動作します。

2つ目は、ラインバッファリング(行バッファリング)です。 この方針では、そのストリームで行われるすべての入出力が、バッファを経由して行われます。 ただし、改行文字が現れた段階で、バッファに蓄えられているデータを放出するように動作します。

3つ目は、バッファリング無しです。 この方針では、そのストリームで行われるすべての入出力は、バッファを経由せずに行われます。

どれを選択するかについては、処理系が決めることになっています。 一般的には、標準入力と標準出力はバッファリングされていて(完全か行かは明確でない)、 標準エラーはバッファリング無しであることが多いです。

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

バッファリングの方針は処理系によって異なりますが、後から変更することができるかもしれません。これには、setbuf関数、あるいは setvbuf関数を使います。

setbuf関数と、setvbuf関数は、<stdio.h> に以下のように宣言されています。

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

setbuf関数は、以下のように呼び出した setvbuf関数と同等です。

setvbuf(stream, buf, _IOFBF, BUFSIZ);

また、戻り値もなく結果が分からないので、setvbuf関数の方だけを使えば良いはずです。

setvbuf関数の第1引数は、対象のストリームを指定します。

第2引数は、バッファとして使用する配列のメモリアドレスか、ヌルポインタのいずれかを渡します。メモリアドレスを渡す場合には、BUFSIZ で表される値以上の大きさを持った配列を指定しなければなりません。ヌルポインタを指定した場合には、setvbuf関数の中で自動的に確保されます。バッファを用意して渡す場合には、ストリームを使っている間、そのバッファが存在し続けなければなりません。
第3引数が、_IONBF の場合には無視されます。

第3引数は、バッファリングのタイプを指定します。これは、_IOFBF_IOLBF_IONBF のいずれかを指定します。それぞれ、フルバッファリング、ラインバッファリング、バッファリング無しを表しています。

第4引数は、バッファの大きさを指定します。 第2引数に配列のメモリアドレスを指定したのならば、その大きさを指定し、ヌルポインタを指定したのなら、自動的に確保させる大きさを指定します。 いずれにしても、BUFSIZ の値以上の大きさが必要です。
第3引数が、_IONBF の場合には無視されます。

戻り値は、成功時には 0、失敗時には 0以外の値です。

それでは、実際に試してみます。

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

#define BUFFERING_MODE  _IOFBF

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

    if( setvbuf( stdin, stdinBuf, BUFFERING_MODE, sizeof(stdinBuf) ) != 0 ){
        fputs( "stdin のバッファリングを変更できませんでした。\n", stderr );
        exit( EXIT_FAILURE );
    }
    if( setvbuf( stdout, stdoutBuf, BUFFERING_MODE, sizeof(stdoutBuf) ) != 0 ){
        fputs( "stdout のバッファリングを変更できませんでした。\n", stderr );
        exit( EXIT_FAILURE );
    }

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

    return 0;
}

setvbuf関数の第3引数を、_IOFBF、_IOLBF、_IONBF のそれぞれに変えたとき、実行結果は次のようになります。 入力は "Hello[改行]" としています。

実行結果(_IOFBF:フルバッファリングの場合)

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

実行結果(_IOLBF:ラインバッファリングの場合)

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

実行結果(_IONBF:バッファリング無しの場合)

文字列を入力して下さいHello  <-- "Hello" は入力した内容
入力内容:Hello

ただし、バッファリングは環境依存の処理なので、このプログラムが全ての環境で同じ結果になる保証はありません。 例えば、VisualStudio では、ラインバッファリングを指定しても、フルバッファリングになる環境があります。

最初に呼び出している printf関数は、改行文字を含まない文字列を出力しています。バッファリングされている場合にはバッファに格納され、バッファリング無しなら即座に出力されます。そのため、バッファリングされている場合には、入力を促すメッセージが表示されないまま、fgets関数の呼び出しへ進みます。

fgets関数は、標準入力から1行分のデータを受け取ろうとします。 バッファリングされている場合には、バッファからデータを得ようとしますが、何もないので、実際の入力装置からの入力を待ち受けます。 バッファリング無しの場合も、実際の入力装置からの入力を待ち受けます。

"Hello[改行]" という入力を行います。 バッファリングされている場合には、バッファにデータが入ります。 fgets関数が必要としているのは、改行文字までの1行分のデータなので、フルバッファリングかラインバッファリングかによらず、 fgets関数はこの入力データを取り出し、変数buf に格納します。
バッファリング無しの場合は、単にバッファを経由しないだけであり、同様に、変数buf に入力データが格納されます。

続いて、printf関数が "入力内容:%s\n" を出力しようとします。 バッファリングされている場合には、元々バッファに入っていた "文字列を入力して下さい" の末尾に追記されます。 フルバッファリングの場合、まだバッファが一杯にならないので出力されません。 ラインバッファリングの場合、改行文字が現れたので、この段階で出力が行われます。
バッファリング無しの場合は、即座に出力されます。

フルバッファリングの場合、バッファが一杯にならないと放出されないので、このプログラムの実行結果はおかしいように思えます。 用意したバッファの大きさに比べて、出力しようとした量は少ないので、 何も出力されないままプログラムは終了しないのでしょうか?

フルバッファリングであっても、きちんとすべての出力が行われているのは、 main関数から抜き出すときに、バッファに取り残されていた内容を放出することが規定されているためです。

ただし、注意しなければならないのは、バッファを自前で用意して setvbuf関数に渡している場合です。そのバッファが、自動記憶域期間を持っていると、main関数を抜き出した直後には、その存在が保証されませんから、未定義の動作になってしまいます。そのため、先ほどのサンプルプログラムのように static指定子を付けるか、グローバル変数にして、静的記憶域期間を持たせておく必要があります。

なお、exit関数を呼び出してプログラムを終えた場合も、バッファの内容は放出されます。

main関数の初回の呼び出しから戻るときの動作は、return文で指定する戻り値と同じものを、exit関数の実引数に指定したときの動作と同じであると規定されています。そして、exit関数は、バッファに取り残された内容を放出し、開かれたままのストリームを閉じることを保証しています。
なお、main関数の「初回」の呼び出しとしたのは、C言語では、main関数が再帰呼び出し(第53章)できるからです。

一方、assertマクロによって、プログラムが強制停止させられた場合には、バッファ内容の放出が行われる保証がありません。これは、assertマクロがプログラムを停止させる際に呼び出される、abort関数の仕様です。


バッファリングの方法を切り替える処理は、基本的にはあまり使うことは無いと思われます。しかし、知識としてバッファリング処理については知っておく必要はあります。入力や出力の関数を呼び出すプログラムを書いていて、実際にその関数が呼び出されていることも確実なのに、実際の入出力結果に現れないという現象に遭遇することがあるかもしれません。そのようなとき、バッファリングを思い出して下さい。バッファに蓄えられたデータを放出するタイミングはいつでしょう?

フラッシュ

バッファに蓄えられたデータは、フルバッファリングならバッファが一杯になったとき、 ラインバッファリングなら改行文字が現れたときに、実際の入出力装置へ放出されます。 これ以外のタイミングであっても、強制的に放出させる方法があります。 このような操作を、フラッシュといいます。 ここでのフラッシュは「光 (flash)」ではなく、「押し流す (flush)」です。

フラッシュを行うには、fflush関数を使用します。fflush関数は、<stdio.h> に以下のように宣言されています。

int fflush(FILE* stream);

引数には、対象のストリームを指定します。 ヌルポインタを指定した場合には、現在オープンされている全ての出力のストリームが対象になります。

戻り値は、成功したら 0 、失敗すると EOF が返されます。

fflush関数が確実に動作するのは、出力のストリームに対してだけです。 よく、以下のように、入力のストリームをフラッシュしようとするプログラムを見かけますが、 これが正常に動作するかどうかは規定されていません。

fflush( stdin );

では、試してみます。

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

#define BUFFERING_MODE  _IOFBF

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

    if( setvbuf( stdin, stdinBuf, BUFFERING_MODE, sizeof(stdinBuf) ) != 0 ){
        fputs( "stdin のバッファリングを変更できませんでした。\n", stderr );
        exit( EXIT_FAILURE );
    }
    if( setvbuf( stdout, stdoutBuf, BUFFERING_MODE, sizeof(stdoutBuf) ) != 0 ){
        fputs( "stdout のバッファリングを変更できませんでした。\n", stderr );
        exit( EXIT_FAILURE );
    }

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

    return 0;
}

前の項のサンプルプログラムと同じ形ですが、今回は fflush関数を使って、強制的にフラッシュしています。 setvbuf関数の第3引数を、_IOFBF、_IOLBF、_IONBF のいずれにしても、同じ結果になります。

実行結果:

文字列を入力して下さいHello  <-- "Hello" は入力した内容
入力内容:Hello


入力を押し戻す

ungetc関数を使って、文字をストリームへ押し戻せます。ただし、入力のストリームに限られます。押し戻された文字は、次回、そのストリームから読み取りを行ったときに、最初に取り出されます。

押し戻しは、そのストリームのバッファリングの有無とは無関係に行えますが、保証されているのは、1文字分だけです。2文字以上押し戻すことができるかどうかは、環境に依存します。

ungetc関数は、<stdio.h> に以下のように宣言されています。

int ungetc(int c, FILE* stream);

第1引数に押し戻す文字を、第2引数にストリームを指定します。

戻り値は、押し戻す操作に成功した場合はその文字を、失敗した場合は EOF を返します。

なお、押し戻された文字を取り出さないまま、fseek関数などを使ってシークさせた場合、その文字は失われます。また、押し戻したからといって、実際の入力装置やファイルの内容にも反映される訳ではありません

動作を確認してみましょう。

#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

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

別に ungetc関数を利用しなくとも、読み過ぎた文字をどこかの変数に退避させておくという手段での解決も図れる訳ですから、必須というものでもありませんが、プログラムの種類によっては便利に使えることもあるかもしれません。


練習問題

問題① 標準出力へ書き出す内容がバッファに残されている状態で、main関数の終了、exit関数による終了、abort関数による終了がそれぞれどのような結果になるか確認して下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/18 全面的に文章を見直し、修正を行った。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

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

'2010/6/11 新規作成。



前の章へ(第42章 バイナリファイルの読み書き)

次の章へ(第44章 ファイルに対する操作)

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー