見出し画像

続C言語教室 - 第10回 ストリーム入出力についての補足

入出力、ファイルや画面・キーボードとのやり取りは、これが無ければ何かを計算するにしても、どんな値を使えば良いのか毎回コードに埋め込むわけにもいきませんし、結果を確認することも出来ません。そんな訳でプログラミングの半分くらい(それ以上かも)は、この入出力をどうするかに費やされるわけで、実に多くの機能と注意点があります。

今までストリーム入出力として分類される入出力のライブラリ関数について説明してきましたが、これらの関数が実際にディスクなどから読み書きするための「低レベル入出力」とも言われるシステム・コール(OSが用意している呼び出し)を呼ぶライブラリ関数もあります。こちらについて説明する前に、ストリーム入出力で登場するいろいろな関数も見ておきましょう(※低レベルって何か失礼な感じもしますが、CPUに近いところという意味の低レベルです。ヒトに近いのが高レベルなんですが、レベルが高くても偉いということはありません)。

これまでに登場した関数としては、fopen と fclose、fprintf に fscanf そして fgets や fputs などと fread、fwrite といった辺りですが、これらに共通するのは fopen で得られたファイル構造体へのポインタでファイルを区別するという点です。このファイル構造体には低ベル入出力でファイルを識別するためのファイル記述子や一時的にデータを蓄えるバッファ、現在の読み書き位置、そしてストリームの状態についての情報が入っています。オープンしてしまえば、もうファイル名などの情報はコードの上で登場することはなく、実際、オープンしてから(変更できるのならですが)ファイル名を変更しても(出来るかはOSによる)影響はありません。

C言語の標準入出力

ということで、今まで登場しなかったいくつかの関数について補足しておくことにします。最初はファイルの読み書き位置に関するもので、ftell を呼ぶ事で、その位置を知ることができます。また fseek を使えば任意の位置に移動させることも可能です。fseekで先頭を指定する場合であれば frewind も使えます(なお、ここで登場するライブラリ関数はすべて stdio.h で定義されています)。

long ftell(FILE *fp);

int fseek(FILE* fp, long offset, int origin);

originはSEEK_SET(先頭からの位置)、SEEK_CUR(現在の位置)、SEEK_END(末尾からの位置)のいずれか

void rewind(FILE *fp);

これらはファイルの長さが long に収まらない処理系の場合、大きなファイルが扱えなくなるという問題があるので、そういった場合に同じ機能で型の違う似た名前の関数を使うケースがあります(VCの_ftelli64や_fseeki64など)。これを嫌うのであれば読み書き位置として fpos_t という型を使う fgetpos または fsetpos を使います。

int fgetpos(FILE *fp, fpos_t *pos);

int fsetpos(FILE *fp, fpos_t *pos);

ファイル位置を知る時に、これらを混ぜてしまうと駄目でどちらかだけを使う必要があります。どうやらテキストモードで文字列が置換される場合などにファイル位置の数値が違うようです(あんまり細かく調べていない)。

また現在の読み書き位置がファイルの最後に到達していていれば EOF(End Of File)という状態になり、fgetc などでは、この値が戻ってくることもありますが、これを調べるために feof という関数を使うことも出来ます(読み書き位置を変更するとEOF状態がクリアされることに注意)。

int feof(FILE *fp);

戻り値はEOF状態であればTRUE(非ゼロ)

またストリームの状態がエラーになっているかは ferror で確認できます。

int ferror(FILE *fp);

一度、エラーになってしまうと、clearerr を呼ぶまで、その状態が継続します。状況の変化などでエラー状態が解消されることはあり得るので、そのような場合は、この関数を呼んでFILE構造体の状態をリセットします。

void clearerror(FILE *fp);

さて、ファイルを開く時に「モード」を指定するのですが、これを変更したくなった場合、素直に考えれば一度閉じてから開くのですが、これを一度に行うことが出来る freopen があります。同じファイル構造体を使い続けられる筈なのですが、それが保証されているかと言えば微妙みたいです。

FILE * freopen(const char *restrict filename, const char *restrict mode, FILE *restrict fp);

開き直す操作を一度にできるのは便利なのですが、エラーが発生した際の処理が少し面倒な気もします。

また、ストリームがバッファを持っているので、ファイルを閉じずにバッファをすべて書き込みたい場合には fflush を使います。これでバッファに残っていたデータはすべて書き出される「ハズ」なのですが、ストリームのバッファからは書き出されてもOSのバッファからディスクに書き出されるかは別の問題なので絶対安心だということにはなりません。但し fclose の時に最後のデータが書き出されなかったというエラーを端折れるので、一旦 flush してから close するのも見通しが良くなることがあります。

int fflush(FILE *fp);

そして最後に、そのバッファを制御する setbuf または setvbuf があります。

void setbuf(FILE *restrict fp, char *restrict buf);

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

mode は、_IOFBF(完全)、_IOLBF(行単位)、_IONBF(なし)のいずれか

setbuf は、用意したバッファを使うように指示をするのですが、これがローカル変数だと有効範囲を抜けた時点でバッファが無効になるので注意が必要です。またマクロBUFSIZの値を変更するわけではないので、この値よりもバッファ長が短いと問題を起こします。このポインタにNULLを指定すると、バッファが無効化され入出力の都度OSとやり取りするようになります。そうそうバッファにデータが残っている状態で、この関数を呼び出すと当然ですがバッファは破壊されます。この関数はエラーを返さないのでバッファを無効化したい場合以外は setvbuf を使うほうが安心です。

setvbuf は少し賢くて、mode を指定してバッファの種類をmodeで指定します。呼び出し側で用意したバッファを使う場合は、そのポインタを渡しますが、NULL を指定すればライブラリ側がsizeで指定した大きさのバッファを用意して使ってくれます。但し本当に指定した大きさのバッファが確保されているのかは定かではありません。もちろんバッファにデータが残っていては駄目ですし、バッファでNULL以外を渡した場合に、その有効範囲に注意しなければならないのも同じです。

バッファを変更するケースはあまり多くは無いはずですが、パフォーマンスに大きな影響を与えることはあるので、目的の環境で、fstatなどで得られるOSのバッファサイズなどを考慮して実際に計測しつつ調整するものです。

ストリーム入出力

ここでは説明しませんでしたが、最近の処理系には1文字が1バイトでは「ない」ストリームを扱うワイドストリームというものがあったり、文字がUnicodeであることを前提としたUnicodeストリームというものが使えることもあります。これらのストリームを使う時には文字も char 以外になるので、今回は端折らせていただきます。

C言語のストリーム入出力の考え方は、その後の多くの言語でも、ほぼ同じような機構が採用されていて、同じような使い方が出来るようになっています。但しテキストとバイナリのストリームがモードではなく明確に区別されるものが多いです。テキストがそもそも1バイトではない処理系もありますしね。

ストリーム (プログラミング)

ということで、いよいよ低レベル入出力に進みます。

ヘッダ画像は、今回もAIに頑張ってもらいました。

#C言語 #C言語教室 #プログラミング講座 #ストリーム入出力 #ftell #fgetpos #EOF #fflush #setbuf #バッファ

いいなと思ったら応援しよう!

kzn
頂いたチップは記事を書くための資料を揃えるために使わせていただきます!