Untuk menyimpan data dalam format teks yang dapat dibaca manusia (dan mudah diperiksa dengan cat atau editor teks), kita menggunakan fprintf untuk menulis dan fscanf untuk membaca. Kedua fungsi ini adalah counterpart dari printf dan scanf yang sudah kita kenal—bedanya, mereka menulis ke atau membaca dari FILE * yang kita berikan, bukan stdout/stdin. Format yang dihasilkan cocok untuk file log, konfigurasi sederhana, atau pertukaran data antar program. Pada chapter ini kita akan menulis data Mahasiswa (nama, umur, nilai) ke file teks, lalu membaca kembali dengan fscanf dan memverifikasi data tetap utuh.

Kontrak fprintf dan fscanf #

ISO/IEC 9899 §7.21.6.1 dan §7.21.6.2 mendefinisikan:

c
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int fscanf(FILE *restrict stream, const char *restrict format, ...);

Kontrak penting:

  • fprintf mengembalikan jumlah karakter yang ditulis — atau nilai negatif bila gagal.
  • fscanf mengembalikan jumlah field yang berhasil diisi — bukan jumlah byte. Bila gagal di awal (misalnya langsung EOF), mengembalikan EOF.
  • Format string mengikuti aturan printf/scanf — %d, %f, %s, %31s (dengan lebar), %[...]* (scan set), dan seterusnya.
  • Whitespace pada fscanf cocok dengan whitespace apa pun di input — termasuk newline, tab, dan space. Inilah sebabnya %s otomatis melompati whitespace sebelum membaca token.

Pola Tulis-Lalu-Baca #

Pola yang sering kita pakai adalah: tulis beberapa record dengan fprintf ke file teks, tutup file, lalu buka kembali dengan mode "r" dan baca dengan fscanf di dalam loop. Kondisi loop bergantung pada return value fscanf.

c
#include <stdio.h>

typedef struct {
    char nama[32];
    int  umur;
    float nilai;
} Mahasiswa;

int main(void) {
    /* Tulis data terformat ke file */
    FILE *f = fopen("/tmp/test_data.txt", "w");
    if (!f) { perror("fopen"); return 1; }

    Mahasiswa mhs[] = {
        {"Budi", 20, 85.5f},
        {"Siti", 21, 92.3f},
        {"Andi", 19, 78.0f},
    };
    for (int i = 0; i < 3; i++) {
        fprintf(f, "%s %d %.2f\n", mhs[i].nama, mhs[i].umur, mhs[i].nilai);
    }
    fclose(f);

    /* Baca kembali dengan fscanf */
    f = fopen("/tmp/test_data.txt", "r");
    if (!f) { perror("fopen read"); return 1; }

    printf("Data yang dibaca kembali:\n");
    Mahasiswa tmp;
    int n = 0;
    /* fscanf mengembalikan jumlah field yang berhasil diisi */
    while (fscanf(f, "%31s %d %f", tmp.nama, &tmp.umur, &tmp.nilai) == 3) {
        printf("  [%d] nama=%s umur=%d nilai=%.2f\n",
               n, tmp.nama, tmp.umur, tmp.nilai);
        n++;
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Data yang dibaca kembali:
  [0] nama=Budi umur=20 nilai=85.50
  [1] nama=Siti umur=21 nilai=92.30
  [2] nama=Andi umur=19 nilai=78.00

Setelah program selesai, file /tmp/test_data.txt berisi:

text
Budi 20 85.50
Siti 21 92.30
Andi 19 78.00

Mari kita telaah baris-baris kode di atas:

  1. Baris 14 (fprintf(f, "%s %d %.2f\n", mhs[i].nama, mhs[i].umur, mhs[i].nilai);): menulis satu baris per record. Specifier %.2f memaksa dua angka di belakang koma—penting agar fscanf dapat membaca kembali tanpa kehilangan presisi tampilan. Karakter \n di akhir memisahkan setiap record ke baris berbeda.
  2. Baris 21 (fopen(..., "r");): kita menutup writer stream terlebih dahulu dengan fclose agar buffer ter-flush, baru membuka kembali sebagai reader stream.
  3. Baris 28 (while (fscanf(f, "%31s %d %f", tmp.nama, &tmp.umur, &tmp.nilai) == 3)): inilah idiom kunci. fscanf mengembalikan jumlah field yang berhasil diproses; bila berhasil membaca ketiga field, kembalian adalah 3. Kondisi == 3 menjamin loop berhenti tepat saat salah satu field tidak terbaca (misalnya karena trailing whitespace atau EOF).
  4. Baris 28 (%31s): penentu lebar 31 membatasi pembacaan maksimal 31 karakter plus null terminator—mencegah buffer overflow pada tmp.nama yang berukuran 32. Ini setara dengan fgets yang membatasi pembacaan.
  5. Baris 29-31 (printf(...)): mencetak kembali data yang baru dibaca untuk verifikasi. Nilai 85.50 yang dikembalikan fscanf sama persis dengan yang kita tulis karena presisi %.2f cukup untuk tipe float 32-bit.

Jebakan fscanf yang Sering Menggigit #

fscanf tampak sederhana, tetapi memilik banyak jebakan. Tiga jebakan utama:

1. Tanpa Lebar pada %s → Buffer Overflow #

c
char nama[8];
fscanf(f, "%s", nama);   /* BERBAHAYA bila token > 7 char */

Tanpa angka di antara % dan s, fscanf akan terus membaca sampai menemui whitespace, terlepas dari ukuran buffer. Token "Alexander" akan menimpa memori setelah nama. Selalu tulis %N-1s (misalnya %31s untuk buffer 32 byte).

2. Format Tidak Cocok → Data Tersisa di Stream #

c
int x;
fscanf(f, "%d", &x);   /* bila file berisi "abc", kembalian 0 */

Bila input tidak cocok dengan format, fscanf mengembalikan jumlah field yang sudah diproses (mungkin 0) dan menyisakan input yang gagal diproses di stream. Tanpa pembersihan, loop berikutnya akan terus membaca karakter yang sama. Karena itu, selalu periksa return value dan jangan asumsikan input selalu valid.

3. %s Tidak Membaca Nama dengan Spasi #

Bila file kita berisi "Budi Santoso 20 85.50", %s hanya membaca "Budi" dan menyisakan "Santoso 20 85.50". Untuk nama dengan spasi, gunakan scan set %[^\n] atau %31[^\t] (baca sampai newline atau tab).

Demo: Membaca Format Bebas #

Mari kita lihat bagaimana fscanf menangani input yang formatnya tidak persis sama dengan ekspektasi.

c
#include <stdio.h>

int main(void) {
    /* File berisi beberapa baris dengan format berbeda */
    FILE *f = fopen("/tmp/test_format_mixed.txt", "w");
    if (!f) return 1;
    fputs("Budi 20 85.5\n", f);
    fputs("Siti Santoso 21 92.3\n", f);   /* nama ada spasi */
    fputs("Andi 19\n", f);                /* tanpa nilai */
    fputs("Dono 22 abc 75.0\n", f);       /* nilai bukan angka */
    fclose(f);

    f = fopen("/tmp/test_format_mixed.txt", "r");
    if (!f) return 1;

    char nama[64];
    int umur;
    float nilai;
    int rc;
    int baris = 0;

    while ((rc = fscanf(f, "%63s %d %f", nama, &umur, &nilai)) != EOF) {
        baris++;
        printf("Baris %d: rc=%d, nama=\"%s\", umur=%d", baris, rc, nama, umur);
        if (rc >= 3) printf(", nilai=%.2f", nilai);
        printf("\n");

        /* Bila rc < 3, ada sisa yang tidak terbaca — bersihkan baris */
        if (rc < 3) {
            int c;
            while ((c = fgetc(f)) != EOF && c != '\n') { /* skip */ }
        }
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Baris 1: rc=3, nama="Budi", umur=20, nilai=85.50
Baris 2: rc=1, nama="Siti", umur=20
Baris 3: rc=2, nama="Andi", umur=19

Perhatikan tiga baris hasil di atas:

  • Baris 1: file berisi "Budi 20 85.5\n". fscanf membaca nama="Budi", umur=20, dan nilai=85.5 (3 field), sehingga rc=3.
  • Baris 2: file berisi "Siti Santoso 21 92.3\n". fscanf membaca nama="Siti", lalu mencoba membaca %d tetapi menemui "Santoso" yang bukan integer. fscanf berhenti dengan rc=1. Variabel umur masih menyimpan nilai 20 dari iterasi sebelumnya—ini bug umum: fscanf tidak mengubah variabel yang gagal diisi. Setelahnya kita membersihkan sisa baris dengan fgetc hingga \n.
  • Baris 3: file berisi "Andi 19\nDono 22 abc 75.0\n". fscanf membaca nama="Andi", umur=19, lalu %f melewati \n (karena %f melewati whitespace) dan mencoba membaca "Dono" sebagai float—gagal. rc=2. Setelahnya kita membersihkan sisa baris sampai \n, sehingga baris "Dono 22 abc 75.0\n" ikut ter-skip.

Tanpa pembersihan baris dengan fgetc ketika rc < 3, fscanf akan terus mencoba membaca karakter yang sama yang menyebabkan kegagalan—loop menjadi infinite loop pada posisi yang tidak bergerak.