C言語の「fflush関数」を解説!知っておくとデバッグにも役立つよ!

fflushの解説ページのアイキャッチ

このページではC言語の fflush 関数について解説します。

fflush 関数?

使ったことないけど困ったことないよ?

プログラム作るだけなら使わなくてもいいからね

だけど、デバッグの時なんかはこの fflush 関数が活躍することあるよ

普段は使う機会がないかもしれませんが、fflush 関数について知っておくとデバッグもしやすくなりますので是非読んでみてくだい。

fflush 関数とは

fflush 関数とは「ストリームをフラッシュする関数」です。

もう少しわかりやすく言うと「ストリームに溜まっているデータを即座に吐き出す関数」です。

ストリーム…?フラッシュ…?

その辺りは追々解説していくよ!

まずは fflush 関数を使うことで動作がどのように違うのかを確認していこう!

fflush 関数を使わない場合と使った時の動作の違い

まずは分かりやすい例でイメージを掴んでいただこうと思います。

下記のプログラムはどのように動作すると思いますか? usleep 関数は引数で指定されたマイクロ秒分プログラムを止める関数です。ですので、おそらく 5 ミリ秒ごとに1つずつ数字が表示される動きを想像した方も多いかと思います。 

nofflush.c
#include <stdio.h>
#include <unistd.h>

#define N 2000

int main(void){
  struct timeval start;
  struct timeval end;

  int i;

  for(i = 0; i < N; i++){
    printf("%d, ", i);
    usleep(5000);
  }

  return 0;
}

実際に実行してみると下記の動画のような動きになりました。全然 5 ミリ秒ごとに数字が表示されませんね。

ある程度時間が経つと一気に数字が表示される様子が分かっていただけると思います。

あ、これなったことある

動作確認用に表示したかっただけだから気にしなかったけど…

もう一つプログラムを用意しました。

上記プログラムの printf 関数の直後に fflush 関数を挿入しただけのものになります。

fflush.c
#include <stdio.h>
#include <unistd.h>

#define N 2000

int main(void){
  struct timeval start;
  struct timeval end;

  int i;

  for(i = 0; i < N; i++){
    printf("%d, ", i);
    fflush(stdout);
    usleep(5000);
  }

  return 0;
}

実行してみると下の動画のように動作します。

ちゃんと 5 ミリ秒ごとに1つずつ数字が出力されている様子が確認できると思います。

2つのプログラムの違いは fflush 関数の有無のみです。

すごい!fflush 関数使うだけで直ったんだね!

でもなんで?

じゃあここからは、その「なんで?」の部分について解説していくよ!

なぜこの fflush 関数を使うだけでこのように動きが変わるのでしょうか?その理由はストリームにあります。

スポンサーリンク

ファイルへの入出力とストリーム

まず「ストリーム」って何?という方は下記ページで解説していますので読んでみると理解していただけると思います。

C言語のファイル入出力の解説ページアイキャッチC言語のファイル入出力について解説

fflush 関数を理解する上でストリームについて知っておいていただきたいポイントは下記の三つです。

  • プログラムとファイルの間にはストリームと呼ばれるバッファがある
  • プログラムからはファイルに直接出力せず、ストリームへデータを書き込む
  • ストリームが満杯になった際などに、ストリームのデータが吐き出されて実際にファイルに書き込まれる(プログラム正常終了時などにも同様にストリームからデータが吐き出される)

つまり、fwrite 関数や fprintf 関数でファイルに書き込んだつもりでも、データがストリームから吐き出されるまでは実際にファイルへは書き込まれないということです。

ファイルストリームの解説図

書き込みに限定して書きましたが、ファイル読み込みの場合も同じようにストリームを使用して処理が行われています。

標準入出力への入出力とストリーム

実はファイルだけでなく、標準入出力への処理においてもこのストリームが使用されています。

標準入出力というのは「デフォルトで設定されている入力元と出力先」のことです。詳細な説明はしませんが、基本的に「標準入力 = キーボード」「標準出力 = コンソール(ターミナルなど)」と考えて良いです。

そして、おそらくみなさんがC言語で最初に使用したであろう printf 関数は「標準出力へ文字列出力」する関数です。つまりコンソールに出力する関数です。

標準出力の場合もストリームが使用されますので、printf 関数においても出力した文字列はいったんストリームに書き込まれ、バッファから吐き出されるまでは画面(コンソール)にその文字列に表示されません。

標準出力ストリームの解説図

つまり、printf 関数を実行したとしても即座に表示されないということです。

これが、fflush 関数を使わない場合と使った時の動作の違いで紹介した nofflush.c で printf 関数の出力した文字列が 5 ミリ秒に表示されなかった理由です。

なるほど!

最初の動画だと 5 ミリ秒毎じゃなくてストリームが満杯になったタイミングで表示されるからあんなガクガクした感じで表示されてたんだね!

そういうこと!

fflush 関数とは即座にデータを吐き出す関数

ではストリームが使用される限り即座にデータをファイルや標準出力に書き込めないかと言うと、そうではありません。

ストリームのデータを即座に吐き出させる関数があります。それが fflush 関数です。

fflush 関数を実行すると、指定したストリームのデータを即座に吐き出させることができます。つまり、fflush 関数を使用することでデータのファイルや標準出力へ出力するタイミングを制御することが可能です。

スポンサーリンク

fflush 関数の定義

fflush 関数は stdio.h で下記のように定義されています。

fflushの定義
#include <stdio.h>
int fflush(FILE *stream);

引数にはストリームへのアドレスを指定します。

fopen でストリームを作成した場合は  fopen 関数の戻り値を指定すれば良いです。

標準出力の場合は stdout を指定します。標準入力等も指定可能で、指定する値と fflush 関数が作用するストリームの一覧は下記のようになっています。

引数作用するストリーム
ファイルストリームのアドレスファイル
stdout標準出力
stdin標準入力
stderr標準(エラー)出力
NULL開いている全ストリーム

ただし、入力ストリームに関しては動作が未定義になっている場合も多いです。使用するのはほぼ次の3つのみだと思います。

  • 書き込みモードで開いたファイルストリームのアドレス
  • stdout
  • stderr

fflush 関数の使いどころ

fflush 関数を使わなくてもいままで不便感じたことないよ?と思われる方も多いと思います。

実はこの関数、なかなか使う機会がないです。

その理由は、わざわざ fflush 関数を使わなくても勝手にストリームからデータが吐き出されるタイミングがたくさんあるからです。

例えば下記のようなタイミングでデータが吐き出されます。

  • プログラム正常終了時
  • “改行” 出力時(標準出力の場合)

なので、わざわざ fflush 関数を使わなくても不便を感じることは少ないと思います。

まあそれでも fflush 関数の使いどころはいくつかありますので、それを紹介していこうと思います。

定期的な(改行なし)文字列の表示

処理時間がかかるループ処理を行なっている時なんかに進行状況を printf 関数で表示する場合に fflush 関数が使えます。

例えば下記のループ処理では、ループが一度実行されるたびに「*」を表示しています。この * が増えるたびに処理が進行していることが確認できるわけです。

ループ処理の進捗表示
for(i = 0; i < N; i++){
  printf("*");
  /* 時間のかかる処理 */
}

ただし、上記のままだとストリームにデータが溜められるだけですので、「ストリームが満杯になる」 or 「プログラムが正常終了する」まで表示が行われませんので、あまり意味がないですよね…。

最初に見た動画みたいな感じで遅れて表示されちゃうんだね…

こんな時に fflush 関数を printf 関数実行直後に挿入することで、狙った通りの動作(つまりループが一度実行されるたびに * を表示する)を実現することができるようになります。

また fflush 関数を使わない場合と使った時の動作の違いで紹介したように、何秒毎かに改行なし文字列を表示するような処理も fflush 関数を使用することで実現することが可能です。

落ちるプログラムのデバッグ

私が fflush 関数をよく使うのはこれですね。「プログラムが途中で落ちてしまった時のデバッグ」の時に fflush 関数を使います。

例えばループの中で落ちてしまう時のデバッグ時によく使います。

ループの中でプログラムが落ちてしまう時には、printf 関数を使ってどのタイミングで落ちているかを確認するのが有効なデバッグの一つだと思います。

落ちるタイミングをprintfで調べる
for(i = 0; i < N; i++){
  printf("%d,", i);
  /* プログラムが落ちるバグが含まれている処理 */
}

しかし、ストリームからのデータの吐き出しは、プログラム正常終了時には実行されますが、プログラムが途中で落ちた後(Segmentation Fault エラーなどで落ちた後)には実行されません。

ですので、上記のように printf 関数を実行してもプログラムが途中で落ちてしまうと、printf で出力しようとした結果が表示されない or 途中までしか表示されないため、どのタイミングで落ちたかが確認できません。

あー、これは経験ある

落ちたタイミング調べたかったのに全然 printf で情報表示してくれないからデバッグに苦労したよ…

そんな時に、printf 関数の直後に fflush 関数を使用し、プログラム落ちる直前までの printf の実行結果を確実に表示するようにすることができます(といってもプログラムが変なタイミングで落ちることもあるので、これだけでは正確に落ちる原因を掴むのは難しいのですが…。ただ手がかりはつかめます)。

スポンサーリンク

fflush 関数の注意点

最後に fflush 関数使用時の注意点について解説します。

ここまでの話を聞いてると、ファイルや標準出力への出力時は毎回 fflush 関数実行すればいいじゃん!と思ったけど、やっぱりデメリットもあるんだね!

その通り!

しっかりデメリットも理解した上で、使いどころを考えるのが需要だよ!

fflush 関数を使うとプログラムの実行速度が低下する可能性があります。

特にファイルストリームに対して fflush 関数を使うと、fflush 関数を使うたびにファイルアクセスが発生します。

そしてファイルアクセスは一般的に処理時間がかかる(特にファイルが HDD 上にあると時間がかかる)ので、fflush 関数を使うとプログラムが遅くなります。

実際に fflush がどれくらい処理速度に影響があるかを見てみましょう。

fflush を一度だけ行う場合の処理速度

下記は fprintf 関数を 1000000 回繰り返すプログラムで、1000000 回の繰り返しが終了した時に fflush を一度のみ実行するプログラムになります。

fflush 関数が実行されるのは一度のみです(ちなみにこのプログラムの場合は fclose 関数実行時にデータが吐き出されるので fflush 関数は本当は不要です。時間の計測をデータを全て吐き出すまでの時間で比較したかったので一応 fflush 関数を実行しているだけです)。

fflushの負荷計測1
#include <stdio.h>
#include <sys/time.h>

#define N 1000000
int main(void){

  struct timeval start;
  struct timeval end;

  int i;
  int usec;

  FILE *fo;

  fo = fopen("out.txt", "w");

  gettimeofday(&start, NULL);

  for(i = 0; i < N; i++){
    fprintf(fo, "%d,", i);
  }

  fflush(fo);
  gettimeofday(&end, NULL);

  usec = (end.tv_sec - start.tv_sec) * 1000000 + (end.tv_usec - start.tv_usec);
  printf("time: %d[us]\n", usec);

  fclose(fo);
  return 0;
}

実行すると下記のように出力され、処理時間が約 180 [ms] であることが確認していただけると思います。

time: 177707[us]

fflushfprintf 毎に行う場合の処理速度

次はfprintf 関数を実行するたびに fflush 関数を実行するプログラムの処理時間を計測してみましょう。

fflushの負荷計測2
#include <stdio.h>
#include <sys/time.h>

#define N 1000000
int main(void){

  struct timeval start;
  struct timeval end;

  int i;
  int usec;

  FILE *fo;

  fo = fopen("out.txt", "w");

  gettimeofday(&start, NULL);

  for(i = 0; i < N; i++){
    fprintf(fo, "%d,", i);
    fflush(fo);
  }

  gettimeofday(&end, NULL);

  usec = (end.tv_sec - start.tv_sec) * 1000000 + (end.tv_usec - start.tv_usec);
  printf("time: %d[us]\n", usec);

  fclose(fo);
  return 0;
}

実行すると下記のように表示され、処理時間が約 3900 [ms] になっています。

time: 3902270[us]

スポンサーリンク

処理時間の比較

つまり、fflush 関数を最後に一回のみ実行するのに比べて処理時間が20倍以上になっているんですね…。

当然出力されるファイルは2つの中身は同じですが、fflush 関数の使用回数によってここまで処理時間が変わることになります。

fflush 関数を使うのはデバッグ時やどうしてもストリームのデータを吐き出し終わってから次の処理を実行したい場合くらいに制限しておいた方が良いです。

まとめ

このページでは fflush 関数について解説しました。

printf 関数での表示が遅い!一度に一気に表示される!などが発生して不便を感じた時に、fflush 関数のことを知っていると fflush 関数を使用することでそれらを解決することができます。

ですので、使いどころはすぐには無いかもしれませんが、ストリームの動作やfflush 関数の動作についての知識はしっかり身に付けておきましょう!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です