Setiap kali kita menyimpan data ke file, kita membuat keputusan format: bagaimana memisahkan field, bagaimana menandai akhir record, bagaimana mengetahui versi format. Tanpa format yang jelas, data yang kita tulis hari ini bisa tidak terbaca esok hari—terutama bila skema berubah. Pada chapter ini kita akan membandingkan tiga pendekatan umum: delimiter-based (CSV), fixed-width (kolom tetap), dan header + body (magic number). Masing-masing memiliki trade-off antara keterbacaan, efisiensi, dan kekakuan skema.
Tiga Pendekatan Format File #
Pilihan format file biasanya jatuh pada salah satu dari tiga pendekatan:
| Format | Karakteristik | Kelebihan | Kekurangan |
|---|---|---|---|
| Delimiter (CSV/TSV) | Field dipisahkan karakter (,, \t, ;) | Mudah dibaca, fleksibel | Field tidak boleh mengandung delimiter (perlu escape) |
| Fixed-width | Tiap field menempati jumlah byte tetap | Akses acak mudah, parse sederhana | Boros ruang bila data bervariasi |
| Binary + header | Magic number + metadata + binary payload | Efisien, mendukung versi | Tidak bisa dibaca manusia, butuh parser biner |
Tidak ada format “terbaik”—pilihan tergantung pada kebutuhan. File konfigurasi cenderung teks (CSV/JSON/INI). File database cenderung biner dengan header. File log sering fixed-width atau delimiter-based.
Format CSV: Delimiter dengan Quoting #
CSV (Comma-Separated Values) adalah format teks paling populer untuk data tabular. RFC 4180 mendefinisikan aturan dasar: field dipisahkan koma, record dipisahkan newline, field yang mengandung koma/double-quote/newline dibungkus dengan double-quote ("...").
#include <stdio.h>
#include <string.h>
int main(void) {
printf("=== Format CSV (delimiter koma) ===\n");
FILE *f = fopen("/tmp/test_format.csv", "w");
if (!f) return 1;
fprintf(f, "id,nama,umur,kota\n");
fprintf(f, "1,Budi,20,Jakarta\n");
fprintf(f, "2,Siti,21,Bandung\n");
/* Field dengan koma harus di-quote */
fprintf(f, "3,\"Dono Saputra\",22,Surabaya\n");
fclose(f);
f = fopen("/tmp/test_format.csv", "r");
if (!f) return 1;
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
printf(" %s", line);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
=== Format CSV (delimiter koma) ===
id,nama,umur,kota
1,Budi,20,Jakarta
2,Siti,21,Bandung
3,"Dono Saputra",22,SurabayaBaris keempat di output (3,"Dono Saputra",22,Surabaya) menunjukkan bahwa field "Dono Saputra" tidak mengandung koma, tetapi kita tetap membungkusnya dengan double-quote—ini konvensi yang baik bila kita tidak yakin apakah nama mengandung koma atau tidak. Pada implementasi nyata, kita perlu parser CSV yang menangani escape double-quote ("" untuk literal ").
Format Fixed-Width: Kolom Tetap #
Pada format fixed-width, setiap field menempati jumlah byte yang sama. Kita pakai format specifier printf seperti %-20s (rata kiri 20 karakter) atau %04d (angka 4 digit dengan leading zero). Format ini umum di sistem mainframe dan laporan keuangan.
#include <stdio.h>
int main(void) {
printf("=== Format Fixed-Width (kolom tetap) ===\n");
FILE *f = fopen("/tmp/test_format_fixed.txt", "w");
if (!f) return 1;
fprintf(f, "%-4s %-20s %3s %3s\n", "ID", "NAMA", "UMUR", "NILAI");
fprintf(f, "%-4s %-20s %3d %3d\n", "001", "Budi Santoso", 20, 85);
fprintf(f, "%-4s %-20s %3d %3d\n", "002", "Siti Aminah", 21, 92);
fprintf(f, "%-4s %-20s %3d %3d\n", "003", "Andi", 19, 78);
fclose(f);
f = fopen("/tmp/test_format_fixed.txt", "r");
if (!f) return 1;
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
printf(" %s", line);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
=== Format Fixed-Width (kolom tetap) ===
ID NAMA UMUR NILAI
001 Budi Santoso 20 85
002 Siti Aminah 21 92
003 Andi 19 78Pada output di atas, kolom ID selalu 4 karakter (rata kiri), NAMA 20 karakter, UMUR 3 karakter (rata kanan), dan NILAI 3 karakter. Bila kita membaca baris ke-2 dengan fgets, kita bisa langsung mengakses ID sebagai substring line[0..3], NAMA sebagai line[5..24], dan seterusnya—tanpa parser kompleks.
Mari kita telaah baris-baris kode di atas:
- Baris 6 (
fprintf(f, "%-4s %-20s %3s %3s\n", ...)): specifiers%-4sberarti string rata kiri dalam 4 karakter.%3dberarti angka rata kanan dalam 3 karakter. Spasi di antara specifier adalah pemisah visual (bukan data). - Baris 7 (
fprintf(f, "%-4s %-20s %3d %3d\n", "001", "Budi Santoso", 20, 85);): menulis satu record."001"di-rata-kiri-kanan menjadi"001 "(4 karakter)."Budi Santoso"menjadi 20 karakter dengan spasi tambahan. - Baris 13-15 (
fgets+printf): membaca baris dan mencetak ulang. Karena data sudah ter-format dengan rapi, output yang dibaca sama persis dengan yang ditulis.
Format Biner dengan Header #
Untuk file yang efisien dan terdokumentasi, kita pakai pendekatan header + body. Header berisi:
- Magic number — 4-8 byte identifikasi format (misalnya
"MHS1"untuk “Mahasiswa versi 1”). - Version — angka versi untuk kompatibilitas naik.
- Count — jumlah record atau metadata lain.
Setelah header, body berisi data biner (array struct).
#include <stdio.h>
#include <stdint.h>
typedef struct {
uint32_t id;
char nama[20];
uint16_t nilai;
} Record;
int main(void) {
printf("=== Format dengan Header ===\n");
FILE *f = fopen("/tmp/test_format_hdr.dat", "wb");
if (!f) return 1;
/* Header: magic (4 byte) + version (2 byte) + count (4 byte) */
fwrite("MHS1", 1, 4, f);
uint16_t version = 1;
fwrite(&version, sizeof(version), 1, f);
uint32_t count = 3;
fwrite(&count, sizeof(count), 1, f);
/* Body: array of Record */
Record recs[] = {
{1, "Budi", 85},
{2, "Siti", 92},
{3, "Andi", 78},
};
fwrite(recs, sizeof(Record), 3, f);
fclose(f);
/* Baca kembali dan verifikasi */
f = fopen("/tmp/test_format_hdr.dat", "rb");
if (!f) return 1;
char magic[5] = {0};
fread(magic, 1, 4, f);
uint16_t ver;
fread(&ver, sizeof(ver), 1, f);
uint32_t cnt;
fread(&cnt, sizeof(cnt), 1, f);
printf(" magic=%s version=%u count=%u\n", magic, ver, cnt);
for (uint32_t i = 0; i < cnt; i++) {
Record r;
fread(&r, sizeof(Record), 1, f);
printf(" -> id=%u nama=%s nilai=%u\n", r.id, r.nama, r.nilai);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
=== Format dengan Header ===
magic=MHS1 version=1 count=3
-> id=1 nama=Budi nilai=85
-> id=2 nama=Siti nilai=92
-> id=3 nama=Andi nilai=78Saat membaca file, kita periksa magic number terlebih dahulu. Bila magic != "MHS1", file bukan format yang kita harapkan—kita bisa menolak dengan pesan error yang jelas. Selanjutnya kita baca version untuk menentukan layout body. Bila version > 1, kita bisa rute ke parser versi yang berbeda—ini adalah dasar backward compatibility.
Pola Magic Number dan Versioning #
Beberapa contoh magic number yang terkenal di dunia nyata:
| Format | Magic | Hex |
|---|---|---|
| ELF executable | \x7fELF | 7F 45 4C 46 |
| PNG image | \x89PNG\r\n\x1a\n | 89 50 4E 47 0D 0A 1A 0A |
| GIF image | GIF87a / GIF89a | 47 49 46 38 37 61 |
| PDF document | %PDF- | 25 50 44 46 2D |
| ZIP archive | PK\x03\x04 | 50 4B 03 04 |
| Java class | \xCA\xFE\xBA\xBE | CA FE BA BE |
Konvensi yang baik: pakai 4-8 byte yang tidak mungkin muncul di awal file teks biasa. Magic number PNG bahkan sengaja memasukkan \r\n\x1a\n untuk mendeteksi transfer yang merusak newline (line-ending conversion) dan Ctrl-Z EOF (file transfer dari Windows).
Validasi Format Saat Membaca #
Saat membaca file dengan header, selalu validasi:
- Magic number — bila tidak cocok, file bukan format yang diharapkan.
- Version — bila lebih tinggi dari yang didukung, tolak dengan pesan jelas.
- Count — bila jumlah record tidak masuk akal (misalnya negatif atau sangat besar), tolak sebelum membaca.
- Ukuran file — bandingkan
count * sizeof(record)dengan ukuran file aktual (fseek+ftell).
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef struct {
uint32_t id;
char nama[20];
uint16_t nilai;
} Record;
int main(void) {
FILE *f = fopen("/tmp/test_format_hdr.dat", "rb");
if (!f) { perror("fopen"); return 1; }
/* 1. Validasi magic number */
char magic[5] = {0};
if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "MHS1", 4) != 0) {
fprintf(stderr, "Magic number tidak cocok (dapat '%s')\n", magic);
fclose(f);
return 1;
}
/* 2. Cek version */
uint16_t ver;
fread(&ver, sizeof(ver), 1, f);
if (ver != 1) {
fprintf(stderr, "Versi %u tidak didukung (hanya versi 1)\n", ver);
fclose(f);
return 1;
}
/* 3. Cek count dengan ukuran file */
uint32_t cnt;
fread(&cnt, sizeof(cnt), 1, f);
fseek(f, 0L, SEEK_END);
long size = ftell(f);
long expected = 10L + (long)cnt * sizeof(Record); /* header 10 byte */
if (size != expected) {
fprintf(stderr, "Ukuran file %ld tidak sesuai (expected %ld)\n",
size, expected);
fclose(f);
return 1;
}
fseek(f, 10L, SEEK_SET); /* kembali ke posisi setelah header */
printf("File valid. Membaca %u record...\n", cnt);
for (uint32_t i = 0; i < cnt; i++) {
Record r;
fread(&r, sizeof(Record), 1, f);
printf(" [%u] id=%u nama=%s nilai=%u\n", i, r.id, r.nama, r.nilai);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
File valid. Membaca 3 record...
[0] id=1 nama=Budi nilai=85
[1] id=2 nama=Siti nilai=92
[2] id=3 nama=Andi nilai=78