Untuk membaca dan menulis blok data biner secara efisien—array, struct, atau buffer besar—fread dan fwrite adalah pilihan yang tepat. Berbeda dengan fscanf/fprintf yang harus memformat teks, fread/fwrite memindahkan byte apa adanya antara memori dan file. Tidak ada translasi, tidak ada parser, tidak ada overhead format. Pada chapter ini kita akan menulis array struct Mahasiswa ke file biner dan membacanya kembali, lalu melihat bagaimana argumen size dan nmemb memengaruhi return value dan interpretasi data.
Kontrak fread dan fwrite #
ISO/IEC 9899 §7.21.8.1 dan §7.21.8.2 mendefinisikan:
size_t fread(void *restrict ptr, size_t size, size_t nmemb,
FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t nmemb,
FILE *restrict stream);Kedua fungsi menerima tiga parameter kunci:
ptr— pointer ke buffer memori (sumber untukfwrite, tujuan untukfread).size— ukuran tiap elemen dalam byte (misalnyasizeof(int)atausizeof(struct Mahasiswa)).nmemb— jumlah elemen yang ingin ditulis/dibaca.stream— file stream yang sudah dibuka dengan mode yang sesuai.
Kedua fungsi mengembalikan jumlah elemen (bukan byte!) yang berhasil diproses. Bila nilai kembali kurang dari nmemb, terjadi EOF (untuk fread) atau error I/O. Untuk membedakan keduanya, gunakan feof dan ferror setelah pemanggilan.
Pola Tulis-Lalu-Baca Array Struct #
Pola yang umum adalah menulis array berukuran count elemen, lalu membaca kembali dengan count yang sama (atau buffer lebih besar untuk menampung seluruh file). Kita gunakan sizeof(*T) sebagai size agar otomatis benar bila tipe berubah.
#include <stdio.h>
#include <string.h>
typedef struct {
char nama[32];
int umur;
float nilai;
} Mahasiswa;
int main(void) {
/* Buat array of struct */
Mahasiswa mhs[] = {
{"Budi", 20, 85.5f},
{"Siti", 21, 92.3f},
{"Andi", 19, 78.0f},
};
size_t count = sizeof(mhs) / sizeof(mhs[0]);
/* Tulis array ke file biner */
FILE *f = fopen("/tmp/test_data.bin", "wb");
if (!f) { perror("fopen wb"); return 1; }
size_t written = fwrite(mhs, sizeof(Mahasiswa), count, f);
fclose(f);
printf("Menulis %zu record (total %zu byte)\n",
written, written * sizeof(Mahasiswa));
/* Baca kembali ke array baru */
f = fopen("/tmp/test_data.bin", "rb");
if (!f) { perror("fopen rb"); return 1; }
Mahasiswa hasil[8];
size_t read = fread(hasil, sizeof(Mahasiswa), 8, f);
printf("Membaca %zu record\n", read);
/* Verifikasi data */
for (size_t i = 0; i < read; i++) {
printf(" [%zu] nama=%s umur=%d nilai=%.2f\n",
i, hasil[i].nama, hasil[i].umur, hasil[i].nilai);
}
/* Bandingkan dengan memcmp */
int sama = (memcmp(mhs, hasil, count * sizeof(Mahasiswa)) == 0);
printf("Data awal dan hasil baca %s\n", sama ? "IDENTIK" : "BERBEDA");
fclose(f);
return 0;
}Output yang Dihasilkan:
Menulis 3 record (total 120 byte)
Membaca 3 record
[0] nama=Budi umur=20 nilai=85.50
[1] nama=Siti umur=21 nilai=92.30
[2] nama=Andi umur=19 nilai=78.00
Data awal dan hasil baca IDENTIKTiga record ditulis sebagai 120 byte (40 byte per Mahasiswa — 32 byte nama + 4 byte umur + 4 byte nilai). Setelah dibaca kembali, data identik dengan aslinya—dibuktikan oleh memcmp yang mengembalikan 0.
Mari kita telaah baris-baris kode di atas:
- Baris 21 (
fwrite(mhs, sizeof(Mahasiswa), count, f);): menuliscountelemen berukuransizeof(Mahasiswa). Argumensizeadalah ukuran tiap elemen,nmembadalah jumlah elemen. Return valuewrittenadalah jumlah elemen yang berhasil ditulis (bukan byte). - Baris 23 (
fclose(f);): flush dan tutup. Penting untuk memastikan data benar-benar di disk sebelum dibaca kembali. - Baris 28 (
size_t read = fread(hasil, sizeof(Mahasiswa), 8, f);): kita meminta membaca hingga 8 elemen, tetapi file hanya berisi 3—freadakan berhenti di EOF dan mengembalikan3. - Baris 33-35 (
printf(...)): kita mencetak tiap record untuk verifikasi. Karenanamaadalah arraychar[32]dengan asumsi selalu null-terminated, kita bisa memakainya dengan%s. Pada produksi, gunakan panjang eksplisit bila null terminator tidak dijamin. - Baris 38 (
memcmp(mhs, hasil, count * sizeof(Mahasiswa)) == 0): membandingkan byte demi byte. Bila sama, mengembalikan0. Inilah cara paling ketat memverifikasi bahwa serialisasi dan deserialisasi tidak mengubah data.
Memahami size vs nmemb #
fread/fwrite memisahkan size dan nmemb karena alasan kuno: bila terjadi short read (EOF di tengah), pemanggil ingin tahu berapa elemen lengkap yang berhasil diproses, bukan berapa byte. Contoh:
- Bila
size=sizeof(int)=4dannmemb=5,freadmengembalikan2bila hanya 8 byte yang tersisa di file (2 elemen lengkap, sisa 0 byte untuk elemen ketiga). - Bila
size=1dannmemb=20,freadmengembalikan8bila hanya 8 byte yang tersisa.
Pemisahan ini memudahkan kita memutuskan apakah data parsial masih berguna. Pada umumnya kita pakai size=sizeof(T) dan nmemb=count agar return value langsung berupa jumlah elemen lengkap.
#include <stdio.h>
int main(void) {
/* Tulis array of int */
int arr[] = {10, 20, 30, 40, 50};
FILE *f = fopen("/tmp/test_ints.bin", "wb");
if (!f) { perror("fopen"); return 1; }
size_t n = fwrite(arr, sizeof(int), 5, f);
fclose(f);
printf("fwrite menulis %zu elemen (size=%zu)\n", n, sizeof(int));
/* Baca kembali dengan size=1 (byte mode) */
f = fopen("/tmp/test_ints.bin", "rb");
if (!f) { perror("fopen"); return 1; }
unsigned char buf[32] = {0};
size_t r = fread(buf, 1, 20, f);
printf("fread (size=1) membaca %zu byte\n", r);
printf("Isi byte: ");
for (size_t i = 0; i < r; i++) printf("%02X ", buf[i]);
printf("\n");
/* Interpretasi sebagai int (little-endian) */
rewind(f);
int as_int[5];
r = fread(as_int, sizeof(int), 5, f);
printf("fread (size=sizeof(int)) membaca %zu elemen: ", r);
for (size_t i = 0; i < r; i++) printf("%d ", as_int[i]);
printf("\n");
fclose(f);
return 0;
}Output yang Dihasilkan:
fwrite menulis 5 elemen (size=4)
fread (size=1) membaca 20 byte
Isi byte: 0A 00 00 00 14 00 00 00 1E 00 00 00 28 00 00 00 32 00 00 00
fread (size=sizeof(int)) membaca 5 elemen: 10 20 30 40 50Perhatikan output byte di atas: angka 10 ditulis sebagai 0A 00 00 00 (little-endian 32-bit). Ini artinya byte paling tidak signifikan (0A) berada di alamat byte pertama. Format little-endian dipakai oleh arsitektur x86 dan x86-64—bila kita membaca file ini di mesin big-endian (misalnya SPARC lama), kita akan dapat 0x0A000000 = 167772160 alih-alih 10. Untuk file lintas-platform, gunakan fungsi htonl/ntohl (POSIX) atau serialisasi eksplisit byte-per-byte.
Memeriksa EOF dan Error #
fread mengembalikan jumlah elemen yang berhasil dibaca—bisa kurang dari nmemb karena EOF atau error I/O. Untuk membedakan, gunakan feof dan ferror.
#include <stdio.h>
int main(void) {
/* Tulis file berisi 3 int (12 byte) */
int arr[] = {100, 200, 300};
FILE *f = fopen("/tmp/test_short.bin", "wb");
if (!f) return 1;
fwrite(arr, sizeof(int), 3, f);
fclose(f);
/* Coba baca 10 elemen (40 byte) — hanya 3 yang ada */
f = fopen("/tmp/test_short.bin", "rb");
if (!f) return 1;
int buf[10];
size_t r = fread(buf, sizeof(int), 10, f);
printf("fread meminta 10 elemen, dapat %zu elemen\n", r);
if (feof(f)) {
printf(" -> sebab: EOF tercapai\n");
}
if (ferror(f)) {
printf(" -> sebab: error I/O\n");
}
fclose(f);
return 0;
}Output yang Dihasilkan:
fread meminta 10 elemen, dapat 3 elemen
-> sebab: EOF tercapaiKarena file hanya berisi 12 byte (3 elemen int), fread berhenti di tengah panggilan dan menetapkan flag EOF. Flag EOF ini hanya bisa dibersihkan dengan clearerr atau rewind/fseek—pemanggilan fread berikutnya tanpa clearerr akan langsung mengembalikan 0.