Di bagian 1, MesinCatatan mengelola datanya dengan rapi, tetapi datanya hidup hanya selama proses berjalan: begitu program berhenti, seluruh catatan kembali kosong. Aplikasi catatan yang berguna harus mengingat isi pekerjaan pengguna antar sesi, dan jawaban klasiknya adalah file. Bagian ini menambahkan dua fungsi bebas — simpanKeFile() dan muatDariFile() — yang menyalin isi mesin ke file teks dan sebaliknya, tanpa library eksternal sama sekali.
Format yang kita pilih adalah teks polos dengan aturan satu catatan per blok: setiap catatan ditulis sebagai satu blok pendek, dan blok-blok itu dipisah baris pemisah. Format seperti ini punya dua kelebihan untuk tahap belajar. Kita bisa membukanya di editor mana pun dan memahami isinya tanpa alat bantu, dan seluruh kode penulis-pembacanya cukup memakai getline serta ofstream yang sudah kita pelajari di Section Input/Output dan File. Format JSON memang lebih kuat, tetapi mekanismenya tersembunyi di balik library — sedangkan di sini setiap byte bisa kita jelaskan.
Format File Satu Catatan per Blok #
Aturannya tegas dan pendek. Baris [catatan N] membuka blok sekaligus menyimpan id catatan. Baris tepat setelahnya adalah judul, dan judul selalu satu baris. Baris-baris setelah judul sampai baris pemisah --- adalah isi, yang boleh kosong atau beberapa baris. Hasil simpanKeFile() dari skenario demo bagian ini berbentuk:
[catatan 1]
Belajar C++
Engine catatan sudah jalan tanpa GUI.
---
[catatan 2]
Daftar belanja
Telur
Beras
Kopi
---
[catatan 3]
Ide proyek
Catatan bertahan sampai sesi berikutnya.
---Perhatikan bahwa id ikut tersimpan. Identitas catatan tidak berubah antar sesi: catatan yang menyandang id 2 hari ini tetap id 2 besok, sehingga antarmuka pada bagian 4 masih bisa menyasar catatan yang sama lewat id yang itu. Pemisah ditulis di akhir setiap blok — termasuk blok terakhir — sehingga pembaca tidak pernah memerlukan kasus khusus.
Program Lengkap Bagian Ini #
File persistensi.cpp berdiri sendiri: engine dari bagian 1 dibawa utuh, ditambah satu method tambahDenganId() untuk keperluan pemuatan, lalu dua fungsi bebas untuk simpan dan muat:
// persistensi.cpp — mesin catatan + persistensi ke file teks.
// Format file: satu catatan per blok, setiap blok ditutup baris pemisah
// "---". File ini mandiri: simpan dan muat diuji lewat skenario deterministik.
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
namespace {
// Satu catatan: id tetap sepanjang usia catatan, judul satu baris,
// isi berupa teks bebas yang boleh beberapa baris.
struct Catatan {
int id = 0;
std::string judul;
std::string isi;
};
class MesinCatatan {
public:
// Menambah catatan baru; id diisi otomatis mulai dari 1 dan naik terus.
int tambah(const std::string& judul, const std::string& isi) {
Catatan c;
c.id = idBerikutnya;
++idBerikutnya;
c.judul = judul;
c.isi = isi;
daftar_.push_back(c);
return c.id;
}
// Menambah catatan dengan id tertentu — dipakai pemuatan file agar id
// yang tersimpan tidak berubah. idBerikutnya dijaga tetap di atasnya.
void tambahDenganId(int id, const std::string& judul,
const std::string& isi) {
Catatan c;
c.id = id;
c.judul = judul;
c.isi = isi;
daftar_.push_back(c);
if (id >= idBerikutnya) {
idBerikutnya = id + 1;
}
}
// Mengubah judul dan isi catatan milik id tertentu.
// Mengembalikan false bila id tidak ditemukan.
bool ubah(int id, const std::string& judulBaru, const std::string& isiBaru) {
Catatan* c = cariId(id);
if (c == nullptr) {
return false;
}
c->judul = judulBaru;
c->isi = isiBaru;
return true;
}
// Menghapus catatan milik id tertentu dari daftar.
// Mengembalikan false bila id tidak ditemukan.
bool hapus(int id) {
for (std::size_t i = 0; i < daftar_.size(); ++i) {
if (daftar_[i].id == id) {
daftar_.erase(daftar_.begin() + i);
return true;
}
}
return false;
}
// Jumlah catatan yang sedang tersimpan di mesin.
int jumlah() const {
return static_cast<int>(daftar_.size());
}
// Akses baca catatan pada posisi ke-i (0-based), mengikuti urutan daftar.
const Catatan& pada(int i) const {
return daftar_[static_cast<std::size_t>(i)];
}
private:
// Alamat catatan milik id tertentu, atau nullptr bila tidak ada.
Catatan* cariId(int id) {
for (Catatan& c : daftar_) {
if (c.id == id) {
return &c;
}
}
return nullptr;
}
std::vector<Catatan> daftar_;
int idBerikutnya = 1;
};
const std::string PEMISAH = "---";
// Menyimpan seluruh catatan ke file: setiap catatan menjadi satu blok
// "[catatan <id>]", judul, isi, lalu pemisah.
// Mengembalikan false bila file tidak bisa dibuat.
bool simpanKeFile(const MesinCatatan& mesin, const std::string& namaFile) {
std::ofstream keluar(namaFile);
if (!keluar) {
return false;
}
for (int i = 0; i < mesin.jumlah(); ++i) {
const Catatan& c = mesin.pada(i);
keluar << "[catatan " << c.id << "]\n"
<< c.judul << "\n"
<< c.isi << "\n"
<< PEMISAH << "\n";
}
return true;
}
// Memuat catatan dari file ke mesin. Mengembalikan false bila file tidak
// bisa dibuka. Blok dibaca berurutan: baris "[catatan N]" membuka blok,
// baris "---" menutupnya; baris setelah judul sampai pemisah adalah isi.
bool muatDariFile(MesinCatatan& mesin, const std::string& namaFile) {
std::ifstream masuk(namaFile);
if (!masuk) {
return false;
}
const std::string awalan = "[catatan ";
std::string baris;
while (std::getline(masuk, baris)) {
if (!baris.starts_with(awalan) || baris.back() != ']') {
continue; // baris di luar blok: pemisah atau baris kosong
}
const int id = std::stoi(
baris.substr(awalan.size(), baris.size() - awalan.size() - 1));
std::string judul;
std::getline(masuk, judul); // baris tepat setelah header: judul
std::string isi;
while (std::getline(masuk, baris) && baris != PEMISAH) {
if (!isi.empty()) {
isi += "\n";
}
isi += baris;
}
mesin.tambahDenganId(id, judul, isi);
}
return true;
}
} // namespace
int main() {
const std::string namaFile = "catatan-demo.txt";
// 1) Mengisi engine lalu menyimpan seluruhnya ke file.
MesinCatatan mesin;
mesin.tambah("Belajar C++", "Engine catatan sudah jalan tanpa GUI.");
mesin.tambah("Daftar belanja", "Telur\nBeras\nKopi");
mesin.tambah("Ide proyek", "Catatan bertahan sampai sesi berikutnya.");
std::cout << "simpan: " << mesin.jumlah() << " catatan -> "
<< namaFile << ": "
<< (simpanKeFile(mesin, namaFile) ? "berhasil" : "gagal")
<< "\n";
// 2) Engine kedua yang benar-benar kosong, lalu diisi dari file.
MesinCatatan mesinKedua;
std::cout << "muat: "
<< (muatDariFile(mesinKedua, namaFile) ? "berhasil" : "gagal")
<< " dari " << namaFile << "\n";
for (int i = 0; i < mesinKedua.jumlah(); ++i) {
const Catatan& c = mesinKedua.pada(i);
std::cout << "[" << c.id << "] " << c.judul << "\n";
// Isi boleh beberapa baris: tiap barisnya dicetak menjorok.
std::istringstream barisIsi(c.isi);
for (std::string potongan; std::getline(barisIsi, potongan);) {
std::cout << " " << potongan << "\n";
}
}
// 3) id berlanjut dari file: catatan baru tidak menabrak id lama.
std::cout << "tambah setelah muat: id "
<< mesinKedua.tambah("Catatan setelah muat", "id lanjutan.")
<< "\n";
// 4) File hilang: muat gagal dengan jelas, engine tetap terpakai.
MesinCatatan mesinKetiga;
std::cout << "muat file-tidak-ada.txt: "
<< (muatDariFile(mesinKetiga, "file-tidak-ada.txt")
? "berhasil"
: "gagal dibuka")
<< "\n";
std::cout << "engine ketiga tetap jalan: " << mesinKetiga.jumlah()
<< " catatan\n";
return 0;
}Output yang Dihasilkan:
simpan: 3 catatan -> catatan-demo.txt: berhasil
muat: berhasil dari catatan-demo.txt
[1] Belajar C++
Engine catatan sudah jalan tanpa GUI.
[2] Daftar belanja
Telur
Beras
Kopi
[3] Ide proyek
Catatan bertahan sampai sesi berikutnya.
tambah setelah muat: id 4
muat file-tidak-ada.txt: gagal dibuka
engine ketiga tetap jalan: 0 catatanBedah Simpan dan Muat #
Mari kita telaah bagian-bagian pentingnya:
simpanKeFile(): membukastd::ofstream; bila file tidak bisa dibuat, fungsi langsung mengembalikanfalse. Setiap catatan ditulis sebagai empat tahap — header, judul, isi, pemisah — sehingga bentuk file selalu seragam.tambahDenganId(): memasukkan catatan dengan id tertentu, lalu menjagaidBerikutnyatetap berada di atas id terbesar. Tanpa method ini, pemuatan harus mengubah id catatan dan identitas antar sesi ikut bergeser.muatDariFile(): membukastd::ifstream; bila file tidak bisa dibuka, fungsi mengembalikanfalse— inilah jalur penanganan file hilang.starts_with(awalan): APIstd::stringdari C++20; baris yang tidak diawali[catatandilewati, sehingga pemisah sisa dan baris kosong tidak mengganggu.substr+stoi: mengambil teks di antara[catatandan], lalu mengubahnya menjadi angka id.- Pengumpulan isi:
getlinemenampung baris demi baris sampai pemisah ditemukan, disambung dengan\n— isi beberapa baris kembali utuh seperti aslinya. - Skenario
main(): menyimpan tiga catatan, memuatnya ke engine kedua yang benar-benar kosong, mencetak hasilnya, menambah catatan dengan id 4, lalu mencoba memuat file yang tidak ada.
Saat File Tidak Ada #
Jalur file hilang diperlakukan sebagai kejadian normal, bukan galat yang menghentikan program. muatDariFile() mengembalikan false, dan main() tetap memakai engine ketiganya — jumlahnya memang nol, tetapi mesin siap menerima catatan baru. Aplikasi GUI kelak menempatkan keputusannya di lapisan antarmuka: menampilkan pesan bantuan, menawarkan memilih lokasi lain, atau sekadar mulai dengan daftar kosong. Mesin hanya melaporkan hasilnya dengan jujur.
Dengan catatan yang bertahan antar sesi, fondasi data sudah lengkap. Yang belum ada adalah cara menemukan catatan kembali di antara banyaknya catatan — dan jalur resmi bagaimana antarmuka menyuruh mesin bekerja. Bagian 3 menambahkan keduanya: pencarian substring dan antrean kejadian.
Rujukan resmi: cppreference — std::basic_ofstream , cppreference — std::string::starts_with .