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 #
| Bug | Deskripsi | Penyebab umum |
|---|---|---|
| Dangling pointer | Pointer ke objek yang sudah hancur | return &local; |
| Use-after-free | Akses memori yang sudah dibebaskan | Lupa delete sebelum akses |
| Double free | delete dua kali pada pointer yang sama | Tidak set nullptr setelah delete |
| Buffer overflow | Akses di luar batas array/container | Index tidak divalidasi |
| Uninitialized read | Membaca variabel sebelum di-assign | Lupa 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 #
#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:
(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 #
int* dangling_demo(): mengembalikan alamat variabel lokallocal. Setelah fungsi kembali, stack framedangling_demodihancurkan — alamatlocaltidak lagi valid. Dereference pointer hasilnya adalah UB. Compiler memberi peringatan-Wreturn-local-addr; di kode demo, pragma menekan peringatan agar kompilasi bersih.use_after_free_demo:delete pmembebaskan memori heap. Mengakses*psetelahnya adalah UB — bisa crash, bisa mengembalikan data lama, bisa memberi data rusak. ASAN menangkap bug ini saat runtime.double_free_demo:delete pkedua pada pointer yang sama adalah UB. Heap allocator bisa crash, mengkorupsi heap, atau “terlihat berhasil” lalu crash di titik lain.buffer_overflow_demo:arr[10]pada array[5]menulis di luar batas. UB — bisa overwrite variabel lain di stack, crash, atau tidak terlihat dampaknya. Pakaistd::vector::at(i)untuk akses dengan bounds checking (throwout_of_range).safe_demo:std::make_unique<int>(42)membuatunique_ptr. Saatreturn p, move constructor mentransfer kepemilikan ke pemanggil. Tidak ada dangling —pdi pemanggil sah selama pemanggil tetap hidup.dangerous_view:std::string_viewnon-owning — hanya pointer + length. Saatlocalkeluar scope, view menjadi dangling. Bug terselubung karena tidak ada warning compiler untuk lifetimestring_view.safe_view: parameterconst std::string&— lifetime dijamin oleh pemanggil. Returnstring_viewaman selamashidup.null_deref_demo: cekif (p == nullptr)sebelum dereference. Idiomatik untuk API yang menerima pointer opsional.Arena::get: mengembalikan raw pointer non-owning. Pemanggil boleh membaca tetapi tidak bolehdelete—unique_ptrinternal masih memegang kepemilikan. Pola umum di API: kembalikanT*(bukanunique_ptr<T>) bila ingin menunjukkan “non-owning view”.
AddressSanitizer (ASAN) #
ASAN adalah tool runtime untuk mendeteksi bug memori. Kompilasi dengan -fsanitize=address:
g++ -std=c++17 -O1 -g -fsanitize=address -o prog prog.cpp
./progASAN 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 —
deletekedua 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 #
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) #
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/spannon-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) — eliminasinew/deletemanual, lebih aman, lebih cepat.