このページではC言語の fflush
関数について解説します。
fflush
関数?
使ったことないけど困ったことないよ?
プログラム作るだけなら使わなくてもいいからね
だけど、デバッグの時なんかはこの fflush
関数が活躍することあるよ
普段は使う機会がないかもしれませんが、fflush
関数について知っておくとデバッグもしやすくなりますので是非読んでみてくだい。
Contents
fflush
関数とは
fflush
関数とは「ストリームをフラッシュする関数」です。
もう少しわかりやすく言うと「ストリームに溜まっているデータを即座に吐き出す関数」です。
その辺りは追々解説していくよ!
まずは fflush
関数を使うことで動作がどのように違うのかを確認していこう!
fflush
関数を使わない場合と使った時の動作の違い
まずは分かりやすい例でイメージを掴んでいただこうと思います。
下記のプログラムはどのように動作すると思いますか? usleep
関数は引数で指定されたマイクロ秒分プログラムを止める関数です。ですので、おそらく 5
ミリ秒ごとに1つずつ数字が表示される動きを想像した方も多いかと思います。
#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
関数を挿入しただけのものになります。
#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言語のファイル入出力について解説fflush
関数を理解する上でストリームについて知っておいていただきたいポイントは下記の三つです。
- プログラムとファイルの間にはストリームと呼ばれるバッファがある
- プログラムからはファイルに直接出力せず、ストリームへデータを書き込む
- ストリームが満杯になった際などに、ストリームのデータが吐き出されて実際にファイルに書き込まれる(プログラム正常終了時などにも同様にストリームからデータが吐き出される)
つまり、fwrite
関数や fprintf
関数でファイルに書き込んだつもりでも、データがストリームから吐き出されるまでは実際にファイルへは書き込まれないということです。
書き込みに限定して書きましたが、ファイル読み込みの場合も同じようにストリームを使用して処理が行われています。
標準入出力とストリーム
実はファイルだけでなく、コンソール画面への文字出力やキーボードからの文字入力においてもストリームが使用されています。
標準入力・標準出力・標準エラー出力という言葉を聞いたことがあるのではないかと思いますが、これらもストリームの一種になります。
ファイルのストリームにおいては、前述の通り、ストリームへのデータの入力元はファイル、ストリームからのデータの出力先もファイルとなります。
それに対し、標準入力においてはデータの入力元はキーボード、標準出力や標準エラー出力においてはデータの出力元は画面(コンソール)であることが多いです(違うこともありますし、リダイレクトなどによって変更することも可能です)。
そして、おそらくみなさんがC言語で最初に使用したであろう printf
関数は「標準出力へ文字列を出力する関数」です。
つまり、printf
関数では画面ではなくストリーム(標準出力)への文字列の出力を行なっています。そして、ストリームが満杯になるなどしてストリームから吐き出されることで、ストリームの出力先となる画面に文字列が表示される事になります。
逆に言えば、ストリームから吐き出されるまでは画面にその文字列は表示されません。
つまり、printf
関数を実行したとしても即座に画面に表示されないということです。
これが、fflush 関数を使わない場合と使った時の動作の違いで紹介した nofflush.c で printf
関数の出力した文字列が 5
ミリ秒に表示されなかった理由です。
なるほど!
最初の動画だと 5
ミリ秒毎じゃなくてストリームが満杯になったタイミングで表示されるからあんなガクガクした感じで表示されてたんだね!
fflush
関数とは即座にデータを吐き出す関数
ではストリームが使用される限り即座にデータをファイルや画面に出力できないかというと、そうではありません。
ストリームのデータを即座に吐き出させる関数があります。それが fflush
関数です。
fflush
関数を実行すると、指定したストリームのデータを即座に吐き出させることができます。つまり、fflush
関数を使用することで、データのファイルや画面への出タイミングを制御することが可能です。
スポンサーリンク
fflush
関数の定義
fflush
関数は stdio.h
で下記のように定義されています。
#include <stdio.h>
int fflush(FILE *stream);
引数にはストリームへのアドレスを指定します。
fopen
でストリームを作成した場合は fopen
関数の戻り値を指定すれば良いです。
標準出力の場合は stdout
を指定します。標準入力等も指定可能で、指定する値と fflush
関数が作用するストリームの一覧は下記のようになっています。
引数 | 作用するストリーム |
ファイルストリームのアドレス | ファイル |
stdout |
標準出力 |
stdin |
標準入力 |
stderr |
標準エラー出力 |
NULL |
開いている全ストリーム |
ただし、入力系のストリームに関しては動作が未定義になっている場合も多いです。使用するのは、ほぼ次の2つのみだと思います。
- 書き込みモードで開いたファイルストリームのアドレス
stdout
fflush
関数の使いどころ
fflush
関数を使わなくてもいままで不便感じたことないよ?と思われる方も多いと思います。
実はこの関数、なかなか使う機会がないです。
その理由は、わざわざ fflush
関数を使わなくても勝手にストリームからデータが吐き出されるタイミングがたくさんあるからです。
例えば下記のようなタイミングでデータが吐き出されます。
- プログラム正常終了時
- “改行” 出力時(標準出力の場合)
なので、わざわざ fflush
関数を使わなくても不便を感じることは少ないと思います。
まあそれでも fflush
関数の使いどころはいくつかありますので、それを紹介していこうと思います。
定期的な(改行なし)文字列の表示
処理時間がかかるループ処理を行なっている時なんかに進行状況を printf
関数で表示する場合に fflush
関数が使えます。
例えば下記のループ処理では、ループが一度実行されるたびに「*」を表示しています。この *
が増えるたびに処理が進行していることが確認できるわけです。
for(i = 0; i < N; i++){
printf("*");
/* 時間のかかる処理 */
}
ただし、上記のままだとストリームにデータが溜められるだけですので、「ストリームが満杯になる」 or 「プログラムが正常終了する」まで表示が行われませんので、あまり意味がないですよね…。
こんな時に fflush
関数を printf
関数実行直後に挿入することで、狙った通りの動作(つまりループが一度実行されるたびに *
を表示する)を実現することができるようになります。
また fflush 関数を使わない場合と使った時の動作の違いで紹介したように、何秒毎かに改行なし文字列を表示するような処理も fflush
関数を使用することで実現することが可能です。
落ちるプログラムのデバッグ
私が fflush
関数をよく使うのはこれですね。「プログラムが途中で落ちてしまった時のデバッグ」の時に fflush
関数を使います。
例えばループの中で落ちてしまう時のデバッグ時によく使います。
ループの中でプログラムが落ちてしまう時には、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
関数を実行しているだけです)。
#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]
fflush
を fprintf
毎に行う場合の処理速度
次はfprintf
関数を実行するたびに fflush
関数を実行するプログラムの処理時間を計測してみましょう。
#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
関数の動作についての知識はしっかり身に付けておきましょう!