読み取りループを制御するためにを使用すると何が問題になるのでしょうかfeof()
? たとえば、次のようになります。
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char **argv)
{
char *path = "stdin";
FILE *fp = argc > 1 ? fopen(path=argv[1], "r") : stdin;
if( fp == NULL ){
perror(path);
return EXIT_FAILURE;
}
while( !feof(fp) ){ /* THIS IS WRONG */
/* Read and process data from file… */
}
if( fclose(fp) != 0 ){
perror(path);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
このループの何が問題なのでしょうか?
ベストアンサー1
要約
while(!feof(file))
は、無関係な何かをテストし、知っておく必要がある何かをテストしていないため、間違っています。その結果、実際には読み取りは行われていないのに、正常に読み取られたデータにアクセスしていると想定して誤ってコードを実行することになります。
ここでは抽象的で高レベルの視点を提供したいと思います。while(!feof(file))
実際に何が起こっているのか興味がある方は、読み続けてください。
並行性と同時性
I/O 操作は環境と対話します。環境はプログラムの一部ではなく、制御下にもありません。環境はプログラムと「同時に」存在します。同時実行に関するすべての事柄と同様に、「現在の状態」に関する質問は意味がありません。同時実行イベント間に「同時性」という概念はありません。状態の多くのプロパティは、単に同時には存在しません。
もっと正確に言うと、「まだデータがありますか」と尋ねたいとします。これは、並行コンテナまたは I/O システムに尋ねることができます。しかし、答えは一般に実行不可能であり、したがって意味がありません。では、コンテナが「はい」と答えたとしても、読み取りを試みるときには、もうデータが存在しない可能性があります。同様に、答えが「いいえ」の場合、読み取りを試みるときには、データが到着している可能性があります。結論としては、「データがあります」のような特性は存在しません。考えられる答えに応じて意味のある行動をとることができないためです。(バッファリングされた入力の場合は状況が少し良くなります。「はい、データがあります」という何らかの保証が得られる可能性がありますが、それでも逆のケースに対処できる必要があります。また、出力の場合は、私が説明したのと同じくらい状況が悪いことは確かです。ディスクまたはネットワーク バッファがいっぱいかどうかはわかりません。)
したがって、I/O システムに I/O 操作を実行できるかどうかを尋ねることは不可能であり、実際は不合理であるという結論に達しました。I/O システムと対話できる唯一の方法は (同時実行コンテナーの場合と同様に)、操作を試行して成功したか失敗したかを確認することです。環境と対話するその瞬間にのみ、対話が実際に可能であったかどうかを知ることができ、その時点で対話の実行をコミットする必要があります (これは「同期ポイント」とも呼ばれます)。
終了
ここで EOF に到達します。EOF は、試行されたI/O 操作から取得する応答です。これは、何かを読み取ったり書き込んだりしようとしたが、その際にデータの読み取りまたは書き込みに失敗し、代わりに入力または出力の終わりに達したことを意味します。これは、C 標準ライブラリ、C++ iostreams、またはその他のライブラリなど、基本的にすべての I/O API に当てはまります。I/O 操作が成功する限り、それ以降の操作が成功するかどうかはわかりません。常に最初に操作を試行してから、成功または失敗に応答する必要があります。
例
それぞれの例で、最初にI/O 操作を試行し、その結果が有効な場合はそれを使用することに留意してください。さらに、結果がそれぞれの例で異なる形や形式をとる場合でも、常に I/O 操作の結果を使用する必要があることにも注意してください。
C stdio、ファイルから読み取り:
for (;;) { size_t n = fread(buf, 1, bufsize, infile); consume(buf, n); if (n == 0) { break; } }
使用する必要がある結果は
n
、読み取られた要素の数(ゼロになる場合もあります)です。C stdio
scanf
、:for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) { consume(a, b, c); }
使用する必要がある結果は、 の戻り値
scanf
、つまり変換された要素の数です。C++、iostreams 形式の抽出:
for (int n; std::cin >> n; ) { consume(n); }
使用する必要がある結果は
std::cin
それ自身であり、これはブールコンテキストで評価でき、ストリームがまだ状態にあるかどうかを示しますgood()
。C++、iostreams getline:
for (std::string line; std::getline(std::cin, line); ) { consume(line); }
使用する必要がある結果は
std::cin
、前と同じように、再び です。POSIX、
write(2)
バッファをフラッシュするには:char const * p = buf; ssize_t n = bufsize; for (ssize_t k = bufsize; (k = write(fd, p, n)) > 0; p += k, n -= k) {} if (n != 0) { /* error, failed to write complete buffer */ }
ここで使用する結果は、書き込まれたバイト数です。ここでのポイントは、書き込み操作後に
k
書き込まれたバイト数しかわからないということです。POSIX
getline()
char *buffer = NULL; size_t bufsiz = 0; ssize_t nbytes; while ((nbytes = getline(&buffer, &bufsiz, fp)) != -1) { /* Use nbytes of data in buffer */ } free(buffer);
使用する必要がある結果は
nbytes
、改行までのバイト数(ファイルが改行で終了していない場合は EOF)です。エラーが発生したり EOF に到達したりすると、関数は明示的に
-1
(EOF ではなく) を返すことに注意してください。
実際の単語「EOF」を綴ることはほとんどないことに気付くかもしれません。通常、エラー状態は、私たちにとってより直接的に興味深い他の方法で検出されます (たとえば、必要な量の I/O を実行できなかった場合など)。すべての例には、EOF 状態が発生したことを明示的に通知できる API 機能がありますが、これは実際にはそれほど役立つ情報ではありません。これは、私たちが気にするよりもはるかに細かいことです。重要なのは、I/O が成功したかどうかであり、失敗した理由ではありません。
実際に EOF 状態を照会する最後の例: 文字列があり、それが空白以外の余分なビットがなく、全体が整数を表していることをテストするとします。C++ iostreams を使用すると、次のようになります。
std::string input = " 123 "; // example std::istringstream iss(input); int value; if (iss >> value >> std::ws && iss.get() == EOF) { consume(value); } else { // error, "input" is not parsable as an integer }
ここでは 2 つの結果を使用します。1 つ目はiss
、ストリーム オブジェクト自体で、フォーマットされた抽出がvalue
成功したかどうかを確認します。ただし、その後、空白も消費した後、別の I/O/ 操作 を実行し、iss.get()
EOF として失敗すると予想します。これは、フォーマットされた抽出によって文字列全体がすでに消費されている場合に当てはまります。
C 標準ライブラリでは、strto*l
終了ポインタが入力文字列の末尾に到達したかどうかを確認することで、関数を使用して同様のことを実現できます。