I/O Biner

Menyimpan dan membaca data dengan fread dan fwrite, termasuk struct, serta catatan portabilitasnya.

Daftar isi

Berkas biner menyimpan data sebagai salinan byte mentah dari memori, tanpa konversi ke teks dan tanpa aturan baris. Topik ini membahas pasangan fwrite dan fread untuk menulis serta membaca blok byte, pemanfaatannya untuk menyimpan struct beserta array-nya, dan risiko portabilitas yang menyertai pendekatan ini. Setelah membacanya, data numerik dan struct dapat disimpan lalu dibaca kembali secara tepat, disertai pemahaman tentang kapan pendekatan ini layak digunakan.

fwrite: menulis data mentah#

Fungsi fwrite menyalin blok byte dari memori ke stream tanpa mengubah isinya sedikit pun. Fungsi ini menerima empat parameter, yaitu alamat data, ukuran satu elemen dalam byte, jumlah elemen, dan stream tujuan. Nilai kembanya berupa jumlah elemen yang benar-benar tertulis. Array empat bilangan int 32-bit menghasilkan berkas berukuran 16 byte karena setiap elemen disalin apa adanya:

c tulis-angka.c
#include <stdio.h>

int main(void) {
    int nilai[4] = {10, 20, 30, 40};

    FILE *berkas = fopen("angka.bin", "wb");
    if (berkas == NULL) {
        perror("fopen angka.bin");
        return 1;
    }

    size_t tertulis = fwrite(nilai, sizeof(int), 4, berkas);
    fclose(berkas);

    printf("Elemen tertulis: %zu\n", tertulis);
    return 0;
}
text Keluaran
Elemen tertulis: 4

Isi angka.bin tidak dapat dibaca manusia, karena berkas itu memang potongan memori yang disalin utuh. Perintah od menampilkan isi berkas byte demi byte dalam heksadesimal sehingga salinan memori itu terlihat:

bash
# kompilasi lalu jalankan program penulis
gcc tulis-angka.c -o tulis-angka && ./tulis-angka
# tampilkan isi angka.bin byte demi byte dalam heksadesimal
od -A d -t x1 angka.bin
text Keluaran
Elemen tertulis: 4
0000000 0a 00 00 00 14 00 00 00 1e 00 00 00 28 00 00 00
0000016

Setiap bilangan menempati empat byte karena sizeof(int) bernilai 4 pada platform ini. Nilai 10 tersimpan sebagai 0a 00 00 00, yaitu byte paling penting diletakkan paling akhir. Pola itu disebut urutan byte (endianness) little-endian yang umum pada prosesor x86-64. Teks "10" untuk nilai yang sama hanya memuat dua karakter, sehingga dua penyimpanan itu berbeda bentuk sekaligus berbeda ukuran. Diagram berikut membandingkan kedua bentuk penyimpanan itu dengan contoh nilai 12345:

Nilai 12345 yang sama dalam bentuk teks dan binerNilai 12345 yang sama dalam dua bentuk penyimpananfprintf(berkas, "%d", 12345)123455 byte teks ASCIIfwrite(&n, sizeof(int), 1, berkas)393000004 byte biner (little-endian)nilai yang sama, tata letak byte berbeda
Nilai 12345 dalam bentuk teks ASCII dan bentuk biner

fread: membaca data mentah#

Fungsi fread bekerja berlawanan arah dengan fwrite, yaitu menyalin blok byte dari stream ke memori. Nilai kembanya berupa jumlah elemen yang berhasil dibaca. Nilai itu bisa lebih kecil dari yang diminta apabila berkas habis lebih dahulu atau terjadi error, sehingga hasilnya selalu diperiksa. Program berikut membaca kembali angka.bin yang ditulis oleh tulis-angka.c, sehingga program penulis dijalankan lebih dahulu:

c baca-angka.c
#include <stdio.h>

int main(void) {
    int bacaan[4] = {0, 0, 0, 0};

    FILE *berkas = fopen("angka.bin", "rb");
    if (berkas == NULL) {
        perror("fopen angka.bin");
        return 1;
    }

    size_t terbaca = fread(bacaan, sizeof(int), 4, berkas);
    fclose(berkas);

    printf("Elemen terbaca: %zu\n", terbaca);
    for (size_t i = 0; i < terbaca; i++) {
        printf("bacaan[%zu] = %d\n", i, bacaan[i]);
    }
    return 0;
}
text Keluaran
Elemen terbaca: 4
bacaan[0] = 10
bacaan[1] = 20
bacaan[2] = 30
bacaan[3] = 40

Empat elemen kembali bernilai persis seperti aslinya karena byte yang ditulis dan dibaca berada pada platform yang sama. Inilah keunggulan data mentah, yaitu tanpa kerugian presisi dan tanpa tahap pengubahan teks menjadi angka. Kelemahannya berbanding terbalik. Berkas mentah tidak terbaca manusia, dan maknanya bergantung pada tata letak memori platform penulisnya.

Urutan menjalankan program penulis dan program pembaca perlu dijaga, karena angka.bin baru ada setelah tulis-angka.c dieksekusi. Bila program pembaca dijalankan lebih dahulu, fopen mengembalikan NULL dan programnya berhenti dengan pesan error, bukan dengan data. Perhatikan pula tipe size_t pada variabel terbaca, yaitu tipe yang sama dengan nilai kembali fread, sehingga format %zu menjadi pasangannya pada printf.

Menyimpan struct dengan fwrite#

Struct dapat ditulis utuh dalam satu panggilan fwrite karena penyusunnya di memori sudah berurutan. Ukuran yang disalin adalah sizeof(Siswa), termasuk byte pengisi (padding) yang disisipkan kompiler agar setiap anggota sejajar. Array struct pun cukup satu panggilan karena elemennya bersambung di memori:

c simpan-siswa.c
#include <stdio.h>

typedef struct {
    char nama[32];
    int umur;
    double ipk;
} Siswa;

int main(void) {
    Siswa kelas[2] = {
        {"Ali",   19, 3.75},
        {"Citra", 20, 3.90},
    };

    FILE *berkas = fopen("kelas.bin", "wb");
    if (berkas == NULL) {
        perror("fopen kelas.bin");
        return 1;
    }

    size_t tertulis = fwrite(kelas, sizeof(Siswa), 2, berkas);
    fclose(berkas);

    printf("Ukuran satu Siswa : %zu byte\n", sizeof(Siswa));
    printf("Struct tertulis   : %zu\n", tertulis);
    return 0;
}
text Keluaran
Ukuran satu Siswa : 48 byte
Struct tertulis   : 2

Ukuran 48 byte berasal dari 32 byte nama, 4 byte umur, 8 byte ipk, ditambah 4 byte pengisi sebelum anggota double yang menuntut posisi sejajar 8 byte. Berkas kelas.bin berukuran 96 byte karena memuat dua struct utuh, dan sisa array nama yang tidak terpakai ikut tersimpan sebagai byte nol. Menyimpan struct dengan cara ini menghemat kode, tetapi menautkan berkasnya pada tata letak memori kompiler penulisnya.

Salinan memori ini menjelaskan mengapa fwrite disebut menulis data mentah, yaitu tidak ada satu pun penentu format atau pengubahan nilai yang terjadi. Anggota nama yang hanya terisi empat karakter tetap menempati 32 byte karena sisanya byte nol hasil inisialisasi array, dan semuanya ikut tersimpan. Kepadatan semacam ini efisien untuk data besar, tetapi menuntut pembacanya memahami tata letak yang sama dengan penulisnya.

Risiko portabilitas berkas biner#

Berkas hasil fwrite dapat dibaca kembali dengan andal hanya pada lingkungan yang tata letak memorinya sama dengan penulisnya. Tiga perbedaan yang paling sering muncul adalah urutan byte yang berlawanan antararsitektur, ukuran tipe yang berbeda antarplatform, dan susunan pengisi yang berbeda antarkompiler. Berkas biner yang ditulis komputer 64-bit little-endian dapat terbaca kacau pada mesin dengan aturan sebaliknya.

Program berikut membaca kelas.bin yang ditulis pada bagian sebelumnya, sehingga program simpan-siswa.c dijalankan lebih dahulu. Pada platform yang sama hasilnya selalu utuh:

c baca-siswa.c
#include <stdio.h>

typedef struct {
    char nama[32];
    int umur;
    double ipk;
} Siswa;

int main(void) {
    Siswa bacaan[2];

    FILE *berkas = fopen("kelas.bin", "rb");
    if (berkas == NULL) {
        perror("fopen kelas.bin");
        return 1;
    }

    size_t terbaca = fread(bacaan, sizeof(Siswa), 2, berkas);
    fclose(berkas);

    printf("Struct terbaca: %zu\n", terbaca);
    for (size_t i = 0; i < terbaca; i++) {
        printf("%-6s umur %d, IPK %.2f\n", bacaan[i].nama, bacaan[i].umur, bacaan[i].ipk);
    }
    return 0;
}
text Keluaran
Struct terbaca: 2
Ali    umur 19, IPK 3.75
Citra  umur 20, IPK 3.90

Format berkas yang digunakan lintas sistem, seperti format gambar, tidak pernah menyimpan struct mentah. Penulisnya menyusun byte satu per satu dengan urutan yang disepakati, menggunakan tipe berlebar tetap dari stdint.h seperti int32_t, dan mendokumentasikan aturannya. Pendekatan yang sama berlaku bagi program sendiri yang datanya perlu berpindah mesin. Adapun fwrite tetap tepat untuk berkas sementara yang hidupnya hanya di satu lingkungan.

Kesalahan umum#

Kesalahan pertama adalah membuka berkas biner tanpa akhiran b. Pada Linux akibatnya tidak terlihat, tetapi pada Windows mode teks menerjemahkan pindah baris sehingga byte data ikut berubah dan berkasnya rusak. Gunakan "wb" dan "rb" untuk data biner agar perilakunya benar di semua platform.

Kesalahan kedua adalah membalik argumen ukuran dan jumlah pada fwrite atau fread, misalnya menulis sizeof(int) pada posisi jumlah. Total byte yang dipindahkan bisa saja sama, tetapi nilai kembali yang dihitung dalam satuan elemen menjadi menyesatkan, dan pemeriksaan terhadap nilai kembali itu ikut salah. Pertahankan urutan fwrite(data, sizeof satu elemen, jumlah elemen, stream) dan periksa nilai kembanya terhadap jumlah elemen.

Kesalahan ketiga adalah menyimpan struct yang memiliki anggota pointer. Yang tertulis ke berkas adalah alamat memori, bukan data yang ditunjuk, sehingga hasil bacaan berisi alamat yang tidak berlaku lagi di sesi berikutnya. Data yang ditunjuk pointer harus disimpan terpisah, dan struct seperti ini memang lebih aman diserialisasi anggota demi anggota. Kesalahan keempat adalah mengabaikan nilai kembali fread maupun fwrite, sehingga tulisan yang setengah jalan akibat disk penuh tetap dianggap berhasil.

Ringkasan#

  • fwrite menyalin blok byte dari memori ke stream apa adanya, dan fread menyalinnya kembali dengan pasangan argumen yang sama.
  • Nilai kembali kedua fungsi berupa jumlah elemen, dan nilainya wajib diperiksa karena bisa lebih kecil dari yang diminta.
  • Struct beserta array-nya dapat disimpan utuh dalam satu panggilan, dengan ukuran salinan mengikuti sizeof termasuk padding.
  • Berkas biner hasil salinan memori hanya portabel antarlingkungan yang tata letaknya sama, sehingga format pertukaran data disusun byte demi byte.

Referensi#