Banyak pemula menulis while (!feof(f)) sebagai kondisi loop baca—dan kemudian bingung kenapa loop mereka memproses record terakhir dua kali. Penyebabnya: feof hanya melaporkan flag EOF yang sudah diset setelah operasi baca gagal, bukan sebelumnya. Begitu pula ferror, yang melaporkan flag error I/O hanya setelah operasi baca/tulis benar-benar gagal. Pada chapter ini kita akan melihat kapan tepatnya flag EOF dan error diset, bagaimana membedakannya, dan bagaimana pola benar untuk loop baca yang tidak menghasilkan record fantom.
Kontrak feof dan ferror #
ISO/IEC 9899 §7.21.10.2 dan §7.21.10.1 mendefinisikan:
int feof(FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);Kontrak penting:
feofmengembalikan non-zero bila end-of-file indicator sudah diset — flag ini hanya diset saat operasi baca mencoba membaca melewati akhir file. Sebelum percobaan baca,feofselalu mengembalikan0.ferrormengembalikan non-zero bila error indicator sudah diset — flag ini diset saat operasi I/O gagal karena alasan selain EOF (misalnya disk penuh, koneksi putus pada stream jaringan).clearerrmenghapus kedua flag — berguna bila kita ingin mencoba baca lagi setelah error sementara (misalnya setelahrewindataufseek).
Mekanisme kerja: saat fgetc, fgets, atau fread mencoba membaca dan menemui kondisi EOF (kernel mengembalikan 0 byte), C standard library menyetel flag EOF pada stream dan mengembalikan indikasi kegagalan (EOF/NULL/short count). Saat operasi baca gagal karena error sistem, flag error diset. Karena itu kedua flag ini hanya relevan setelah pemanggilan fungsi I/O.
Demonstrasi: feof Hanya Aktif Setelah Baca Gagal #
#include <stdio.h>
int main(void) {
/* Tulis file dengan 3 baris */
FILE *f = fopen("/tmp/test_eof.txt", "w");
if (!f) return 1;
fputs("satu\ndua\ntiga\n", f);
fclose(f);
/* Total 14 byte: "satu\n" (5) + "dua\n" (4) + "tiga\n" (5) */
f = fopen("/tmp/test_eof.txt", "r");
if (!f) return 1;
/* Tes 1: feof SEBELUM baca — harus 0 (false) */
printf("feof sebelum baca: %d\n", feof(f));
/* Baca seluruh file dengan fgetc */
int c;
int count = 0;
while ((c = fgetc(f)) != EOF) {
count++;
}
printf("Total byte dibaca: %d\n", count);
/* Tes 2: feof SESUDAH EOF — harus non-zero */
printf("feof setelah EOF: %d\n", feof(f));
/* Tes 3: ferror setelah membaca normal — harus 0 */
printf("ferror setelah EOF normal: %d\n", ferror(f));
fclose(f);
return 0;
}Output yang Dihasilkan:
feof sebelum baca: 0
Total byte dibaca: 14
feof setelah EOF: 1
ferror setelah EOF normal: 0Output di atas membuktikan tiga fakta penting:
- Sebelum baca apapun,
feof(f)mengembalikan0—meskipun kita tahu file hanya berisi 14 byte. Flag EOF belum diset karena belum ada percobaan baca yang gagal. - Setelah
fgetcmengembalikanEOF(karena mencoba membaca byte ke-15 yang tidak ada),feof(f)mengembalikan1—flag EOF baru saja diset. ferror(f)tetap0karena tidak ada error I/O—hanya EOF biasa.
Mari kita telaah baris-baris kode di atas:
- Baris 17 (
printf("feof sebelum baca: %d\n", feof(f));): pemanggilanfeofpertama. Flag EOF masih0karena belum ada operasi baca. Inilah inti masalah:feofbukan prediksi “apakah baca berikutnya akan mencapai EOF”, melainkan laporan “apakah baca sebelumnya menyebabkan EOF”. - Baris 21 (
while ((c = fgetc(f)) != EOF)): idiom benar untuk membaca byte demi byte. Kondisi loop memeriksa return valuefgetc, bukanfeof. Loop berhenti segera saatfgetcmengembalikanEOF. - Baris 26 (
printf("feof setelah EOF: %d\n", feof(f));): setelah loop keluar karenafgetcmengembalikanEOF, flag EOF sudah diset, sehinggafeofmengembalikan1. - Baris 28 (
printf("ferror ... %d\n", ferror(f));): tidak ada error I/O—hanya EOF. Flag error tetap0. Inilah cara membedakan EOF biasa dari error sebenarnya: cek kedua flag.
Jebakan: while(!feof) Menghasilkan Record Fantom #
Anti-pattern paling sering ditemui:
while (!feof(f)) {
fscanf(f, "%s %d", nama, &umur);
proses(nama, umur);
}Pemula mengira feof akan menjadi true tepat saat EOF tercapai, sehingga loop berhenti di saat yang tepat. Kenyataannya, feof baru menjadi true setelah fscanf mencoba membaca dan gagal—yang berarti loop sudah memproses sekali dengan data yang gagal dibaca.
#include <stdio.h>
int main(void) {
FILE *f = fopen("/tmp/test_eof.txt", "w");
if (!f) return 1;
fputs("Budi 20\nSiti 21\nAndi 19\n", f);
fclose(f);
f = fopen("/tmp/test_eof.txt", "r");
if (!f) return 1;
/* BUG klasik: pakai while(!feof) lalu fscanf */
char nama[32] = {0};
int umur = 0;
int iter = 0;
while (!feof(f)) {
iter++;
int rc = fscanf(f, "%31s %d", nama, &umur);
printf("Iter %d: rc=%d, nama=%s umur=%d\n", iter, rc, nama, umur);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
Iter 1: rc=2, nama=Budi umur=20
Iter 2: rc=2, nama=Siti umur=21
Iter 3: rc=2, nama=Andi umur=19
Iter 4: rc=-1, nama=Andi umur=19Perhatikan Iter 4 di output di atas. Pada awal Iter 4, flag EOF belum diset (karena fscanf di Iter 3 berhasil membaca Andi 19), sehingga !feof(f) masih true dan loop masuk. Tetapi fscanf di Iter 4 gagal membaca apapun (file sudah habis), mengembalikan EOF (-1), dan tidak mengubah nama maupun umur—yang masih berisi Andi dan 19 dari iterasi sebelumnya. Hasilnya, kita memproses Andi 19 dua kali—record fantom.
Idiom yang benar adalah memeriksa return value fscanf sebagai kondisi loop:
while (fscanf(f, "%31s %d", nama, &umur) == 2) {
proses(nama, umur);
}Dengan pola ini, loop berhenti tepat saat fscanf gagal, tanpa iterasi fantom.
Demonstrasi: ferror untuk Error I/O #
ferror mengembalikan non-zero saat operasi I/O gagal karena alasan selain EOF—misalnya disk penuh saat fwrite, atau koneksi socket terputus saat fread. Pada Linux, cara paling mudah mendemonstrasikan ferror adalah dengan membaca dari stream yang ditutup di tengah jalan. Untuk kesederhanaan, kita simulasi dengan read dari file yang sengaja di-corrupt dengan freopen.
#include <stdio.h>
int main(void) {
FILE *f = fopen("/tmp/test_eof.txt", "r");
if (!f) return 1;
/* Baca semua hingga EOF */
int c;
while ((c = fgetc(f)) != EOF) { }
printf("Setelah EOF: feof=%d, ferror=%d\n", feof(f), ferror(f));
/* Bersihkan flag dengan clearerr */
clearerr(f);
printf("Setelah clearerr: feof=%d, ferror=%d\n", feof(f), ferror(f));
/* Coba baca lagi setelah clearerr */
rewind(f);
c = fgetc(f);
printf("Setelah rewind + fgetc: byte pertama='%c' (kode %d)\n",
(c >= 32 && c < 127) ? c : '?', c);
fclose(f);
return 0;
}Output yang Dihasilkan:
Setelah EOF: feof=1, ferror=0
Setelah clearerr: feof=0, ferror=0
Setelah rewind + fgetc: byte pertama='s' (kode 115)Setelah EOF tercapai, feof=1 dan ferror=0. clearerr menghapus kedua flag—sehingga feof kembali 0. Setelah rewind, kita bisa membaca file lagi dari awal. Inilah cara membaca file beberapa kali tanpa menutup dan membuka kembali.
Pola Baca yang Aman #
Ringkasan pola baca yang aman dan idiomatik di C:
/* Pola 1: baca per byte */
int c;
while ((c = fgetc(f)) != EOF) {
/* proses c */
}
/* Pola 2: baca per baris */
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
/* proses line */
}
/* Pola 3: baca dengan fscanf — periksa jumlah field */
while (fscanf(f, "%31s %d", nama, &umur) == 2) {
/* proses nama, umur */
}
/* Pola 4: baca blok dengan fread — periksa jumlah elemen */
Record rec;
while (fread(&rec, sizeof(Record), 1, f) == 1) {
/* proses rec */
}Pola 1-4 sama dalam satu hal: kondisi loop memeriksa return value fungsi baca, bukan feof. Hanya setelah loop keluar dan kita ingin membedakan “EOF biasa” dari “error I/O”, kita pakai feof dan ferror.