C++ memberi kontrol langsung atas memori — kekuatan sekaligus sumber bug paling mahal. Dangling pointer, use-after-free, double-free, dan buffer overflow adalah kategori bug yang sering tidak crash pada input kecil tetapi menyebabkan undefined behavior yang menggagalkan program pada input besar atau konfigurasi tertentu. Untungnya, C++ modern menawarkan tiga pertahanan: RAII (otomatis bebaskan), smart pointer (kepemilikan jelas), dan AddressSanitizer (deteksi runtime saat testing). Pada Chapter ini kita akan memetakan lima kategori bug memori, mendemonstrasikan idiom aman, dan menutup dengan penggunaan ASAN untuk menangkap bug saat development.

Lima Kategori Bug Memori #

BugDeskripsiPenyebab umum
Dangling pointerPointer ke objek yang sudah hancurreturn &local;
Use-after-freeAkses memori yang sudah dibebaskanLupa delete sebelum akses
Double freedelete dua kali pada pointer yang samaTidak set nullptr setelah delete
Buffer overflowAkses di luar batas array/containerIndex tidak divalidasi
Uninitialized readMembaca variabel sebelum di-assignLupa inisialisasi

Semua di atas adalah undefined behavior — kompilator bebas menghasilkan kode apa pun, termasuk yang “kebetulan benar” lalu gagal nanti.

Mekanisme: Lifetime dan Ownership #

Setiap objek memiliki lifetime — interval saat objek valid untuk diakses. Lifetime dimulai setelah constructor selesai dan berakhir saat destructor mulai. Pointer/reference ke objek di luar lifetime adalah UB.

Ownership menentukan siapa bertanggung jawab membebaskan. Bila ownership jelas (satu pemilik), tidak ada double-free. Bila ownership ambigu, bug muncul. Smart pointer membuat ownership eksplisit: unique_ptr = satu pemilik, shared_ptr = berbagi dengan ref count.

Kode: Bug dan Solusi #

cpp
#include <iostream>
#include <memory>
#include <vector>
#include <string>

// 1) Dangling pointer — pointer ke objek yang sudah hancur
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-local-addr"
int* dangling_demo() {
    int local = 42;
    return &local;   // ❌ local keluar scope — pointer dangling
}
#pragma GCC diagnostic pop

// 2) Use-after-free
void use_after_free_demo() {
    int* p = new int(42);
    delete p;
    // *p = 99;   // UB — use after free
    std::cout << "(use-after-free di-skip agar tidak crash)\n";
}

// 3) Double free
void double_free_demo() {
    int* p = new int(42);
    delete p;
    // delete p;  // ❌ UB — double free
    std::cout << "(double-free di-skip agar tidak crash)\n";
}

// 4) Buffer overflow
void buffer_overflow_demo() {
    int arr[5] = {1, 2, 3, 4, 5};
    // arr[10] = 99;  // ❌ UB — out of bounds write
    std::cout << "arr[4] = " << arr[4] << " (in-bounds, aman)\n";
}

// 5) RAII mengatasi dangling
auto safe_demo() {
    auto p = std::make_unique<int>(42);
    return p;   // move — pemanggil mendapat kepemilikan
}

// 6) std::string_view — jebakan lifetime
std::string_view dangerous_view() {
    std::string local = "temporary";
    return local;   // ❌ local dihancurkan — view dangling
}
std::string_view safe_view(const std::string& s) {
    return s;   // OK — lifetime s dijamin pemanggil
}

// 7) Null dereference
void null_deref_demo(int* p) {
    if (p == nullptr) {
        std::cout << "p null — skip dereference\n";
        return;
    }
    std::cout << "*p = " << *p << '\n';
}

class Arena {
    std::vector<std::unique_ptr<int>> slots_;
public:
    // Mengembalikan raw pointer — non-owning; pemanggil tidak boleh delete
    int* get(size_t i) {
        return slots_.at(i).get();
    }
    void add(int v) {
        slots_.push_back(std::make_unique<int>(v));
    }
    size_t size() const { return slots_.size(); }
};

int main() {
    // Dangling — di-skip agar tidak crash
    // int* bad = dangling_demo(); *bad;  // ❌ UB

    use_after_free_demo();
    double_free_demo();
    buffer_overflow_demo();

    auto sp = safe_demo();
    std::cout << "safe_demo: " << *sp << '\n';

    // string_view lifetime
    std::string s = "hello";
    auto sv = safe_view(s);
    std::cout << "safe_view: " << sv << '\n';

    // Null check
    null_deref_demo(nullptr);
    int v = 7;
    null_deref_demo(&v);

    // Arena — raw pointer non-owning
    Arena arena;
    arena.add(10);
    arena.add(20);
    int* p1 = arena.get(0);
    std::cout << "arena.get(0) = " << *p1 << '\n';
    // delete p1;  // ❌ — unique_ptr sudah punya, double-free!

    // Demo: ASAN mendeteksi bug (untuk dijalankan dengan -fsanitize=address)
    std::cout << "\nUntuk mendeteksi bug memori, kompilasi dengan:\n"
              << "  g++ -std=c++17 -O1 -g -fsanitize=address -o prog prog.cpp\n"
              << "ASAN menangkap: use-after-free, double-free, heap-buffer-overflow,\n"
              << "stack-buffer-overflow, use-of-uninitialized, dst.\n";

    return 0;
}

Output yang Dihasilkan:

text
(use-after-free di-skip agar tidak crash)
(double-free di-skip agar tidak crash)
arr[4] = 5 (in-bounds, aman)
safe_demo: 42
safe_view: hello
p null — skip dereference
*p = 7
arena.get(0) = 10

Untuk mendeteksi bug memori, kompilasi dengan:
  g++ -std=c++17 -O1 -g -fsanitize=address -o prog prog.cpp
ASAN menangkap: use-after-free, double-free, heap-buffer-overflow,
stack-buffer-overflow, use-of-uninitialized, dst.

Bedah Kode Baris-per-Baris #

  1. int* dangling_demo(): mengembalikan alamat variabel lokal local. Setelah fungsi kembali, stack frame dangling_demo dihancurkan — alamat local tidak lagi valid. Dereference pointer hasilnya adalah UB. Compiler memberi peringatan -Wreturn-local-addr; di kode demo, pragma menekan peringatan agar kompilasi bersih.
  2. use_after_free_demo: delete p membebaskan memori heap. Mengakses *p setelahnya adalah UB — bisa crash, bisa mengembalikan data lama, bisa memberi data rusak. ASAN menangkap bug ini saat runtime.
  3. double_free_demo: delete p kedua pada pointer yang sama adalah UB. Heap allocator bisa crash, mengkorupsi heap, atau “terlihat berhasil” lalu crash di titik lain.
  4. buffer_overflow_demo: arr[10] pada array [5] menulis di luar batas. UB — bisa overwrite variabel lain di stack, crash, atau tidak terlihat dampaknya. Pakai std::vector::at(i) untuk akses dengan bounds checking (throw out_of_range).
  5. safe_demo: std::make_unique<int>(42) membuat unique_ptr. Saat return p, move constructor mentransfer kepemilikan ke pemanggil. Tidak ada dangling — p di pemanggil sah selama pemanggil tetap hidup.
  6. dangerous_view: std::string_view non-owning — hanya pointer + length. Saat local keluar scope, view menjadi dangling. Bug terselubung karena tidak ada warning compiler untuk lifetime string_view.
  7. safe_view: parameter const std::string& — lifetime dijamin oleh pemanggil. Return string_view aman selama s hidup.
  8. null_deref_demo: cek if (p == nullptr) sebelum dereference. Idiomatik untuk API yang menerima pointer opsional.
  9. Arena::get: mengembalikan raw pointer non-owning. Pemanggil boleh membaca tetapi tidak boleh delete — unique_ptr internal masih memegang kepemilikan. Pola umum di API: kembalikan T* (bukan unique_ptr<T>) bila ingin menunjukkan “non-owning view”.

AddressSanitizer (ASAN) #

ASAN adalah tool runtime untuk mendeteksi bug memori. Kompilasi dengan -fsanitize=address:

bash
g++ -std=c++17 -O1 -g -fsanitize=address -o prog prog.cpp
./prog

ASAN menangkap:

  • Use-after-free — menulis/membaca memori yang sudah dibebaskan.
  • Heap-buffer-overflow — akses di luar alokasi new/malloc.
  • Stack-buffer-overflow — akses di luar array lokal.
  • Global-buffer-overflow — akses di luar variabel global.
  • Double-free — delete kedua pada pointer yang sama.
  • Memory leak — bila pakai opsi LSAN_OPTIONS=detect_leaks=1 (default on Linux).
  • Use-of-uninitialized — bila pakai MemorySanitizer (-fsanitize=memory).

ASAN menambah overhead ~2× (CPU) dan ~3× (memori). Untuk DSA kompetitif, ASAN berguna saat development — jalankan kode dengan ASAN sebelum submit untuk menangkap bug yang tidak terlihat di sample test.

Pola Aman di DSA #

Stack vs Heap #

cpp
struct Node { int val; Node* next; };  // raw pointer, no ownership
// ❌ Manual new/delete di seluruh tempat
// ✅ Pakai std::vector<Node> sebagai storage; "pointer" jadi index
struct VecNode { int val; int next; };  // next == -1 untuk null
std::vector<VecNode> pool;

Pointer = Index (Linked List on Vector) #

cpp
class LinkedList {
    std::vector<VecNode> nodes_;
public:
    int alloc(int v, int next) {
        nodes_.push_back({v, next});
        return (int)nodes_.size() - 1;
    }
    // ...
};

Pendekatan ini menghindari new/delete sama sekali — semua node di std::vector, otomatis dibebaskan saat vector hancur. Tidak ada memory leak, tidak ada dangling. Untuk kompetisi, pendekatan ini juga lebih cepat karena cache-friendly.

Ringkasan #

  • Lima kategori bug: dangling pointer, use-after-free, double-free, buffer overflow, uninitialized read — semuanya UB.
  • RAII + smart pointer menyelesaikan mayoritas bug: unique_ptr (default), shared_ptr (berbagi), weak_ptr (observer).
  • string_view/span non-owning — pastikan lifetime sumber lebih panjang dari view.
  • ASAN (-fsanitize=address) menangkap bug memori saat runtime; wajib dipakai saat development.
  • Untuk DSA: pertimbangkan menyimpan node di std::vector (index sebagai pointer) — eliminasi new/delete manual, lebih aman, lebih cepat.