Hampir setiap program serius perlu menyimpan data melebihi lifetime proses—konfigurasi, log, hasil pengolahan, maupun data pengguna. Tanpa kemampuan menyimpan dan memuat kembali data dari disk, program kita hanya bisa bekerja selama berjalan, lalu kehilangan semuanya saat keluar. Di C, abstraksi utama untuk urusan ini adalah file: deretan byte pada storage yang program akses melalui stream. Pada chapter ini kita akan memahami apa sebenarnya file dari sudut pandang sistem operasi, bagaimana bahasa C memodelkannya dengan tipe FILE, dan kenapa konsep stream byte inilah yang membuat operasi file di C terasa seperti membaca dari keyboard atau menulis ke layar.

File sebagai Stream Byte #

Pada level sistem operasi, sebuah file adalah urutan byte yang disimpan pada perangkat storage (disk, SSD, flash). Sistem operasi tidak peduli apakah byte-byte itu merepresentasikan teks, gambar, angka floating-point, atau executable—semuanya hanya urutan byte. Arti dari byte-byte tersebut sepenuhnya ditentukan oleh program yang membaca atau menulisnya. Inilah yang disebut stream of bytes: model I/O yang memperlakukan file sebagai pipe byte sekuensial, di mana program boleh menafsirkan setiap byte sesuka hati.

Sebagai contoh, sebuah file berisi lima byte 0x48 0x69 0x00 0x43 0x0A bisa dibaca sebagai:

  • Teks “Hi” bila kita menganggap byte 0x00 sebagai terminator string.
  • Buffer biner berisi 5 byte bila kita membacanya dengan fread ke dalam array unsigned char[5].
  • Satu integer 32-bit little-endian bila kita membaca 4 byte pertama sebagai int.

Perbedaan interpretasi ini sepenuhnya tergantung mode yang kita pilih saat membuka file dan fungsi I/O yang kita pakai—sistem operasi tidak ikut campur.

c
#include <stdio.h>

int main(void) {
    /* Tulis file teks ke /tmp untuk demo konsep stream byte */
    FILE *f = fopen("/tmp/test_konsep.txt", "wb");
    if (!f) { perror("fopen"); return 1; }

    /* Tulis beberapa byte: huruf, angka, null byte, newline */
    unsigned char bytes[] = {'H', 'i', 0, 'C', '\n'};
    fwrite(bytes, 1, sizeof(bytes), f);
    fclose(f);

    /* Baca kembali byte demi byte */
    f = fopen("/tmp/test_konsep.txt", "rb");
    if (!f) { perror("fopen"); return 1; }

    printf("Isi file sebagai byte:\n");
    int c;
    long pos = 0;
    while ((c = fgetc(f)) != EOF) {
        printf("  pos %ld: 0x%02X  %d", pos, (unsigned)c, c);
        if (c >= 32 && c < 127) printf("  '%c'", c);
        printf("\n");
        pos++;
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Isi file sebagai byte:
  pos 0: 0x48  72  'H'
  pos 1: 0x69  105  'i'
  pos 2: 0x00  0
  pos 3: 0x43  67  'C'
  pos 4: 0x0A  10

Perhatikan bahwa byte ke-3 (0x00) tetap ditulis apa adanya. Bila kita membaca file ini sebagai string C, byte null akan dianggap terminator dan string akan tampak hanya berisi "Hi". Tetapi pada level stream byte, file tersebut tetap berisi 5 byte dan program kita membaca semuanya dengan fgetc.

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (FILE *f = fopen(..., "wb");): kita membuka file dengan mode "wb" (write binary). Tipe FILE * adalah opaque pointer yang menunjuk ke struktur internal milik C standard library—kita tidak perlu tahu isinya, hanya perlu tahu cara menggunakannya.
  2. Baris 10 (unsigned char bytes[] = {'H', 'i', 0, 'C', '\n'};): kita membuat array 5 byte yang sengaja memuat null byte untuk mendemonstrasikan bahwa file benar-benar menyimpan byte apa adanya.
  3. Baris 11 (fwrite(bytes, 1, sizeof(bytes), f);): menulis 5 elemen berukuran 1 byte dari bytes ke stream f. Argumen kedua adalah ukuran tiap elemen, ketiga adalah jumlah elemen.
  4. Baris 14 (f = fopen(..., "rb");): membuka kembali file yang sama dengan mode "rb" (read binary) untuk membaca isinya.
  5. Baris 19 (while ((c = fgetc(f)) != EOF)): fgetc membaca satu byte dan mengembalikan int (bukan char) supaya bisa membedakan byte 0xFF yang sah dari penanda EOF (yang biasanya -1).
  6. Baris 21-24 (printf(" pos %ld: 0x%02X %d", ...)): mencetak posisi, nilai heksadesimal, dan nilai desimal. Bila byte berada di rentang ASCII printable (32-126), kita juga mencetak karakternya.

Abstraksi FILE di C #

Standar ISO/IEC 9899 §7.21.1 mendefinisikan tipe FILE sebagai object type yang mampu merekam semua informasi yang dibutuhkan untuk mengendalikan sebuah stream: posisi file position indicator, penanda end-of-file, penanda error, pointer ke buffer, serta mode yang sedang aktif. Detail isi struct FILE berbeda antar platform—di glibc Linux ada fields seperti _fileno, _flags, _IO_read_ptr—tetapi kita tidak pernah mengakses fields tersebut secara langsung.

Yang penting bagi kita adalah: setiap operasi I/O membutuhkan FILE * yang valid. Tiga stream standar sudah dibuka otomatis oleh runtime C dan dapat dipakai langsung:

  • stdin — stream input standar (biasanya keyboard).
  • stdout — stream output standar (biasanya terminal).
  • stderr — stream error standar (biasanya terminal, tidak di-buffer).

Ketiganya bertipe FILE * sehingga fungsi seperti fprintf(stdin, ...) valid secara sintaks meskipun tidak masuk akal secara semantik. Hubungan ini menjelaskan kenapa printf("halo") dan fprintf(stdout, "halo") ekuivalen—keduanya menulis ke stream yang sama.

Tipe Data dan Mode Interpretasi #

Untuk memperjelas perbedaan antara file sebagai stream byte dan interpretasi yang kita berikan, mari kita lihat bagaimana file 0x48 0x69 0x00 0x43 0x0A dapat ditafsirkan secara berbeda dalam satu program:

c
#include <stdio.h>
#include <stddef.h>

/* Versi sederhana strlen yang menghormati panjang maksimum */
size_t strlen_dengan_null(const char *s, size_t maks) {
    size_t i = 0;
    while (i < maks && s[i] != '\0') i++;
    return i;
}

int main(void) {
    FILE *f = fopen("/tmp/test_konsep.txt", "rb");
    if (!f) { perror("fopen"); return 1; }

    /* Tafsir 1: stream byte mentah */
    unsigned char buf[5];
    size_t n = fread(buf, 1, 5, f);
    printf("Sebagai %zu byte: %02X %02X %02X %02X %02X\n",
           n, buf[0], buf[1], buf[2], buf[3], buf[4]);

    /* Tafsir 2: string C (null-terminated) */
    char str[6] = {0};
    rewind(f);
    fread(str, 1, 5, f);
    printf("Sebagai string: \"%s\" (panjang %zu)\n", str, strlen_dengan_null(str, 5));
    printf("String null-terminated terpotong di byte 0x00\n");

    /* Tafsir 3: integer 32-bit little-endian dari 4 byte pertama */
    rewind(f);
    unsigned int angka = 0;
    fread(&angka, 4, 1, f);
    printf("Sebagai uint32 LE: %u (0x%08X)\n", angka, angka);

    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Sebagai 5 byte: 48 69 00 43 0A
Sebagai string: "Hi" (panjang 2)
String null-terminated terpotong di byte 0x00
Sebagai uint32 LE: 1124100424 (0x43006948)

Karakteristik Penting Stream di C #

Beberapa karakteristik stream di C yang perlu kita pahami sebelum melangkah ke chapter berikutnya:

  • Buffered — kebanyakan stream yang dibuka dengan fopen di-buffer oleh C standard library untuk efisiensi. Kita bisa mengubah perilaku buffering dengan setvbuf.
  • Sequential by default — posisi baca/tulis maju otomatis setelah setiap operasi, tetapi fseek dan ftell memungkinkan akses acak.
  • Directional — stream memiliki arah baca atau tulis (atau keduanya bila dibuka dengan mode seperti "r+"). Mencampur baca dan tulis tanpa fflush atau fseek di antaranya menyebabkan undefined behavior.
  • Two modes: text dan binary — pada text mode, C standard library menerjemahkan newline sesuai konvensi platform. Pada binary mode, byte apa adanya.

Pada chapter berikutnya kita akan membedah tipe FILE * lebih dalam, termasuk bagaimana file position indicator bekerja dan apa yang dimaksud dengan file descriptor di balik layar.