Operasi berkas gagal karena banyak sebab, mulai dari berkas yang belum ada, izin akses yang ditolak, sampai media yang penuh di tengah penulisan. Akhir data yang wajar dan error baca yang sesungguhnya tampak sama dari luar, tetapi program yang tangguh merespons keduanya secara berbeda. Topik ini membahas feof dan ferror beserta jebakan perulangan feof yang paling sering ditemui. Pelaporan pesan error dengan errno, perror, dan strerror menyusul, lalu modul ditutup dengan proyek sistem catatan siswa yang menerapkan semuanya.
Nilai kembali sebagai sumber kebenaran#
Setiap fungsi baca memberi tanda melalui nilai kembanya, yaitu EOF pada fgetc, NULL pada fgets, serta jumlah yang kurang dari yang diminta pada fscanf maupun fread. Nilai-nilai itu hanya menyatakan bahwa pembacaan berhenti, sedangkan sebabnya belum diketahui. Dua fungsi pemeriksa bertugas menjelaskannya, yaitu feof yang mengembalikan nilai tidak nol bila indikator akhir berkas terpasang, dan ferror yang mengembalikan nilai tidak nol bila indikator error terpasang.
Kedua indikator itu baru terpasang setelah ada upaya baca yang menabrak akhir berkas atau menemui kegagalan, sehingga pemeriksaannya dilakukan setelah pembacaan, bukan sebelumnya. Pola yang dihasilkan selalu sama, yaitu perulangan yang kondisinya adalah nilai kembali fungsi baca, lalu pemeriksaan feof dan ferror setelah perulangan berakhir. Dengan pola itu, akhir data yang wajar dan error yang sesungguhnya tidak pernah tertukar. Fungsi clearerr melengkapi keduanya dengan menghapus kedua indikator sekaligus. Rewind yang dibahas pada topik Akses Acak ikut menghapusnya sebagai efek samping, sehingga keadaan stream dapat dipulihkan secara eksplisit.
Jebakan while (!feof(f))#
Fungsi feof melaporkan masa lalu, bukan meramalkan pembacaan berikutnya. Berkas skor.txt berikut menjadi bahan uji pernyataan itu, dan isinya hanya tiga baris:
Ali 90
Budi 75
Citra 88Program berikut menggunakan feof sebagai kondisi perulangan, dan hasilnya menunjukkan akibat kesalahan itu:
#include <stdio.h>
int main(void) {
FILE *berkas = fopen("skor.txt", "r");
if (berkas == NULL) {
perror("fopen skor.txt");
return 1;
}
char nama[32];
int nilai = 0;
/* perulangan yang keliru: feof dipakai sebagai kondisi */
while (!feof(berkas)) {
int cocok = fscanf(berkas, "%31s %d", nama, &nilai);
(void)cocok; /* kelalaian yang disengaja: nilai kembali tidak diperiksa */
printf("%s %d\n", nama, nilai);
}
fclose(berkas);
return 0;
}Ali 90
Budi 75
Citra 88
Citra 88Catatan terakhir tercetak dua kali meskipun berkasnya hanya memuat tiga baris. Setelah Citra 88 terbaca, penunjuk posisi memang sudah berada di ujung berkas, tetapi indikator akhir berkas belum terpasang karena belum ada upaya baca yang gagal. Perulangan menilai feof sebagai belum terpasang dan mencoba membaca lagi. fscanf pun gagal sekaligus memasang indikator tersebut, sedangkan variabel nama dan nilai tetap berisi data lama yang kemudian tercetak ulang.
Perbaikannya memindahkan kondisi ke nilai kembali fungsi baca itu sendiri, sehingga tidak ada satu iterasi pun yang berjalan dengan data basi:
#include <stdio.h>
int main(void) {
FILE *berkas = fopen("skor.txt", "r");
if (berkas == NULL) {
perror("fopen skor.txt");
return 1;
}
char nama[32];
int nilai = 0;
/* perulangan yang benar: nilai kembali fungsi baca yang diperiksa */
while (fscanf(berkas, "%31s %d", nama, &nilai) == 2) {
printf("%s %d\n", nama, nilai);
}
/* bedakan akhir data yang wajar dari error baca yang sesungguhnya */
if (ferror(berkas)) {
perror("Kesalahan membaca skor.txt");
fclose(berkas);
return 1;
}
fclose(berkas);
printf("Seluruh catatan terbaca tanpa error.\n");
return 0;
}Ali 90
Budi 75
Citra 88
Seluruh catatan terbaca tanpa error.Setelah perulangan berakhir, ferror membedakan dua sebab berhentinya, yaitu akhir data yang wajar atau error baca yang memerlukan perlakuan khusus. Diagram berikut merangkum urutan pemeriksaan tersebut:
errno, perror, dan strerror#
Fungsi fopen hanya mengembalikan NULL tanpa menjelaskan sebabnya, tetapi pustaka standar mencatat sebab itu pada variabel errno yang dideklarasikan di errno.h. Fungsi perror mencetak argumennya, diikuti titik dua dan pesan sesuai nilai errno terkini, sedangkan strerror mengubah nilai errno menjadi teks pesan yang dapat disusun sendiri. Program berikut membuka berkas pada direktori yang tidak ada, lalu menampilkan kegagalannya dengan tiga cara:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
FILE *berkas = fopen("lapor/nilai.txt", "r");
if (berkas == NULL) {
printf("fopen mengembalikan NULL\n");
printf("errno = %d\n", errno);
printf("strerror: %s\n", strerror(errno));
fflush(stdout); /* pastikan baris di atas tiba sebelum pesan perror */
perror("Pesan dari perror");
return 1;
}
fclose(berkas);
return 0;
}fopen mengembalikan NULL
errno = 2
strerror: No such file or directory
Pesan dari perror: No such file or directoryNilai errno 2 adalah kode ENOENT, yaitu berkas atau direktori tidak ditemukan, dan angkanya stabil pada Linux untuk sebab yang sama. Perbedaan perror dan strerror terletak pada formatnya, karena perror menulis langsung ke stderr dengan argumen sebagai awalan, sedangkan strerror menyediakan teks yang bebas disisipkan ke pesan apa pun. Pesan error selalu ditujukan ke stderr agar tidak tercampur dengan data keluaran program.
Proyek: sistem catatan siswa#
Proyek penutup modul ini menerapkan seluruh pola di atas pada satu program bernama catatan. Programnya bekerja lewat argumen baris perintah dengan tiga operasi. tambah <nama> <nilai> menambah satu catatan, daftar memuat dan menampilkan semuanya, sedangkan cari <nama> mencari catatan satu siswa. Datanya disimpan pada berkas catatan.txt, satu catatan per baris berisi nama dan nilai yang dipisah spasi. Akibatnya, menambah berarti membuka berkas dalam mode "a", sedangkan memuat berarti membacanya dengan fscanf.
Setiap langkahnya diperiksa, mulai dari validasi argumen, pemeriksaan NULL setiap fopen, kondisi perulangan pada nilai kembali fscanf, sampai ferror setelah perulangan. Program ini membutuhkan argumen, sehingga demonstrasinya dilakukan lewat terminal pada blok setelah kode:
// tidak-dijalankan
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define PANJANG_NAMA 64
#define NAMA_BERKAS "catatan.txt"
/* tambah: membuka berkas dalam mode tambah, lalu menulis satu catatan */
static int tambah(const char *nama, const char *teks_nilai) {
int nilai = 0;
if (sscanf(teks_nilai, "%d", &nilai) != 1) {
fprintf(stderr, "Nilai harus berupa angka, diterima: %s\n", teks_nilai);
return 2;
}
FILE *berkas = fopen(NAMA_BERKAS, "a");
if (berkas == NULL) {
perror("Gagal membuka " NAMA_BERKAS);
return 1;
}
fprintf(berkas, "%s %d\n", nama, nilai);
if (ferror(berkas)) {
perror("Gagal menulis catatan");
fclose(berkas);
return 1;
}
fclose(berkas);
printf("Catatan tersimpan: %s %d\n", nama, nilai);
return 0;
}
/* daftar: memuat seluruh catatan lalu menampilkannya */
static int daftar(void) {
FILE *berkas = fopen(NAMA_BERKAS, "r");
if (berkas == NULL) {
fprintf(stderr, "Berkas %s belum ada. Tambahkan catatan lebih dahulu.\n", NAMA_BERKAS);
return 1;
}
char nama[PANJANG_NAMA];
int nilai = 0;
while (fscanf(berkas, "%63s %d", nama, &nilai) == 2) {
printf("%-10s %d\n", nama, nilai);
}
if (ferror(berkas)) {
perror("Kesalahan membaca " NAMA_BERKAS);
fclose(berkas);
return 1;
}
fclose(berkas);
return 0;
}
/* cari: memuat catatan lalu menampilkan yang namanya cocok */
static int cari(const char *kunci) {
FILE *berkas = fopen(NAMA_BERKAS, "r");
if (berkas == NULL) {
fprintf(stderr, "Berkas %s belum ada. Tambahkan catatan lebih dahulu.\n", NAMA_BERKAS);
return 1;
}
char nama[PANJANG_NAMA];
int nilai = 0;
int ketemu = 0;
while (fscanf(berkas, "%63s %d", nama, &nilai) == 2) {
if (strcmp(nama, kunci) == 0) {
printf("%-10s %d\n", nama, nilai);
ketemu++;
}
}
if (ferror(berkas)) {
perror("Kesalahan membaca " NAMA_BERKAS);
fclose(berkas);
return 1;
}
fclose(berkas);
if (ketemu == 0) {
printf("Tidak ada catatan untuk %s\n", kunci);
return 1;
}
return 0;
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Pemakaian: catatan tambah <nama> <nilai> | daftar | cari <nama>\n");
return 2;
}
if (strcmp(argv[1], "tambah") == 0) {
if (argc != 4) {
fprintf(stderr, "Pemakaian: catatan tambah <nama> <nilai>\n");
return 2;
}
return tambah(argv[2], argv[3]);
}
if (strcmp(argv[1], "daftar") == 0) {
return daftar();
}
if (strcmp(argv[1], "cari") == 0) {
if (argc != 3) {
fprintf(stderr, "Pemakaian: catatan cari <nama>\n");
return 2;
}
return cari(argv[2]);
}
fprintf(stderr, "Perintah tidak dikenal: %s\n", argv[1]);
return 2;
}Fungsi tambah mengubah teks nilai menjadi angka dengan sscanf dan menolak argumen yang bukan angka sebelum menyentuh berkas sama sekali. Penulisan menggunakan mode "a" agar catatan lama tidak pernah tersentuh, lalu ferror memastikan tulisannya benar-benar tiba. Fungsi daftar dan cari memuat berkas dengan pola yang sama seperti loop-benar.c. Khusus cari, kode keluar 1 diberikan bila tidak ada catatan yang cocok, sehingga kasus tidak ditemukan dapat diuji oleh skrip shell:
# kompilasi dengan peringatan diaktifkan penuh
gcc -Wall -Wextra catatan.c -o catatan
# tambah tiga catatan, berkas catatan.txt terbentuk otomatis
./catatan tambah Ali 90
./catatan tambah Budi 75
./catatan tambah Citra 88
# muat dan tampilkan seluruh catatan
./catatan daftar
# cari catatan satu siswa
./catatan cari Budi
# nilai yang bukan angka ditolak sebelum berkas disentuh
./catatan tambah Ali abc
# nama yang belum tercatat memberi kode keluar 1
./catatan cari DewiCatatan tersimpan: Ali 90
Catatan tersimpan: Budi 75
Catatan tersimpan: Citra 88
Ali 90
Budi 75
Citra 88
Budi 75
Nilai harus berupa angka, diterima: abc
Tidak ada catatan untuk DewiSatu batasan format ini perlu diakui, yaitu penentu %63s hanya membaca satu kata, sehingga nama yang memuat spasi tidak pernah cocok saat dicari. Perluasan yang wajar berikutnya adalah menyimpan nama dalam format berkutip atau menambah operasi hapus yang menulis ulang berkas dari data yang sudah dimuat. Kedua perluasan itu menggunakan perangkat yang sama, yaitu pemeriksaan setiap panggilan I/O dan pembedaan akhir data dari error.
Ringkasan#
- Nilai kembali fungsi baca menentukan kapan pembacaan berhenti, sedangkan feof dan ferror menjelaskan sebabnya setelah perulangan berakhir.
- feof melaporkan pembacaan yang sudah terjadi, sehingga kondisi perulangan baca adalah nilai kembali fungsi bacanya, bukan feof.
- errno, perror, dan strerror mengubah kegagalan diam fopen menjadi pesan yang dapat ditindaklanjuti, dan pesannya ditulis ke stderr.
- Program catatan menerapkan pemeriksaan pada setiap langkah, dengan kode keluar yang bermakna untuk setiap jalur keberhasilan dan kegagalan.
Referensi#
- feof — cppreference — Dokumentasi feof beserta penekanan bahwa nilainya baru sah setelah bacaan gagal.
- ferror — cppreference — Dokumentasi ferror dan clearerr pada cppreference.
- perror(3) — Linux manual pages — Man page perror dan strerror di Linux.
- errno — cppreference — Dokumentasi variabel errno beserta makro kode errornya.