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:

ErrorPola kode pemicuGejala tanpa alatAlat deteksi
memory leakalokasi tanpa pasangan pelepasanRAM naik perlahan, proses dihentikan sistemLeakSanitizer (bawaan ASan), valgrind
heap-buffer-overflowindeks di luar batas array heaptak terlihat; merusak data tetangga acakASan
use-after-freeakses setelah deletejalan mulus di dev, runtuh acak di produksiASan
double-freedelete dua kali pada blok yang samaheap tercemar, crash diniASan
uninitialized readmembaca variable yang belum diisihasil 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:

cpp demo-leak.cpp — alokasi yang tidak pernah dilepas
#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:

text Contoh output LeakSanitizer — memory leak
=================================================================
==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:

cpp baca-nilai.cpp — membaca variable yang belum diisi
#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:

text Contoh output valgrind (ilustrasi)
$ 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 contexts

Baris 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.

cpp
// 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:

text
constructor: buffer 1000 int siap
destructor: buffer dilepas otomatis
tertangkap: input tidak valid
program berlanjut tanpa kebocoran

Perhatikan urutan output — ia adalah bukti mekanisme RAII di jalur exception:

  1. Baris 1 — constructor PemrosesData mengisi data_ hingga 1000 elemen; kepemilikan buffer melekat pada objek pemroses, bukan pada kode yang berantakan di luar.
  2. Baris 2 sebelum baris 3 — proses(true) melempar exception, dan sebelum blok catch sempat berjalan, proses stack unwinding memanggil destructor ~PemrosesData. Itulah sebabnya destructor: buffer dilepas otomatis tercetak lebih dulu — pelepasan bukan pekerjaan kita lagi, tetapi pekerjaan bahasa.
  3. Baris 4 — setelah catch, program berlanjut sehat: tidak ada leak, tidak ada blok tertahan, dan jalur error tidak butuh penanganan delete khusus.

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:

  1. Berikan nilai awal pada setiap deklarasi (int hasil = 0;, std::string nama; sudah aman bawaannya) — kelas uninitialized read punah sejak lahir.
  2. Tidak ada new/delete manual di kode produksi: std::vector, std::string, std::unique_ptr, dan std::make_unique mengambil alih seluruh pasangan alokasi-pelepasan.
  3. Satu pemilik untuk setiap alokasi; kepemilikan bersama hanya dengan std::shared_ptr bila benar-benar dibutuhkan, karena pemilik ganda berarti ambiguitas pelepasan.
  4. Batas selalu dari container: ukuran dari data.size(), akses berisiko lewat .at() — indeks tulis-tangan seperti data[3] adalah pintu masuk buffer overflow.
  5. Build test memakai -fsanitize=address,undefined: LSan ikut menyala otomatis di akhir setiap eksekusi test, sehingga kebocoran tertangkap menit yang sama dengan lahirnya.
  6. Peringatan compiler dituntaskan, terutama -Wuninitialized dan -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:

LapisanAlatPertanyaan yang dijawab
Pengamatan langsungdebugger: 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 runtimeASan, UBSan, TSan“apakah eksekusi ini melanggar aturan?”
Analisis & pola-fanalyzer, clang-format, clang-tidy“apakah struktur dan gayanya sehat?”
Bukti perilakuunit test, integration test“apakah hasilnya sesuai kontrak?”
Katalog & pencegahanchecklist 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 .