Sebelum kita bisa membaca atau menulis data ke file, kita harus membukanya terlebih dahulu dengan fopen. Setiap fopen yang berhasil harus diimbangkan dengan fclose pada akhirnya—kalau tidak, file descriptor bocor, buffer tidak ter-flush, dan perubahan terakhir kita mungkin hilang. Sayangnya banyak pemula melewatkan dua hal: memeriksa apakah fopen benar-benar berhasil, dan menangani error yang dilaporkan sistem operasi. Pada chapter ini kita akan membuka file dengan benar—baik skenario sukses maupun gagal—dan menutupnya dengan aman. Kita juga akan melihat apa yang sebenarnya terjadi saat fopen mengembalikan NULL.
Kontrak fopen #
Fungsi fopen dideklarasikan di <stdio.h> (ISO/IEC 9899 §7.21.5.3) dengan signature:
FILE *fopen(const char *restrict filename, const char *restrict mode);Kontraknya sederhana namun ketat:
- Mengembalikan
FILE *non-NULL bila berhasil — pointer ini wajib diperiksa sebelum dipakai. - Mengembalikan
NULLbila gagal — kode error spesifik disimpan dierrno(lihat chaptererrno-dan-perror). - Argumen
filenameadalah path — bisa relatif ("data.txt"), absolut ("/tmp/x"), atau berisi direktori ("sub/dir/x"). - Argumen
modemenentukan arah dan perilaku — kombinasi"r","w","a","+","b", dan"x"(C11).
Mekanisme kerja fopen: C standard library menerjemahkan mode menjadi flags open(2) (misalnya O_RDONLY, O_CREAT|O_TRUNC, O_APPEND), lalu memanggil system call open ke kernel. Kernel mengalokasikan file descriptor integer (misalnya 3), lalu C standard library membungkusnya dalam struct FILE beserta buffer default dan menambahkannya ke stream table. Pada glibc Linux, buffering default untuk file adalah fully buffered dengan ukuran BUFSIZ (biasanya 8192 byte).
Pola Aman: Buka, Periksa, Tutup #
Pola yang selalu kita pakai adalah: panggil fopen, segera periksa apakah hasilnya NULL, lalu tutup dengan fclose di akhir blok yang bertanggung jawab.
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
/* Sukses: buka file untuk ditulis ulang */
FILE *ok = fopen("/tmp/test_fopen.txt", "w");
if (ok == NULL) {
fprintf(stderr, "Gagal membuka /tmp/test_fopen.txt: %s\n",
strerror(errno));
return 1;
}
fprintf(ok, "data demo\n");
int rc = fclose(ok);
printf("fclose berhasil (rc=%d)\n", rc);
/* Kegagalan: buka file yang TIDAK ADA di mode "r" */
FILE *bad = fopen("/tmp/nonexistent_file_xyz.txt", "r");
if (bad == NULL) {
printf("fopen gagal, errno=%d: %s\n", errno, strerror(errno));
} else {
printf("Seharusnya gagal, tapi berhasil!\n");
fclose(bad);
}
/* Kegagalan: buka file di direktori yang tidak ada */
FILE *bad2 = fopen("/no/such/dir/file.txt", "w");
if (bad2 == NULL) {
printf("fopen gagal, errno=%d: %s\n", errno, strerror(errno));
} else {
fclose(bad2);
}
return 0;
}Output yang Dihasilkan:
fclose berhasil (rc=0)
fopen gagal, errno=2: No such file or directory
fopen gagal, errno=2: No such file or directoryPada contoh di atas, fopen("/tmp/nonexistent_file_xyz.txt", "r") kembali dengan NULL karena file tidak ada—mode "r" tidak pernah membuat file baru. Nilai errno=2 adalah konstanta ENOENT yang berarti No such file or directory. Demikian pula, fopen("/no/such/dir/file.txt", "w") gagal karena direktori /no/such/dir/ tidak ada—mode "w" mau membuat file baru, tetapi kernel menolak membuat file di direktori yang tidak ada.
Mari kita telaah baris-baris kode di atas:
- Baris 8 (
FILE *ok = fopen("/tmp/test_fopen.txt", "w");): mode"w"berarti write. Jika file sudah ada, isinya dipotong menjadi kosong; jika belum ada, file dibuat baru. - Baris 9 (
if (ok == NULL)): pengecekan wajib setelah setiapfopen. Tanpa pengecekan,fprintf(ok, ...)pada baris 13 akan segfault karena men-dereferenceNULL. - Baris 14 (
int rc = fclose(ok);):fclosemengembalikan0bila berhasil,EOF(-1) bila gagal. Banyak pemula mengabaikan return valuefclose—padahal flush terakhir bisa gagal (misalnya disk penuh). - Baris 18 (
FILE *bad = fopen("/tmp/nonexistent_file_xyz.txt", "r");): mode"r"membuka file yang sudah ada untuk dibaca; tidak pernah membuat file baru. Karena file tidak ada,fopenmengembalikanNULL. - Baris 20 (
printf("fopen gagal, errno=%d: %s\n", errno, strerror(errno));): kita membacaerrnolalu menerjemahkannya menjadi string denganstrerror. Catatan:errnohanya valid sampai pemanggilan library berikutnya—bila menunda, salin dulu ke variabel lokal. - Baris 27 (
fopen("/no/such/dir/file.txt", "w");): meskipun mode"w"bisa membuat file, kernel menolak karena direktori induknya tidak ada.errnotetapENOENT(2).
Mengapa fclose Tidak Boleh Dilupakan #
fclose bukan sekadar formalitas. Fungsi ini melakukan tiga hal penting:
- Flush buffer — data yang masih berada di C standard library buffer ditulis ke disk via
write(2). Tanpafclose(ataufflush), data yang ditulis terakhir bisa hilang. - Membebaskan resource — file descriptor kernel (sumber daya terbatas, biasanya 1024 per proses) dikembalikan. Buffer memori juga dibebaskan.
- Melaporkan error akhir — bila disk penuh saat flush terakhir,
fclosemengembalikanEOF. Mengabaikan nilai kembali ini menyembabkan kita tidak sadar data hilang.
Lebih Aman dengan fclose pada kondisi error #
Pada fungsi yang melakukan banyak alokasi, kita sering perlu menutup file di tengah jalan karena ada error. Pola yang aman adalah dengan cleanup label di akhir fungsi.
#include <stdio.h>
#include <stdlib.h>
int proses_dengan_cleanup(const char *path) {
FILE *f = fopen(path, "r");
if (f == NULL) return -1;
char *buffer = malloc(1024);
if (buffer == NULL) goto cleanup_file;
/* Lakukan pekerjaan... */
if (fread(buffer, 1, 4, f) != 4) goto cleanup_buffer;
printf("4 byte pertama: %02X %02X %02X %02X\n",
(unsigned char)buffer[0], (unsigned char)buffer[1],
(unsigned char)buffer[2], (unsigned char)buffer[3]);
cleanup_buffer:
free(buffer);
cleanup_file:
fclose(f);
return 0;
}
int main(void) {
/* Buat dummy file dulu */
FILE *w = fopen("/tmp/test_fopen.txt", "w");
if (w) { fprintf(w, "ABCD"); fclose(w); }
return proses_dengan_cleanup("/tmp/test_fopen.txt");
}Output yang Dihasilkan:
4 byte pertama: 41 42 43 44Pola goto cleanup di atas adalah idiom standar C untuk manajemen resource tanpa exception. Setiap resource (file, buffer, lock) ditangani terurut: dialokasikan dari awal ke akhir, dibebaskan dari akhir ke awal. Pada contoh, bila fread gagal membaca 4 byte, kita melompat ke cleanup_buffer yang membebaskan buffer lalu secara fallthrough ke cleanup_file yang menutup file.
Verifikasi dengan File yang Benar-Benar Tertulis #
Untuk yakin bahwa data tersimpan ke disk, kita bisa langsung memverifikasi dengan cat di terminal atau membaca kembali file dengan fopen terpisah. Inilah praktik umum yang dipakai pada chapter-chapter berikutnya.
#include <stdio.h>
int main(void) {
FILE *f = fopen("/tmp/test_fopen_verify.txt", "w");
if (!f) { perror("fopen"); return 1; }
fputs("baris pertama\nbaris kedua\n", f);
if (fclose(f) != 0) {
perror("fclose");
return 1;
}
printf("File ditulis dan ditutup dengan sukses.\n");
/* Baca kembali untuk verifikasi */
f = fopen("/tmp/test_fopen_verify.txt", "r");
if (!f) { perror("fopen read"); return 1; }
char line[64];
while (fgets(line, sizeof(line), f) != NULL) {
printf("Dibaca: %s", line);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
File ditulis dan ditutup dengan sukses.
Dibaca: baris pertama
Dibaca: baris keduaPada chapter file-modes berikutnya, kita akan membahas secara mendalam kombinasi mode seperti "r+", "w+", "a+", dan perbedaan antara text mode dan binary mode.