Setiap operasi I/O di C yang melibatkan file butuh satu benda: sebuah FILE * yang valid. Tipe FILE * ini bukan sekadar address ke data pada disk—melainkan handle ke sebuah struktur internal yang merekam keadaan stream secara lengkap. Tanpa memahami apa yang dimiliki FILE *, mudah bagi kita untuk salah mengira bahwa fprintf(f, "...") langsung menulis ke disk. Nyatanya, C standard library menggunakan buffer perantara, dan FILE * menyimpan posisi baca/tulis serta status error dan end-of-file. Pada chapter ini kita akan membedah isi konseptual FILE *, mengenal tiga stream standar yang sudah otomatis tersedia, dan melihat bagaimana file position indicator bergerak setiap kali kita membaca atau menulis.
Apa Itu Tipe FILE * #
Standar ISO/IEC 9899 §7.21.1 menyebut FILE sebagai object type yang mampu menyimpan semua informasi untuk mengontrol stream. Implementasi glibc, misalnya, menyimpan di dalamnya:
- File descriptor — integer dari sistem operasi (misalnya
3,4). - Mode —
"r","w","a","r+", dan seterusnya. - Buffer pointer — alamat memori tempat C standard library menampung data sementara.
- Buffer posisi — indeks byte berikutnya yang akan dibaca/ditulis di dalam buffer.
- File position indicator — offset byte logis dalam file (yang dilaporkan
ftell). - EOF indicator — flag yang diset saat operasi baca menemui end-of-file.
- Error indicator — flag yang diset saat terjadi kesalahan I/O.
Kita tidak pernah mengakses field ini langsung—FILE sengaja dibuat opaque. Yang kita lakukan hanyanya memegang FILE * dan meneruskannya ke fungsi I/O.
Tiga Stream Standar #
Sebelum membuka file pertama kita, runtime C sudah menyiapkan tiga stream standar yang dapat dipakai langsung tanpa fopen:
| Nama | Tipe | Tujuan umum | Buffering |
|---|---|---|---|
stdin | FILE * | Input standar (keyboard, pipe) | line-buffered |
stdout | FILE * | Output standar (terminal) | line-buffered (TTY) / fully-buffered (pipe) |
stderr | FILE * | Pesan error | unbuffered |
Karena ketiganya bertipe FILE *, fungsi seperti fprintf, fscanf, fputs, dan fgetc dapat dipakai langsung pada mereka—seperti pada file biasa.
#include <stdio.h>
int main(void) {
/* Tulis ke stdout, stderr, dan sebuah file untuk bandingkan */
fprintf(stdout, "Tulis ke stdout\n");
fprintf(stderr, "Tulis ke stderr (tidak ter-buffer)\n");
FILE *f = fopen("/tmp/test_fp.txt", "w");
if (!f) { perror("fopen"); return 1; }
fprintf(f, "Halo dari file\n");
fclose(f);
/* Demo file position indicator */
f = fopen("/tmp/test_fp.txt", "r");
if (!f) { perror("fopen"); return 1; }
long awal = ftell(f);
printf("Posisi awal: %ld\n", awal);
int c = fgetc(f);
printf("Baca 1 char: '%c' (kode %d)\n", c, c);
printf("Posisi sekarang: %ld\n", ftell(f));
char buf[8] = {0};
fgets(buf, sizeof(buf), f);
printf("Baca sisa baris: \"%s\"\n", buf);
printf("Posisi sekarang: %ld\n", ftell(f));
rewind(f);
printf("Setelah rewind: %ld\n", ftell(f));
fclose(f);
return 0;
}Output yang Dihasilkan:
Tulis ke stdout
Tulis ke stderr (tidak ter-buffer)
Posisi awal: 0
Baca 1 char: 'H' (kode 72)
Posisi sekarang: 1
Baca sisa baris: "alo dar"
Posisi sekarang: 8
Setelah rewind: 0Pada output di atas, baris Tulis ke stderr muncul lebih dulu karena stderr tidak di-buffer—setiap fprintf langsung dikirim ke kernel. Sementara itu, stdout di-buffer hingga newline \n ditemukan (mode line-buffered pada TTY). Setelah membaca satu byte ('H'), posisi file position indicator naik dari 0 ke 1. Setelah fgets membaca 7 byte lagi (alo dar—fgets memotong pada sizeof(buf)-1), posisi naik ke 8. Terakhir, rewind mengembalikan posisi ke 0 tanpa harus menutup file dan membukanya lagi.
Mari kita telaah baris-baris kode di atas:
- Baris 6 (
fprintf(stdout, "Tulis ke stdout\n");): kita menggunakanfprintfdengan argumen pertamastdoutalih-alihprintf. Secara semantik sama persis—printf(...)adalah shortcut darifprintf(stdout, ...)(ISO/IEC 9899 §7.21.3.5 catatan kaki). - Baris 7 (
fprintf(stderr, ...)): menulis kestderryang tidak di-buffer. Pesan muncul sebelum output berikutnya daristdoutjika keduanya dipipa ke file yang sama. - Baris 14 (
long awal = ftell(f);):ftellmengembalikan offset byte logis dari file position indicator relatif terhadap awal file. Pada awalnya posisi adalah0. - Baris 18 (
int c = fgetc(f);):fgetcmembaca 1 byte, mengembalikanint(bukanchar) supaya bisa membedakan byte0xFFyang sah dari penandaEOF. - Baris 22 (
fgets(buf, sizeof(buf), f);): membaca sampai salah satu dari tiga kondisi: menemui\n, mengisisizeof(buf)-1byte, atau menemui EOF.fgetsselalu menambahkan null terminator. - Baris 27 (
rewind(f);): mengembalikan file position indicator ke awal dan menghapus flag EOF serta error—setara denganfseek(f, 0L, SEEK_SET)plusclearerr(f).
File Position Indicator #
Setiap FILE * yang aktif memiliki tepat satu file position indicator (FPI). Indikator inilah yang melacak “di byte ke berapa kita sekarang”. Pada stream sekuensial murni (seperti pipe atau socket), FPI tidak memiliki makna absolut karena data mengalir satu arah. Pada file biasa, FPI dapat dipindahkan dengan fseek dan dibaca dengan ftell.
Karakteristik penting FPI:
- Maju otomatis — setiap
fgetc,fgets,fread,fputc,fputs,fwritememajukan FPI sebanyak byte yang diproses. - Satu per stream — bila kita membuka file yang sama dua kali dengan dua
fopenterpisah, kita mendapat duaFILE *independen dengan masing-masing FPI. - Bisa dibaca tapi terbatas —
ftellmengembalikan tipelong. Untuk file lebih besar dari 2 GB pada sistem 32-bit, gunakanftellodengan tipeoff_tataufseeko.
Demo: Dua Stream pada Satu File #
Untuk membuktikan bahwa dua FILE * pada file yang sama memiliki FPI independen, mari kita lakukan eksperimen sederhana—menulis dengan satu stream sambil membaca dengan stream lain yang dibuka terpisah.
#include <stdio.h>
int main(void) {
/* Stream A: tulis file */
FILE *a = fopen("/tmp/test_fp2.txt", "w");
if (!a) { perror("fopen A"); return 1; }
fprintf(a, "ABCDE\n");
fflush(a); /* pastikan data benar-benar sampai ke disk */
/* Stream B: baca file yang sama, secara independen */
FILE *b = fopen("/tmp/test_fp2.txt", "r");
if (!b) { perror("fopen B"); fclose(a); return 1; }
printf("Posisi A (writer): %ld\n", ftell(a)); /* 6 (setelah menulis) */
printf("Posisi B (reader): %ld\n", ftell(b)); /* 0 (baru dibuka) */
int c = fgetc(b);
printf("B membaca char pertama: '%c'\n", c);
printf("Posisi B setelah baca: %ld\n", ftell(b));
printf("Posisi A setelah B membaca: %ld\n", ftell(a)); /* tetap 6 */
fclose(a);
fclose(b);
return 0;
}Output yang Dihasilkan:
Posisi A (writer): 6
Posisi B (reader): 0
B membaca char pertama: 'A'
Posisi B setelah baca: 1
Posisi A setelah B membaca: 6Hasil di atas membuktikan bahwa meskipun kedua stream menunjuk ke file fisik yang sama, FPI mereka independen. Stream A berada di posisi 6 (setelah menulis 6 byte "ABCDE\n"), sementara stream B mulai dari 0 dan baru sampai 1 setelah membaca satu karakter. Inilah dasar penting yang akan kita pakai pada chapter-chapter berikutnya, terutama saat membahas random access dengan fseek dan ftell.