Sepanjang section ini kita sudah lancar menulis dan membaca file, tetapi selalu pada level teks dan byte mentah. Pertanyaan yang muncul begitu program sungguhan dibangun: bagaimana menyimpan kumpulan objek kita — misalnya std::vector berisi struct hasil input pengguna — ke sebuah file, lalu mengembalikannya persis seperti semula pada eksekusi berikutnya? Proses mengubah objek di memori menjadi deretan byte yang bisa disimpan atau dikirim itulah yang disebut serialization, dan proses kebalikannya deserialization. Chapter ini membangun fondasinya dengan format teks berlabel yang sederhana, mengujinya dengan uji round-trip — simpan, muat, bandingkan — lalu memetakan jalan ke format biner dan library eksternal bila data kita membesar.
Serialisasi adalah Konversi, Bukan Salinan #
Hal pertama yang perlu lurus: menyimpan objek bukan menyalin blok memorinya ke disk. Memori program berisi alamat pointer, padding antar member, dan representasi mesin — hal-hal yang tidak bermakna bagi file yang harus dibuka besok, apalagi di mesin lain. Chapter Membaca dan Menulis File Biner sudah menunjukkan risikonya: reinterpret_cast atas struct yang mengandung std::string atau pointer menghasilkan file yang tidak sah dan tidak portabel.
Serialisasi yang benar adalah konversi yang disepakati: setiap field diubah menjadi bentuk yang punya aturan baca-tulis jelas, dan formatnya kita tentukan sendiri. Untuk tahap belajar, format teks adalah pilihan terbaik: bisa diperiksa manusia, bisa diedit, dan tidak terikat representasi memori. Keputusan desain yang perlu dibuat cuma dua — bagaimana satu record (satu objek) ditulis, dan bagaimana record-record dipisahkan.
Format Teks Berlabel untuk Struct #
Kita memakai format baris berlabel: setiap field ditulis sebagai kunci=nilai dalam satu baris, dan record ditutup baris ---. Format ini tahan terhadap spasi di dalam data — nama “Budi Pratama” tetap satu baris utuh, sesuatu yang menyulitkan bila kita memakai koma sebagai pemisah. Perhatikan bahwa struct Siswa di bawah adalah struct biasa dari Chapter Struct; yang baru hanyalah pasangan fungsi simpanRecord dan muatRecord:
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
struct Siswa {
std::string nama;
int umur = 0;
double nilai = 0.0;
};
// Serialisasi satu record: tiga baris berlabel plus penanda akhir record.
void simpanRecord(std::ostream& keluar, const Siswa& s) {
keluar << "nama=" << s.nama << '\n';
keluar << "umur=" << s.umur << '\n';
keluar << "nilai=" << s.nilai << '\n';
keluar << "---\n";
}
// Deserialisasi satu record; kembalikan true bila ada record terbaca.
bool muatRecord(std::istream& masuk, Siswa& s) {
s = {};
bool ada = false;
std::string baris;
while (std::getline(masuk, baris)) {
if (baris == "---") {
return ada;
}
auto posisi = baris.find('=');
if (posisi == std::string::npos) {
continue; // baris kosong/tak dikenal: lewati
}
std::string kunci = baris.substr(0, posisi);
std::string nilai = baris.substr(posisi + 1);
if (kunci == "nama") {
s.nama = nilai;
ada = true;
} else if (kunci == "umur") {
s.umur = std::stoi(nilai);
ada = true;
} else if (kunci == "nilai") {
s.nilai = std::stod(nilai);
ada = true;
}
}
return ada; // akhir file tanpa '---': record terakhir tetap sah
}
int main() {
std::vector<Siswa> kelas = {
{"Budi Pratama", 17, 88.5},
{"Siti Rahayu", 16, 92.0},
{"Dewi Lestari", 17, 79.25},
};
// 1. Serialisasi: vector<Siswa> -> file teks
{
std::ofstream keluar("kelas.txt");
if (!keluar) {
std::cerr << "Gagal membuka kelas.txt untuk ditulis\n";
return 1;
}
for (const Siswa& s : kelas) {
simpanRecord(keluar, s);
}
}
std::cout << "Disimpan: " << kelas.size()
<< " record ke 'kelas.txt'\n";
// 2. Tunjukkan wujud formatnya
std::cout << "Isi 'kelas.txt':\n";
std::ifstream lihat("kelas.txt");
std::string baris;
while (std::getline(lihat, baris)) {
std::cout << " | " << baris << '\n';
}
// 3. Deserialisasi: file teks -> vector<Siswa>
std::vector<Siswa> hasil;
std::ifstream masuk("kelas.txt");
if (!masuk) {
std::cerr << "Gagal membuka kelas.txt untuk dibaca\n";
return 1;
}
Siswa s;
while (muatRecord(masuk, s)) {
hasil.push_back(s);
}
std::cout << "Dibaca : " << hasil.size()
<< " record dari 'kelas.txt'\n";
// 4. Uji round-trip: bandingkan dengan data asal
std::cout << std::boolalpha;
bool identik = (hasil.size() == kelas.size());
for (std::size_t i = 0; identik && i < kelas.size(); ++i) {
identik = kelas[i].nama == hasil[i].nama &&
kelas[i].umur == hasil[i].umur &&
kelas[i].nilai == hasil[i].nilai;
}
for (const Siswa& t : hasil) {
std::cout << " " << t.nama << " | " << t.umur << " | "
<< t.nilai << '\n';
}
std::cout << "Round-trip identik: " << identik << '\n';
return 0;
}Output yang Dihasilkan:
Disimpan: 3 record ke 'kelas.txt'
Isi 'kelas.txt':
| nama=Budi Pratama
| umur=17
| nilai=88.5
| ---
| nama=Siti Rahayu
| umur=16
| nilai=92
| ---
| nama=Dewi Lestari
| umur=17
| nilai=79.25
| ---
Dibaca : 3 record dari 'kelas.txt'
Budi Pratama | 17 | 88.5
Siti Rahayu | 16 | 92
Dewi Lestari | 17 | 79.25
Round-trip identik: true- Baris 13-18 (
simpanRecord): satu objek menjadi empat baris. Bentukstd::ostream&sebagai parameter membuat fungsi ini bisa dipakai untuk file maupunostringstream— pemisahan antara “cara menyusun teks” dan “ke mana teksnya pergi”. - Baris 21-47 (
muatRecord): kebalikannya, per baris. Baris---mengakhiri record; baris tanpa=dilewati supaya file yang diberi komentar atau baris kosong tidak merusak pembacaan;find('=')dipilih bukangetline(masuk, x, '=')agar pemisahan kunci-nilai tetap berlaku per baris. - Baris 39 dan 42 (
stoi,stod): teks dikembalikan menjadi angka. Dua fungsi ini menerimastd::stringdan melaporkan kegagalan lewat exception — untuk file hasil tangan sendiri cukup begitu; untuk input dari luar, pasangkan dengan pemeriksaan seperti pada Chapter Input Validation. - Baris 50-54 (data uji): nilai
doubleyang dipilih (88.5, 92.0, 79.25) semuanya persis terwakili dalam biner floating-point, sehingga perbandingan==setelah round-trip sah untuk contoh ini. - Baris 57-68 (tulis):
ofstreammode bawaan (trunc) berarti menjalankan program lagi menimpa “file simpanan” dari awal — perilaku yang justru kita mau untuk format yang selalu ditulis ulang lengkap. - Baris 85-88 (muat):
muatRecorddipanggil berulang sampaifalse; setiap keberhasilan menambah satu objek kehasil. Struktur loop ini yang nanti juga dipakai saat membaca CSV. - Baris 92-104 (uji round-trip): jumlah dan ketiga field dibandingkan satu per satu.
Round-trip identik: trueadalah bukti bahwa simpan-muat tidak mengubah apa pun — kriteria minimum setiap format serialisasi.
Batas Format Sederhana dan Jalan Berikutnya #
Format berlabel di atas jujur tentang batasnya: tanpa skema, tanpa kompresi, dan std::stod menolak tulisan angka yang tidak sah. Untuk data internal program kecil, itu semua masih terjangkau sendiri. Saat kebutuhan naik kelas, ada dua arah yang biasa ditempuh, dan keduanya penting kita kenal walau belum dipraktikkan di lingkungan latihan ini:
- Format biner terdefinisi: seperti dibahas pada Chapter Membaca dan Menulis File Biner — hemat dan cepat, tetapi menuntut disiplin tipe berlebar tetap (
std::int32_tdan temannya) dan aturan urutan byte. Kombinasi lazim di industri: teks untuk konfigurasi dan pertukaran, biner untuk data besar internal. - Library eksternal: cereal melakukan serialisasi objek C++ (termasuk container standar) dengan sedikit kode tambahan, sementara Protocol Buffers (protobuf) membuat kita mendefinisikan skema data di file tersendiri lalu menghasilkan kode baca-tulisnya secara otomatis. JSON juga sangat umum sebagai format pertukaran antar aplikasi. Keduanya layak dicari saat proyek Anda serius — dokumentasi resminya masing-masing adalah titik mulai terbaik.
Dengan serialisasi, lingkaran I/O kita tertutup penuh: data lahir dari input pengguna, hidup di container, tidur di file, dan bangun kembali persis seperti semula. Keterampilan memilah baris menjadi field yang dipakai muatRecord di sini adalah persis bekal yang dibutuhkan pada Proyek: Analisis Data CSV — di sana record dipisahkan koma, field-nya beragam, dan volumenya jauh lebih besar.