【C言語】setvbuf関数やsetbuf関数でストリームのバッファの設定を行う

setvbuf関数やsetbuf関数でのストリームのバッファの設定方法の解説ページアイキャッチ

このページでは、C言語の標準ライブラリ関数である setvbuf 関数と setbuf 関数について解説を行います!

下記ページでの fflush 関数の解説において、プログラムとファイル・コンソール画面等の間にはストリームが存在し、そのストリームでデータがバッファリングされる(データが一時的に溜められる)ことを説明しています。

fflushの解説ページのアイキャッチC言語の「fflush関数」を解説!知っておくとデバッグにも役立つよ!

なぜストリームでデータを溜めることが出来るかというと、これはストリームがデータを溜めるためのメモリを持っているからです。このメモリのことをバッファと呼びます。このバッファにデータが溜められ、バッファが満杯になる等のタイミングでデータがファイルやコンソール画面等に出力される事になります。

ストリームのバッファの説明図

上記ページで紹介している fflush 関数は、そのバッファ内のデータをファイル等に強制的に出力する関数となります。

fflush関数の説明図

今回紹介する setvbuf 関数と setbuf 関数は、このストリームのバッファの設定を行う関数になります。

setvbuf関数とsetbuf関数の説明図

この設定を行う事により、ストリームに溜められるデータのサイズを変更したり、ストリームでのデータのバッファリングの仕方(データの溜め方)を変更したりすることができるようになります。

setvbuf 関数

まずは setvbuf 関数について解説していきます。

前述の通り、setvbuf 関数はストリームのバッファの設定を行う関数であり、下記のように宣言されています。

setvbuf関数
#include <stdio.h>

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

setvbuf 関数でのバッファの設定(引数)

setvbuf 関数でのバッファの設定は、setvbuf 関数に指定する引数に応じて行われます。

ということで、ここでは setvbuf 関数の引数について解説していきます。

stream

setvbuf 関数の第1引数 stream には、バッファの設定を行いたいストリームを指定します。

stream への指定例として分かりやすいのが fopen 関数の返却値ですね。これによりプログラムとファイル間のストリームのバッファの設定を行うことができます。

また、標準入力・標準出力・標準エラー出力もストリームであり、これらのバッファの設定も行うことが可能です。これらのバッファの設定を行う際には、stream に下記を指定します。

  • stdin:標準入力
  • stdout:標準出力
  • stderr:標準エラー出力

例えば printf 関数は標準出力に文字を出力する関数ですので、streamstdout を指定すれば、printf 関数実行時に使用されるバッファの設定を変更することができ、printf 関数による文字の表示の動作を変化させることができます。

type

順番前後しますが、setvbuf 関数の第3引数 type には、ストリームでのバッファリングの仕方を指定します。

type に指定できるパラメータは下記の3つであり、これらは stdio.h をインクルードすることで使用できる定数マクロとなります。

  • _IOFBUF:フルバッファリング
    • バッファが満杯になるまでバッファリングを行う
  • _IOLBUF:ラインバッファリング
    • バッファに改行文字が入力されるまで or バッファが満杯になるまでバッファリングを行う
  • _IONBUF:バッファリングなし

例えば _IOLBUF を指定した場合は、バッファに改行文字が入力された or バッファが満杯になったタイミングでストリームの接続先にバッファからデータが吐き出され、その後はまたバッファの先頭からデータがバッファリングされていく事になります。

ただし、type を指定したからといって、必ず指定した通りにバッファリングされるとは限らないので注意してください。

例えば type_IOFBUF を指定していたとしても、途中で入力処理が行われたりすると、バッファが満杯になる前にバッファからデータが吐き出される事になります。

buf

setvbuf 関数の第2引数 buf には、ストリームで使用するバッファの先頭アドレスを指定します。

これにより、第3引数 type_IONBUF 以外を指定している場合、ストリームへ出力されたデータが buf で指定されたバッファにバッファリングされる事になります(第2引数 type_IONBUF を指定している場合は、バッファへのバッファリングは行われません)。

buf には、例えば配列の先頭アドレスや malloc 関数等で確保したメモリの先頭アドレスを指定します。

第3引数 type_IONBUF 以外を指定する場合、buf に指定する配列やメモリのサイズは第4引数で指定する size 以上である必要があります。setvbuf 関数ではアドレス bufbuf + size - 1  をバッファとみなしてバッファリングを行いますので、buf に指定する配列やメモリのサイズが size よりも小さい場合、バッファオーバーランが発生する事になります。

また、バッファリングなしに設定するために bufNULL を指定するのは間違いなので注意してください。バッファリングなしの設定は、前述の通り第3引数 type への _IONBUF の指定により行う必要があります。

bufNULL を指定した場合、第4引数 size に応じたメモリが関数内部で確保され、そのメモリがバッファとして利用されるようになるはずです(環境によって動作が異なるかも)。

size

setvbuf 関数の第4引数 size には、ストリームのバッファのサイズをバイト単位で指定します。

基本的には setvbuf 関数の第2引数に指定する配列やメモリのサイズを指定します(それらよりも小さなサイズでも問題ない)。

size0 を指定した場合、第2引数 buf で指定したバッファではなく、元々ストリームに対して用意されるバッファが使用される事になります。

ですので、例えばあるストリームに対してバッファリングの仕方(type)だけ変更したいような場合は、第4引数 size0 を指定してやれば良いです(第2引数も NULL で良い)。

スポンサーリンク

setvbuf 関数の返却値

setvbuf 関数は成功時に 0 を、失敗時には 0 以外を返却します。

例えば、第3引数 type_IOFBUF_IOLBUF_IONBUF 以外を指定した場合などに setvbuf 関数は 0 以外を返却します。

setvbuf 関数の使用例と効果

続いて setvbuf 関数の使用例を示しながら setvbuf 関数の効果を紹介していきまます。

setvbuf 関数を使用しない場合の printf の出力

まずは、setvbuf 関数を使用しない場合の printf 関数の動作を確認しておきたいと思います。

下記は、setvbuf 関数を使用せずに、500 ミリ秒毎に printf 関数を実行して数字を出力し、5文字出力するたびに改行を出力するプログラムとなります。

usleep 関数を使っているので Windows 環境ではコンパイルできないかもしれないですが、Windows 用のスリープ関数を利用すれば同様のプログラムは実現できるはずです。

setvbuf関数を使用しない例
#include <stdio.h>
#include <unistd.h>

int main(void) {

    for (int i = 0; i < 20; i++) {
        printf("%d", i);

        if (i % 5 == 4) {
            printf("\n");
        }

        usleep(500000);
    }
    printf("\n");
}

私の環境で実行した場合の実行結果は下のアニメのようになりました。

デフォルトのバッファを利用した際のprintfでの出力の動作を示すアニメ

改行文字 \nprintf で出力される度に、そこまで printf で出力していた数字が一度に表示されることが確認できると思います。

もしかしたら環境によっては動作が異なるかもしれませんが、少なくともこの動作は、私の環境におけるストリームのバッファのデフォルト設定での動作という事になります(バッファリングの仕方がラインバッファリングに設定されている)。

setvbuf 関数でのバッファリングの仕方の設定変更

ここからは setvbuf 関数を実行して setvbuf 関数の効果を確認していきたいと思います。

まずは下記のソースコードを利用して setvbuf 関数の効果を確認してみましょう!

setvbuf 関数を使用しない場合の printf の出力 の時と printf の実行の仕方などは同じですが、今回は setvbuf 関数を実行しています。この例におけるポイントは、setvbuf 関数の第3引数 type_IOFBF を指定している点になります。

setvbuf関数でのバッファの設定1
#include <stdio.h>
#include <unistd.h>

int main(void) {
    
    if(setvbuf(stdout, NULL, _IOFBF, 0) != 0 ){
        printf("setvbuf error\n");
    }

    for (int i = 0; i < 20; i++) {
        printf("%d", i);

        if (i % 5 == 4) {
            printf("\n");
        }

        usleep(500000);
    }
    printf("\n");
}

今回は、私の環境で実行した場合の実行結果は下のアニメのようになりました。

バッファリングの仕方を古バッファリングに変更した際のprintfでの出力の動作を示すアニメ

setvbuf 関数を使用しない場合の printf の出力 の時とは異なり、改行 \nprintf で出力されても数字は出力されず、最後(プログラム終了時)に一気に printf で出力された数字や改行が表示されていることが確認できると思います。

このように printf で出力される文字の表示のされ方が変化したのは、下記のように setvbuf 関数を実行しているからになります。

setvbuf関数の実行例1
setvbuf(stdout, NULL, _IOFBF, 0)

第1引数 streamstdout を指定しているため、上記の setvbuf 関数の実行では標準出力ストリームのバッファの設定が行われる事になります。printf 関数は標準出力への出力を行う関数ですので、printf 関数の出力先となるバッファの設定が行われる事になります。

さらに、第3引数 type_IOFBF を指定しているため、バッファリングの仕方がフルバッファリングに設定されます。すなわち、バッファが満杯になるまでバッファリングされるようになったため、途中で改行文字がバッファに入力されたとしてもデータが吐き出されないように動作が変化する事になります。

上記のバッファの設定により、printf 関数の出力する文字に関わらずバッファが満杯になるまでデータが溜められるようになったため、printf で出力される文字の表示のされ方が変化したというわけです。

ちなみに、上記のプログラムではバッファは結局満杯にならないので、プログラム終了時に溜められたデータが一気に吐き出されることになります。

setvbuf 関数でのバッファのサイズの設定変更

次は、下記のソースコードを利用して setvbuf 関数の効果を確認していきたいと思います。

setvbuf 関数でのバッファリングの仕方の設定変更 に比べて、setvbuf 関数実行時の第2引数と第4引数の指定を変更しています。

setvbuf関数でのバッファの設定2
#include <stdio.h>
#include <unistd.h>

#define SIZE 2

int main(void) {
    static char buffer[SIZE];
    
    if(setvbuf(stdout, buffer, _IOFBF, sizeof(buffer)) != 0 ){
        printf("setvbuf error\n");
    }

    for (int i = 0; i < 20; i++) {
        printf("%d", i);

        if (i % 5 == 4) {
            printf("\n");
        }

        usleep(500000);
    }
    printf("\n");
}

今回は、私の環境で実行した場合の実行結果は下のアニメのようになりました。

バッファのサイズを2バイトに変更した際のprintfでの出力の動作を示すアニメ

setvbuf 関数を使用しない場合の printf の出力setvbuf 関数でのバッファリングの仕方の設定変更 の時とは異なり、今回は2文字(改行文字も含めて)ずつ表示が行われるようになったことが確認できると思います。

これは、setvbuf の第4引数 size の指定によりバッファのサイズが 2 バイトに設定されたからです(sizeof(buffer)2 となる)。

なので、printf 関数で1バイトの文字を2つ出力するだけでバッファから画面に文字が吐き出される事になり、2文字ずつ文字が画面に表示される動作となります。

当然、2バイトのデータを溜める必要があるため、setvbuf の第2引数 buf には、サイズが2バイト以上となる配列やメモリの先頭アドレスを指定する必要があります。

今回は、配列 buffer を第2引数 buf に指定しており、配列 buffer はサイズが2バイト(型が char で要素数が 2)であるため、2バイトのデータを溜め込むことが可能となります。

今回は2文字ずつ文字が表示される様子を示したかったのでバッファのサイズを小さくしましたが、もちろんもっと大きなバッファにデータを溜めるように設定することも可能ですし、逆に第3引数に _IONBF を指定してバッファリングしないように設定することも可能です。

setvbuf 関数の注意点

setvbuf 関数の最後の解説として、setvbuf 関数使用時の注意点について説明しておきます。

実行するタイミング

setvbuf 関数使用時の注意点の1つ目は、setvbuf 関数の実行タイミングです。

setvbuf 関数は、第1引数 stream で指定するストリームに対し「オープンされてから入出力等で使用されるまで」のタイミングで実行する必要があります。

例えば streamfopen の返却値を指定するのであれば、fopen 実行してストリームをオープンしてから fwritefreadfprintf 等の関数によってストリームに対して入出力を行うまでの間のタイミングで setvbuf 関数を実行する必要があります。

そのため、fopen の直後に setvbuf 関数を実行するのが無難だと思います。

また、標準入力・標準出力・標準エラー出力に関してはプログラム実行時に自動的にオープンされますので、streamstdoutstdinstderr を指定する場合は、最初に printfscanf 関数等を実行する前に setvbuf 関数を実行する必要があります。

そのため、この場合はプログラム開始直後に setvbuf 関数を実行するのが無難だと思います。

バッファの生存期間

setvbuf 関数使用時の注意点の2つ目は、setvbuf 関数の第2引数 buf で指定するバッファの生存期間です。

setvbuf 関数の第2引数 buf で指定するバッファは、setvbuf 関数実行後から第1引数 stream で指定したストリームがクローズされるまでの期間で使用される可能性があります。

そのため、buf に指定するバッファは、stream に指定するストリームがクローズされるまで生存している必要があります。

具体例を示すと、下記は良くない setvbuf 関数の使用例となります。なぜなら、buf に指定するバッファは setting 関数終了後に生存期間が終了して解放されてしまうからです。

解放後の配列やメモリは使用してはいけません。

誤ったバッファの設定
#include <stdio.h>

void setting(void) {
    char buffer[256];

    if (setvbuf(stdout, buffer, _IOLBF, sizeof(buffer))) {
        printf("setvbuf error\n");
    }
}

void print(void) {
    char data[1024];

    for (int i = 0; i < 1024; i++) {
        data[i] = i % 26 + 'A';
    }
}

int main(void) {

    setting();
    printf("hello world!\n");
    print();
    printf("hello world!\n");
}

どのように変数がメモリ上に配置されるかにもよりますが、私の環境で上記を実行すると次のように Segmentation fault が発生してしまいました。解放後の配列がバッファに設定されているので、printf が実行されると解放後の配列にデータを格納する事になり、メモリ破壊が発生してプログラムが正常に動作できなくなってしまいます。

% ./main.exe 
hello world!
zsh: segmentation fault  ./main.exe

特に streamstdoutstdinstderr を指定する場合、これらがクローズされるのはプログラム終了時ですので、プログラム終了時まで buf に指定するバッファは生存していなければならない事になります。

そう考えると、buf に指定するバッファに関しては、例えば下記を利用するのが良いと思います。

  • グローバル変数の配列
  • static ローカル変数の配列

これらの配列の生存期間はプログラム終了までとなりますので、前述の例のように途中で解放されてプログラムが異常終了してしまうような現象を防ぐことができます。

スポンサーリンク

setbuf 関数

続いて setbuf 関数について解説していきます。

setbuf 関数は setvbuf 関数の簡易版みたいなものですので、ここでも簡単にのみ解説させていただきます。

まず setbuf 関数は、setvbuf 関数同様にストリームのバッファの設定を行う関数で、下記のように宣言されています。

setbuf関数
#include <stdio.h>

void setbuf(FILE *stream, char *buf);

setbuf 関数の返却値は「なし」です。また上記の通り、setvbuf 関数と比べて引数が少なく、設定できることも少なくなっています。

setbuf 関数でのバッファの設定(引数)

setbuf 関数でも、setvbuf 関数同様に指定する引数に応じてバッファの設定が行われる事になります。

setbuf 関数で指定する引数は下記の通りになります。

stream

setbuf 関数の第1引数 stream には、バッファの設定を行いたいストリームを指定します。

setvbuf 関数の第1引数 stream と同じ引数となりますので、説明は省略します。

buf

setbuf 関数の第2引数 buf には、ストリームで使用するバッファの先頭アドレスを指定します。

setvbuf 関数の第2引数も buf ではあるのですが、指定の仕方等が若干異なるので注意してください。

まず、setbuf 関数の第2引数 bufNULL を指定した場合、ストリームのバッファリングの仕方がノンバッファリングに設定されます。つまり、そのストリームではバッファリングされなくなります。

逆に第2引数 bufNULL 以外を指定した場合は、ストリームのバッファリングの仕方はおそらくフルバッファリングに設定されるはずです。

これらに対し、setvbuf 関数の場合は第2引数 buf の指定によってバッファリングの仕方が変化することはありません。ここが setbuf 関数と setvbuf 関数とで異なるので注意してください。

また、setvbuf 関数の場合はバッファのサイズを第4引数 size で指定できるのに対し、setbuf 関数の場合はバッファのサイズを指定することはできません。setbuf 関数では、バッファのサイズは BUFSIZE バイトであることを前提にバッファの設定が行われます(BUFSIZEstdio.h をインクルードすることで使用できる定数マクロ)。

したがって、setbuf 関数の場合、第2引数で指定する配列やメモリのサイズは BUFSIZE バイト以上である必要があります。これよりもサイズが小さい場合、バッファオーバーランが発生する可能性があるので注意してください。

setbuf 関数の使用例

簡単に使用例も紹介しておきます。

下記は、setbuf 関数を利用してバッファリングの仕方をノンバッファリングに設定する例となります。

setbuf関数の利用例
#include <stdio.h>

int main(void) {

    static char buffer[BUFSIZ];

    setbuf(stdout, NULL);

    /* 略 */
}

動作例の紹介は省略させていただきますが、バッファリングの仕方がノンバッファリングに設定されるため、以降で実行する printf では1文字ずつ即座に画面に出力される事になります。

スポンサーリンク

setbuf 関数の注意点

setbuf 関数においても setvbuf 関数と同じ注意点が挙げられます。詳細に関しては setvbuf 関数の注意点 を参照してください。

また、setbuf 関数の場合、第2引数の指定によってバッファリングの仕方が設定される事になります。この点は setvbuf 関数と異なるため注意してください。

setvbuf 関数と setbuf 関数の使いどころ

最後に setvbuf 関数と setbuf 関数の使いどころについて説明しておきます。

バッファリングしたくない場合

使いどころの1つ目は、ストリームのバッファリングをしたくない場合が挙げられます。

ストリームではバッファリングが行われるため、例えば printf などの出力結果が画面に表示されるのが遅れます。そして、この遅れがあるためにデバッグしにくいというケースもあり得ます(ストリームにデータが溜まっている状態でプログラムが落ちてしまい、溜まっていたデータが出力されない等)。

そんな時に、setvbuf 関数や setbuf 関数を利用してバッファリングの仕方をノンバッファリングに変更することで、即座に printf などの出力結果が画面に反映されるようになり、デバッグや動作確認をしやすくすることができます。

下記ページで紹介している fflush 関数でもストリームから強制的にデータを吐き出させることで同様のことを実現することは可能です。

fflushの解説ページのアイキャッチC言語の「fflush関数」を解説!知っておくとデバッグにも役立つよ!

ただ、fflush 関数の場合はストリームから強制的にデータを吐き出させたいタイミングで毎回 fflush 関数を実行する必要がありますので、そういったタイミングが多い場合はコーディングが大変です。

それに対し、setvbuf 関数や setbuf 関数に関しては、最初に一度実行するだけでストリームのノンバッファリングを実現することができます。ただ、fflush 関数に比べるとちょっと引数の指定がややこしいですかね…。

スポンサーリンク

パフォーマンスを向上させたい場合

また、大量に出力を行うようなプログラムであれば、バッファのサイズやバッファリングの仕方の変更によりプログラムのパフォーマンスを向上させられる可能性があります。

例えば標準出力のバッファのサイズが小さい場合、printf が実行されるたびに画面への出力が行われて負荷が大きくなってしまい、パフォーマンスが低下する可能性があります。

そういった場合、バッファのサイズを大きくすることで画面への出力回数を減らし、パフォーマンスを向上することができる可能性があります。

ただし、これはデフォルトのバッファのサイズ(BUFSIZE)が小さい時のみの話で、デフォルトのバッファのサイズが十分大きい場合はパフォーマンスはほぼ向上しないと思います。

また、標準出力のバッファリングの仕方がラインバッファリング or ノンバッファリングの場合、フルバッファリングに変更することで画面への出力回数を減らし、パフォーマンスを向上させることができる可能性があります。

ただ、パフォーマンスが求められるようなプログラムにおいて、そもそもパフォーマンスが低下するほど printf を実行するのは実装上問題アリと考えて良いかもしれないですね…。

デバッグ時のみ printf を行うようにしたり、不要な printf を行わないよう検討してみても良いと思います。

まとめ

このページでは、C言語の標準ライブラリ関数である setvbuf 関数と setbuf 関数について解説しました!

これらの関数を利用することで、ストリームのバッファの設定を行うことができます。

なかなか使う機会は少ないかもしれませんが、printf 等の動作を変化させることのできるおもしろい関数だと思います。また、バッファリングさせずに printf での文字出力を行いたいような場面に今後出会う可能性もありますので、こういった関数が存在することだけでも覚えておくと良いと思います!

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。