Section ini bergerak dari alat yang paling manual ke yang paling otomatis: debugger, compiler warnings, sanitizer, static analysis, clang-format, clang-tidy, hingga unit dan integration test. Kini saatnya menyatukan semuanya pada kelas masalah yang paling melekat dengan bahasa C++: error memori. Lima error — memory leak, buffer overflow, use-after-free, double-free, dan uninitialized read — adalah biang kerok runtuhnya program C++ di produksi, dan hampir semuanya tidak terlihat saat program dijalankan biasa. Chapter ini menjadi katalog lengkapnya: kapan tiap error muncul, alat mana yang menangkapnya, laporan deteksi yang belum kita lihat, lalu satu pola pencegahan yang membuat kelas error ini nyaris tidak bisa lahir. Setelahnya, section debugging dan code quality kita tutup.
Katalog Lima Error Memori Klasik #
Tabel ini merangkum seluruh peta yang sudah kita jelajahi, ditambah satu-dua yang baru:
| Error | Pola kode pemicu | Gejala tanpa alat | Alat deteksi |
|---|---|---|---|
| memory leak | alokasi tanpa pasangan pelepasan | RAM naik perlahan, proses dihentikan sistem | LeakSanitizer (bawaan ASan), valgrind |
| heap-buffer-overflow | indeks di luar batas array heap | tak terlihat; merusak data tetangga acak | ASan |
| use-after-free | akses setelah delete | jalan mulus di dev, runtuh acak di produksi | ASan |
| double-free | delete dua kali pada blok yang sama | heap tercemar, crash dini | ASan |
| uninitialized read | membaca variable yang belum diisi | hasil tak tentu, perilaku “kadang jalan kadang tidak” | warning compiler, valgrind |
Empat baris pertama sudah pernah kita lihat laporannya lengkap pada chapter AddressSanitizer — termasuk protokol membacanya baris demi baris. Dua yang belum kita lihat bentuk aslinya adalah laporan kebocoran dan pembacaan tak terinisialisasi, dan itulah bahan dua bagian berikutnya.
Memory Leak: Laporan LeakSanitizer #
Leak adalah error memori yang paling sabar: program berjalan dengan benar, hasilnya benar, hanya saja setiap alokasi yang terlepas dari penguasaannya menjadi memori yang tak akan pernah kembali sampai proses berakhir. Berikut polanya dalam bentuk terkecil:
#include <iostream>
int main() {
int* log = new int[100]{0};
std::cout << "log dibuat: " << log[0] << "\n";
return 0; // lupa delete[] log
}Dikompilasi dengan build sanitizer (g++ -std=c++23 -g -Wall -Wextra -fsanitize=address demo-leak.cpp -o demo-leak), program ini berjalan normal dan mencetak hasilnya — tetapi saat berakhir, LeakSanitizer (LSan), yang aktif otomatis di dalam ASan, memeriksa heap dan melaporkan blok yang masih tertahan. Laporan berikut adalah hasil nyata dari g++ 14.2 di sandbox penulis; alamat dan nomor proses pasti berbeda di mesin Anda:
=================================================================
==1157==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 0x7f73b1ec88d8 in operator new[](unsigned long)
#1 0x55c90a1c71da in main demo-leak.cpp:4
SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).Cara membacanya: Direct leak of 400 byte(s) berarti blok 100 × 4 byte yang hilang langsung — tak ada lagi pointer aktif yang menunjuknya, sehingga mustahil dilepas oleh siapa pun (sebaliknya, indirect leak adalah blok yang hanya bisa dicapai lewat blok lain yang juga bocor). Frame #1 menunjuk baris alokasinya, sehingga perbaikan tinggal satu langkah: pasangkan delete[] log;, atau lebih baik lagi, ikuti pola pencegahan di bawah. Dalam program server yang berjalan berminggu-minggu, kebocoran sekecil ini terakumulasi menjadi kejadian paling klasik: layanan pelan-pelan kehabisan memori dan dihentikan sistem, tanpa satu pun exception di log.
Uninitialized Read: Kelas yang Berbeda #
Empat error pertama melanggar aturan siapa boleh menyentuh memori mana. Error kelima berbeda: memorinya sah, tetapi isinya belum pernah kita tetapkan. Membaca variable yang belum diisi adalah undefined behavior — nilainya bisa apa saja, dan compiler tidak wajib melakukan apa pun yang masuk akal:
#include <iostream>
int main() {
int hasil; // dideklarasikan tanpa nilai awal
if (hasil > 0) { // membaca variable yang belum diisi — UB
std::cout << "positif\n";
}
return 0;
}Kasus sesederhana ini sudah tertangkap compiler: g++ 14.2 dengan -Wall -Wextra menandai baris 5 dengan warning: 'hasil' is used uninitialized [-Wuninitialized] — peringatan yang kita pelajari polanya pada chapter Compiler Warnings. Untuk kasus yang lebih rumit (nilai menyebar antar function, misalnya), pemeriksa runtime datang membantu. Valgrind tidak terinstal di sandbox penulis, sehingga tampilan berikut adalah ilustrasi format laporannya:
$ valgrind ./baca-nilai
==31821== Conditional jump or move depends on uninitialised value(s)
==31821== at 0x10917F: main (baca-nilai.cpp:5)
==31821==
==31821== ERROR SUMMARY: 1 errors from 1 contextsBaris pertamanya yang khas: keputusan cabang (if (hasil > 0)) bergantung pada nilai yang tidak pasti. Perlu dicatat bahwa ASan tidak menangkap kelas ini — aksesnya legal menurut peta memorinya — sehingga pertahanannya berbeda: warning compiler sebagai lapisan pertama, valgrind (atau MemorySanitizer pada Clang) sebagai lapisan runtime. Benteng paling sederhana tetap yang paling tua: beri nilai awal saat deklarasi (int hasil = 0;) dan kebiasaan ini tidak pernah sempat tumbuh.
Pencegahan: RAII sebagai Fondasi #
Seluruh katalog di atas punya satu akar yang sama: alokasi dan pelepasan yang ditangani dua tangan berbeda. RAII (Resource Acquisition Is Initialization) membalik tata kelanya — setiap sumber daya dimiliki objek yang mengambilnya di constructor dan melepaskannya di destructor, sehingga pelepasan mengikuti scope secara otomatis, bahkan saat exception melewati jalan tengah. std::vector, std::string, std::unique_ptr, dan std::lock_guard yang kita pakai sepanjang course ini semuanya bekerja dengan pola ini. Program berikut membuktikannya di jalur yang paling sering bocor: jalur exception.
// raii-selamat.cpp — pola RAII membuat kelima error memori sulit terjadi:
// tidak ada new/delete manual, pelepasan mengikuti scope secara otomatis.
#include <iostream>
#include <stdexcept>
#include <vector>
class PemrosesData {
public:
PemrosesData() {
data_.resize(1000);
std::cout << "constructor: buffer 1000 int siap\n";
}
~PemrosesData() {
std::cout << "destructor: buffer dilepas otomatis\n";
}
void proses(bool gagalDiTengah) {
for (std::size_t i = 0; i < data_.size(); ++i) {
data_[i] = static_cast<int>(i);
}
if (gagalDiTengah) {
throw std::runtime_error("input tidak valid");
}
}
private:
std::vector<int> data_;
};
int main() {
try {
PemrosesData pemroses;
pemroses.proses(true);
std::cout << "baris ini tidak pernah tercapai\n";
} catch (const std::exception& e) {
std::cout << "tertangkap: " << e.what() << "\n";
}
std::cout << "program berlanjut tanpa kebocoran\n";
return 0;
}Output yang Dihasilkan:
constructor: buffer 1000 int siap
destructor: buffer dilepas otomatis
tertangkap: input tidak valid
program berlanjut tanpa kebocoranPerhatikan urutan output — ia adalah bukti mekanisme RAII di jalur exception:
- Baris 1 — constructor
PemrosesDatamengisidata_hingga 1000 elemen; kepemilikan buffer melekat pada objekpemroses, bukan pada kode yang berantakan di luar. - Baris 2 sebelum baris 3 —
proses(true)melempar exception, dan sebelum blokcatchsempat berjalan, proses stack unwinding memanggil destructor~PemrosesData. Itulah sebabnyadestructor: buffer dilepas otomatistercetak lebih dulu — pelepasan bukan pekerjaan kita lagi, tetapi pekerjaan bahasa. - Baris 4 — setelah
catch, program berlanjut sehat: tidak ada leak, tidak ada blok tertahan, dan jalur error tidak butuh penanganandeletekhusus.
Bandingkan dengan versi manual yang harus ditulis delete[] di tiga tempat (jalur sukses, jalur gagal, dan jalur exception) — dan satu tempat terlupakan langsung menjadi laporan LSan seperti di atas. Inilah ringkasan section Manajemen Memori yang perlu tertanam: di C++ modern, new dan delete manual nyaris tidak pernah ditulis; sumber daya diserahkan ke objek pemilik, dan error lima kelas di atas kehilangan pintu masuknya.
Checklist Menjaga Memori #
Bagian penutup kerja harian, dirangkum jadi enam kebiasaan yang bisa diperiksa setiap menulis kode:
- Berikan nilai awal pada setiap deklarasi (
int hasil = 0;,std::string nama;sudah aman bawaannya) — kelas uninitialized read punah sejak lahir. - Tidak ada
new/deletemanual di kode produksi:std::vector,std::string,std::unique_ptr, danstd::make_uniquemengambil alih seluruh pasangan alokasi-pelepasan. - Satu pemilik untuk setiap alokasi; kepemilikan bersama hanya dengan
std::shared_ptrbila benar-benar dibutuhkan, karena pemilik ganda berarti ambiguitas pelepasan. - Batas selalu dari container: ukuran dari
data.size(), akses berisiko lewat.at()— indeks tulis-tangan sepertidata[3]adalah pintu masuk buffer overflow. - Build test memakai
-fsanitize=address,undefined: LSan ikut menyala otomatis di akhir setiap eksekusi test, sehingga kebocoran tertangkap menit yang sama dengan lahirnya. - Peringatan compiler dituntaskan, terutama
-Wuninitializeddan-Wmaybe-uninitialized— kebiasaan ini murah dan menutup kelas yang tidak terjangkau sanitizer.
Penutup Section: Dari Alat Menjadi Kebiasaan #
Section Debugging dan Code Quality kini utuh, dan arsitekturnya bertingkat sengaja:
| Lapisan | Alat | Pertanyaan yang dijawab |
|---|---|---|
| Pengamatan langsung | debugger: breakpoints, watchpoints, stack trace | “apa isi variable di baris ini, saat ini?” |
| Peringatan dini | -Wall -Wextra -Wpedantic | “apakah ada pola yang nyaris pasti salah?” |
| Pengawasan runtime | ASan, UBSan, TSan | “apakah eksekusi ini melanggar aturan?” |
| Analisis & pola | -fanalyzer, clang-format, clang-tidy | “apakah struktur dan gayanya sehat?” |
| Bukti perilaku | unit test, integration test | “apakah hasilnya sesuai kontrak?” |
| Katalog & pencegahan | checklist RAII di chapter ini | “apakah kelas error ini bisa lahir lagi?” |
Tidak ada satu lapisan pun yang cukup sendirian — debugger menelusuri satu bug pada satu waktu, test membuktikan perilaku yang sudah tertulis, dan sanitizer mengawasi eksekusi yang benar-benar terjadi. Kekuatan sejatinya muncul dari urutannya: kode yang lolos lapisan awal (warning, format, linting) tiba di test dalam keadaan bersih, sehingga kegagalan test hampir selalu berarti masalah logika yang nyata, bukan noise.
Sampai titik ini semua contoh kita dikompilasi dengan satu perintah g++ — dan untuk program kecil itu sah. Proyek nyata membawa puluhan berkas, dependensi eksternal, flag berbeda untuk debug dan release, serta compile_commands.json yang sempat kita singgung di clang-tidy. Mengatur semua itu secara manual tidak bertahan; itulah pekerjaan build system. Section berikutnya, Build System dan Project Structure, memindahkan seluruh kebiasaan flag, target, dan pemeriksaan yang telah kita bangun menjadi konfigurasi yang bisa dijalankan satu perintah.
Referensi: Clang — AddressSanitizer , Clang — LeakSanitizer .