Mengapa "while (! Feof (file))" selalu salah?

574

Saya telah melihat orang yang mencoba membaca file seperti ini di banyak posting belakangan ini:

#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;
}

Apa yang salah dengan loop ini?

William Pursell
sumber

Jawaban:

454

Saya ingin memberikan perspektif abstrak tingkat tinggi.

Konkurensi dan simultan

Operasi I / O berinteraksi dengan lingkungan. Lingkungan bukan bagian dari program Anda, dan bukan di bawah kendali Anda. Lingkungan benar-benar ada "bersamaan" dengan program Anda. Seperti halnya semua hal bersamaan, pertanyaan tentang "kondisi saat ini" tidak masuk akal: Tidak ada konsep "simultanitas" di seluruh peristiwa yang terjadi bersamaan. Banyak sifat negara tidak ada secara bersamaan.

Biarkan saya membuat ini lebih tepat: Misalkan Anda ingin bertanya, "apakah Anda memiliki lebih banyak data". Anda bisa menanyakan ini pada wadah bersamaan, atau sistem I / O Anda. Tetapi jawabannya pada umumnya tidak dapat dipertanyakan, dan dengan demikian tidak berarti. Jadi bagaimana jika wadah itu mengatakan "ya" - pada saat Anda mencoba membaca, itu mungkin tidak lagi memiliki data. Demikian pula, jika jawabannya "tidak", pada saat Anda mencoba membaca, data mungkin telah tiba. Kesimpulannya adalah bahwa ada cukup adalahtidak ada properti seperti "Saya punya data", karena Anda tidak dapat bertindak secara bermakna sebagai jawaban atas kemungkinan jawaban. (Situasinya sedikit lebih baik dengan input buffered, di mana Anda mungkin mendapatkan "ya, saya punya data" yang merupakan semacam jaminan, tetapi Anda masih harus mampu menangani kasus yang berlawanan. Dan dengan output situasi tentu saja sama buruknya dengan yang saya jelaskan: Anda tidak pernah tahu apakah disk itu atau buffer jaringan sudah penuh.)

Jadi kami menyimpulkan bahwa tidak mungkin, dan pada kenyataannya tidak masuk akal , untuk menanyakan sistem I / O apakah akan dapat melakukan operasi I / O. Satu-satunya cara yang memungkinkan kita untuk berinteraksi dengannya (seperti halnya wadah bersamaan) adalah dengan mencoba operasi dan memeriksa apakah berhasil atau gagal. Pada saat di mana Anda berinteraksi dengan lingkungan, saat itu dan baru Anda dapat mengetahui apakah interaksi itu benar-benar mungkin, dan pada saat itu Anda harus berkomitmen untuk melakukan interaksi. (Ini adalah "titik sinkronisasi", jika Anda mau.)

EOF

Sekarang kita sampai ke EOF. EOF adalah respons yang Anda dapatkan dari percobaan operasi I / O. Ini berarti bahwa Anda mencoba membaca atau menulis sesuatu, tetapi ketika melakukannya Anda gagal membaca atau menulis data apa pun, dan sebaliknya akhir input atau output itu ditemukan. Ini berlaku untuk dasarnya semua I / O API, apakah itu pustaka standar C, C ++ iostreams, atau pustaka lainnya. Selama operasi I / O berhasil, Anda tidak dapat mengetahui apakah operasi selanjutnya di masa depan akan berhasil. Anda harus selalu mencoba operasi terlebih dahulu dan kemudian menanggapi keberhasilan atau kegagalan.

Contohnya

Dalam masing-masing contoh, perhatikan dengan seksama bahwa kita pertama-tama mencoba operasi I / O dan kemudian mengkonsumsi hasilnya jika itu valid. Perhatikan lebih lanjut bahwa kita selalu harus menggunakan hasil operasi I / O, meskipun hasilnya mengambil bentuk dan bentuk yang berbeda dalam setiap contoh.

  • C stdio, baca dari file:

    for (;;) {
        size_t n = fread(buf, 1, bufsize, infile);
        consume(buf, n);
        if (n < bufsize) { break; }
    }

    Hasil yang harus kita gunakan adalah n, jumlah elemen yang dibaca (yang mungkin hanya nol).

  • C stdio, scanf:

    for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) {
        consume(a, b, c);
    }

    Hasil yang harus kita gunakan adalah nilai balik scanf, jumlah elemen yang dikonversi.

  • C ++, iostreams diekstraksi dengan format:

    for (int n; std::cin >> n; ) {
        consume(n);
    }

    Hasil yang harus kita gunakan adalah std::cindirinya sendiri, yang dapat dievaluasi dalam konteks boolean dan memberi tahu kita apakah aliran masih dalam good()keadaan.

  • C ++, iostreams getline:

    for (std::string line; std::getline(std::cin, line); ) {
        consume(line);
    }

    Hasil yang harus kita gunakan lagi std::cin, sama seperti sebelumnya.

  • POSIX, write(2)untuk menyiram buffer:

    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 */ }

    Hasil yang kami gunakan di sini adalah k, jumlah byte yang ditulis. Intinya di sini adalah bahwa kita hanya bisa tahu berapa byte yang ditulis setelah operasi penulisan.

  • 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);

    Hasil yang harus kita gunakan adalah nbytes, jumlah byte hingga dan termasuk baris baru (atau EOF jika file tidak diakhiri dengan baris baru).

    Perhatikan bahwa fungsi secara eksplisit mengembalikan -1(dan bukan EOF!) Ketika kesalahan terjadi atau mencapai EOF.

Anda mungkin memperhatikan bahwa kami sangat jarang mengeja kata "EOF" yang sebenarnya. Kami biasanya mendeteksi kondisi kesalahan dengan cara lain yang lebih menarik bagi kami (mis. Kegagalan untuk melakukan I / O sebanyak yang kami inginkan). Dalam setiap contoh ada beberapa fitur API yang dapat memberi tahu kami secara eksplisit bahwa keadaan EOF telah dijumpai, tetapi ini sebenarnya bukan sepotong informasi yang sangat berguna. Ini jauh lebih detail daripada yang sering kita pedulikan. Yang penting adalah apakah I / O berhasil, lebih-daripada bagaimana gagal.

  • Contoh terakhir yang benar-benar menanyakan keadaan EOF: Misalkan Anda memiliki string dan ingin menguji bahwa itu mewakili bilangan bulat secara keseluruhan, tanpa bit tambahan di akhir kecuali spasi. Menggunakan C ++ iostreams, seperti ini:

    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
    }

    Kami menggunakan dua hasil di sini. Yang pertama adalah iss, objek stream itu sendiri, untuk memeriksa apakah ekstraksi yang diformat valueberhasil. Tetapi kemudian, setelah juga mengkonsumsi spasi putih, kami melakukan operasi I / O / lainnya iss.get(),, dan mengharapkannya gagal sebagai EOF, yang merupakan kasus jika seluruh string telah dikonsumsi oleh ekstraksi yang diformat.

    Di pustaka standar C Anda dapat mencapai sesuatu yang mirip dengan strto*lfungsi dengan memeriksa bahwa pointer akhir telah mencapai akhir string input.

Jawabannya

while(!feof)salah karena tes untuk sesuatu yang tidak relevan dan gagal untuk menguji sesuatu yang perlu Anda ketahui. Hasilnya adalah Anda salah mengeksekusi kode yang menganggap bahwa itu mengakses data yang berhasil dibaca, padahal sebenarnya ini tidak pernah terjadi.

Kerrek SB
sumber
34
@CanPan: Saya rasa itu tidak benar. Baik C99 dan C11 memungkinkan ini.
Kerrek SB
11
Tapi ANSI C tidak.
CiaPan
3
@ JonathanMee: Ini buruk untuk semua alasan yang saya sebutkan: Anda tidak dapat melihat ke masa depan Anda tidak bisa mengatakan apa yang akan terjadi di masa depan.
Kerrek SB
3
@ JonathanMee: Ya, itu akan sesuai, meskipun biasanya Anda dapat menggabungkan pemeriksaan ini ke dalam operasi (karena sebagian besar operasi iostreams mengembalikan objek aliran, yang itu sendiri memiliki konversi boolean), dan dengan cara itu Anda membuatnya jelas bahwa Anda tidak mengabaikan nilai pengembalian.
Kerrek SB
4
Paragraf ketiga sangat menyesatkan / tidak akurat untuk jawaban yang diterima dan sangat tervvotasikan. feof()tidak "bertanya pada sistem I / O apakah memiliki lebih banyak data". feof(), Menurut (Linux) halaman manual : "tes akhir-of-file indikator aliran yang ditunjukkan oleh aliran, kembali nol jika sudah diatur." (juga, panggilan eksplisit ke clearerr()adalah satu-satunya cara untuk mengatur ulang indikator ini); Dalam hal ini, jawaban William Pursell jauh lebih baik.
Arne Vogel
234

Itu salah karena (jika tidak ada kesalahan baca) itu memasuki loop sekali lagi dari yang diharapkan penulis. Jika ada kesalahan baca, loop tidak pernah berakhir.

Pertimbangkan kode berikut:

/* WARNING: demonstration of bad coding technique!! */

#include <stdio.h>
#include <stdlib.h>

FILE *Fopen(const char *path, const char *mode);

int main(int argc, char **argv)
{
    FILE *in;
    unsigned count;

    in = argc > 1 ? Fopen(argv[1], "r") : stdin;
    count = 0;

    /* WARNING: this is a bug */
    while( !feof(in) ) {  /* This is WRONG! */
        fgetc(in);
        count++;
    }
    printf("Number of characters read: %u\n", count);
    return EXIT_SUCCESS;
}

FILE * Fopen(const char *path, const char *mode)
{
    FILE *f = fopen(path, mode);
    if( f == NULL ) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    return f;
}

Program ini akan secara konsisten mencetak satu lebih besar dari jumlah karakter dalam aliran input (dengan asumsi tidak ada kesalahan baca). Pertimbangkan kasus di mana aliran input kosong:

$ ./a.out < /dev/null
Number of characters read: 1

Dalam hal ini, feof()dipanggil sebelum data apa pun dibaca, sehingga mengembalikan false. Loop dimasukkan, fgetc()disebut (dan kembali EOF), dan jumlah bertambah. Kemudian feof()dipanggil dan mengembalikan true, menyebabkan loop dibatalkan.

Ini terjadi dalam semua kasus seperti itu. feof()tidak mengembalikan true sampai setelah membaca pada aliran menemui akhir file. Tujuan feof()BUKAN untuk memeriksa apakah pembacaan berikutnya akan mencapai akhir file. Tujuannya feof()adalah untuk membedakan antara kesalahan baca dan telah mencapai akhir file. Jika fread()mengembalikan 0, Anda harus menggunakan feof/ ferroruntuk memutuskan apakah ada kesalahan atau jika semua data dikonsumsi. Demikian pula jika fgetckembali EOF. feof()hanya berguna setelah ketakutan kembali nol atau fgetckembali EOF. Sebelum itu terjadi, feof()akan selalu mengembalikan 0.

Itu selalu perlu untuk memeriksa nilai balik dari suatu pembacaan (baik suatu fread(), atau fscanf(), atau suatu fgetc()) sebelum memanggil feof().

Lebih buruk lagi, pertimbangkan kasus di mana kesalahan baca terjadi. Dalam hal itu, fgetc()mengembalikan EOF, feof()mengembalikan false, dan loop tidak pernah berakhir. Dalam semua kasus di mana while(!feof(p))digunakan, harus ada setidaknya pemeriksaan di dalam loop untuk ferror(), atau setidaknya kondisi sementara harus diganti dengan while(!feof(p) && !ferror(p))atau ada kemungkinan yang sangat nyata dari loop tak terbatas, mungkin memuntahkan semua jenis sampah sebagai data yang tidak valid sedang diproses.

Jadi, secara ringkas, walaupun saya tidak dapat menyatakan dengan pasti bahwa tidak pernah ada situasi di mana secara semantik benar untuk menulis " while(!feof(f))" (walaupun harus ada pemeriksaan lain di dalam loop dengan istirahat untuk menghindari loop tak terbatas pada kesalahan baca. ), ini adalah kasus yang hampir pasti selalu salah. Dan bahkan jika suatu kasus pernah muncul di mana itu akan benar, itu sangat salah secara idiom sehingga tidak akan menjadi cara yang tepat untuk menulis kode. Siapa pun yang melihat kode itu harus segera ragu dan berkata, "itu bug". Dan mungkin menampar penulis (kecuali jika penulis adalah bos Anda dalam hal kebijaksanaan disarankan.)

William Pursell
sumber
7
Tentu itu salah - tetapi selain itu tidak "jelek jelek".
nobar
89
Anda harus menambahkan contoh kode yang benar, karena saya bayangkan banyak orang akan datang ke sini mencari perbaikan cepat.
jleahy
6
@ Thomas: Saya bukan ahli C ++, tapi saya percaya file.eof () mengembalikan secara efektif hasil yang sama feof(file) || ferror(file), jadi sangat berbeda. Tetapi pertanyaan ini tidak dimaksudkan untuk diterapkan pada C ++.
William Pursell
6
@ m-ric juga tidak benar, karena Anda masih akan mencoba memproses pembacaan yang gagal.
Mark Ransom
4
inilah jawaban yang benar sebenarnya. feof () digunakan untuk mengetahui hasil dari upaya membaca sebelumnya. Jadi mungkin Anda tidak ingin menggunakannya sebagai kondisi loop break Anda. +1
Jack
63

Tidak, itu tidak selalu salah. Jika kondisi loop Anda adalah "sementara kami belum mencoba membaca file yang sudah lewat" maka Anda gunakan while (!feof(f)). Namun ini bukan kondisi loop umum - biasanya Anda ingin menguji sesuatu yang lain (seperti "dapatkah saya membaca lebih lanjut"). while (!feof(f))tidak salah, itu hanya digunakan salah.

Erik
sumber
1
Saya ingin tahu ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }atau (akan menguji ini)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg
1
@ pmg: Seperti yang dikatakan, "bukan kondisi loop umum" hehe. Saya tidak dapat benar-benar memikirkan kasus apa pun yang saya butuhkan, biasanya saya tertarik pada "bisakah saya membaca apa yang saya inginkan" dengan semua yang menyiratkan penanganan kesalahan
Erik
@ pmg: Seperti yang Anda katakan, Anda jarang inginwhile(!eof(f))
Erik
9
Lebih tepatnya, syaratnya adalah "sementara kami belum mencoba membaca melewati akhir file dan tidak ada kesalahan baca" feofbukan tentang mendeteksi akhir file; ini tentang menentukan apakah pembacaan singkat karena kesalahan atau karena input habis.
William Pursell
35

feof()menunjukkan jika seseorang telah mencoba membaca melewati akhir file. Itu artinya memiliki sedikit efek prediksi: jika itu benar, Anda yakin bahwa operasi input berikutnya akan gagal (Anda tidak yakin yang sebelumnya gagal BTW), tetapi jika itu salah, Anda tidak yakin input berikutnya operasi akan berhasil. Terlebih lagi, operasi input mungkin gagal karena alasan lain selain akhir file (kesalahan format untuk input yang diformat, kegagalan IO murni - kegagalan disk, batas waktu jaringan - untuk semua jenis input), sehingga bahkan jika Anda dapat memperkirakan tentang akhir file (dan siapa pun yang telah mencoba menerapkan Ada satu, yang dapat diprediksi, akan memberi tahu Anda itu bisa rumit jika Anda perlu melewati spasi, dan bahwa ia memiliki efek yang tidak diinginkan pada perangkat interaktif - kadang-kadang memaksa input dari yang berikutnya baris sebelum memulai penanganan yang sebelumnya),

Jadi idiom yang benar dalam C adalah untuk mengulang dengan keberhasilan operasi IO sebagai kondisi loop, dan kemudian menguji penyebab kegagalan. Contohnya:

while (fgets(line, sizeof(line), file)) {
    /* note that fgets don't strip the terminating \n, checking its
       presence allow to handle lines longer that sizeof(line), not showed here */
    ...
}
if (ferror(file)) {
   /* IO failure */
} else if (feof(file)) {
   /* format error (not possible with fgets, but would be with fscanf) or end of file */
} else {
   /* format error (not possible with fgets, but would be with fscanf) */
}
Pemrogram
sumber
2
Mendapatkan ke akhir file bukanlah kesalahan, jadi saya mempertanyakan frasa "operasi input mungkin gagal karena alasan lain selain akhir file".
William Pursell
@ WilliamPursell, mencapai titik belum tentu kesalahan, tetapi tidak dapat melakukan operasi input karena titik adalah satu. Dan tidak mungkin dalam C untuk mendeteksi dengan andal bukti tanpa membuat operasi input gagal.
Pemrogram
Setuju terakhir elsetidak mungkin dengan sizeof(line) >= 2dan fgets(line, sizeof(line), file)tetapi mungkin dengan patologis size <= 0dan fgets(line, size, file). Mungkin bahkan mungkin dengan sizeof(line) == 1.
chux - Reinstate Monica
1
Semua pembicaraan "nilai prediktif" itu ... saya tidak pernah memikirkannya seperti itu. Di duniaku, feof(f)tidak MEMPREDIKSI apa pun. Ini menyatakan bahwa operasi SEBELUMNYA telah mencapai akhir file. Tidak lebih, tidak kurang. Dan jika tidak ada operasi sebelumnya (hanya membukanya), itu tidak melaporkan akhir file bahkan jika file itu kosong untuk memulai. Jadi, terlepas dari penjelasan konkurensi dalam jawaban lain di atas, saya tidak berpikir ada alasan untuk tidak melanjutkan feof(f).
BitTickler
@AProgrammer: A "membaca hingga N byte" permintaan itu hasil nol, apakah karena "permanen" EOF atau karena tidak ada lagi data yang tersedia belum , bukan kesalahan. Meskipun feof () mungkin tidak dapat memprediksi dengan andal bahwa permintaan di masa depan akan menghasilkan data, itu dapat dipercaya menunjukkan bahwa permintaan di masa depan tidak akan . Mungkin harus ada fungsi status yang akan menunjukkan "Masuk akal bahwa permintaan baca di masa depan akan berhasil", dengan semantik bahwa setelah membaca hingga akhir file biasa, implementasi kualitas seharusnya mengatakan bacaan di masa depan tidak mungkin berhasil tanpa alasan untuk percaya mereka mungkin .
supercat
0

feof()sangat tidak intuitif. Menurut pendapat saya yang sangat rendah hati, FILEstatus file akhir harus ditetapkan truejika operasi membaca apa pun menghasilkan akhir file tercapai. Sebagai gantinya, Anda harus memeriksa secara manual apakah akhir file telah tercapai setelah setiap operasi baca. Misalnya, sesuatu seperti ini akan berfungsi jika membaca dari file teks menggunakan fgetc():

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(1) {
    char c = fgetc(in);
    if (feof(in)) break;
    printf("%c", c);
  }

  fclose(in);
  return 0;
}

Alangkah baiknya jika sesuatu seperti ini akan bekerja sebagai gantinya:

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(!feof(in)) {
    printf("%c", fgetc(in));
  }

  fclose(in);
  return 0;
}
Scott Deagan
sumber
1
printf("%c", fgetc(in));? Itu perilaku yang tidak terdefinisi. fgetc()kembali int, tidak char.
Andrew Henle
Sepertinya saya bahwa idiom standar while( (c = getchar()) != EOF)adalah "sesuatu seperti ini".
William Pursell
while( (c = getchar()) != EOF)bekerja pada salah satu desktop saya yang menjalankan GNU C 10.1.0, tetapi gagal pada Raspberry Pi 4 saya yang menjalankan GNU C 9.3.0. Pada RPi4 saya, itu tidak mendeteksi akhir file, dan terus berjalan.
Scott Deagan
@AndrewHenle Anda benar! Mengubah char cke int ckarya! Terima kasih!!
Scott Deagan