Ketika program menghasilkan output yang salah, kebiasaan pertama banyak programmer adalah menyebarkan printf di sana-sini lalu mengompilasi ulang. Cara itu bekerja, tetapi setiap tebakan baru soal lokasi bug menuntut satu siklus edit-kompilasi-jalankan yang memakan waktu. Debugger memberi kita kemampuan yang tidak dimiliki printf: menghentikan program tepat di baris tertentu, mengintip nilai semua variabel pada saat itu, lalu melangkah baris demi baris sambil mengamati state berubah. Chapter ini membuka Section Debugging dan Code Quality dengan membandingkan kedua pendekatan secara jujur dan memandu sesi gdb pertama Anda pada program yang mengandung bug sungguhan.

printf vs Debugger: Perbandingan yang Jujur #

printf debugging bukan hal yang memalukan — untuk kasus ringan ia cepat dan tidak butuh alat tambahan. Kita hanya menyisipkan satu baris std::cout << "nilai x: " << x << "\n"; lalu menjalankan program. Masalahnya muncul ketika bug tidak sederhana:

  • Bug hanya muncul pada kondisi tertentu — printf harus mencetak banyak variabel di banyak titik “untuk jaga-jaga”, sehingga output penuh derau.
  • Setiap tebakan baru = kompilasi ulang — pada proyek besar satu siklus kompilasi bisa berhitung menit; debugger mengeksplorasi tanpa menyentuh kode.
  • printf mengubah program — menambah baris bisa menggeser timing (terutama pada kode multithread) dan lupa menghapusnya kerap lolos sampai review.
  • Debugger melihat semua sekaligus — saat program berhenti, kita bebas menanyakan nilai variabel mana pun, termasuk yang tidak kita curigai.

Sebaliknya, debugger punya kurva belajar: perintahnya perlu dihafal, dan pada program yang sangat teroptimasi atau multithread, kehadiran breakpoint bisa mengubah perilaku (efek Heisenberg). Pendekatan yang sehat: pakai printf untuk kesimpulan cepat, dan angkat debugger begitu bug menolak terlihat dalam beberapa menit.

Program Target: Bug di Fungsi Diskon #

Kita butuh program dengan bug yang sengaja ditanam. Kebijakan bisnisnya: belanja mulai 500000 rupiah mendapat diskon 10%, sisanya 5%. Perhatikan pembanding yang tertulis di fungsi hitungDiskon():

cpp
// analisis-penjualan.cpp
// Program analisis penjualan dengan diskon bertingkat.
// Ada satu bug halus di fungsi hitungDiskon() — kita akan memburunya
// dengan debugger pada chapter ini.

#include <iostream>
#include <string>
#include <vector>

struct Transaksi {
    std::string pelanggan;
    int nominal;
};

// Kebijakan diskon: 10% untuk belanja mulai 500000, selain itu 5%.
int hitungDiskon(int nominal) {
    if (nominal > 500000) {
        return 10;
    }
    return 5;
}

int main() {
    std::vector<Transaksi> data = {
        {"Budi", 350000},
        {"Siti", 500000},
        {"Dewi", 725000},
    };

    long totalDiskon = 0;
    for (const Transaksi& t : data) {
        int persen = hitungDiskon(t.nominal);
        long diskon = static_cast<long>(t.nominal) * persen / 100;
        totalDiskon += diskon;
        std::cout << t.pelanggan << ": diskon " << persen << "%\n";
    }

    std::cout << "total diskon: " << totalDiskon << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Budi: diskon 5%
Siti: diskon 5%
Dewi: diskon 10%
total diskon: 115000

Mari kita bedah:

  1. Baris 15-21 (hitungDiskon): pembanding yang tertulis adalah > (lebih besar dari), padahal kebijakan menuntut >=. Transaksi tepat 500000 jatuh ke cabang 5% — inilah bugnya, dan compiler tidak akan pernah mengeluh soal ini karena kodenya sah secara bahasa.
  2. Baris 31-36 (loop utama): setiap transaksi diproses dengan urutan yang benar — ambil persen, hitung nominal diskon, tambahkan ke total.
  3. Output yang muncul: Siti: diskon 5% dan total diskon: 115000. Nilai yang benar seharusnya Siti: diskon 10% dengan total 140000. Bug tersembunyi di tengah output yang tampak “masuk akal” — alasan klasik mengapa bug boundary lolos dari sekadar membaca kode.

Program ini dikompilasi dengan flag -g agar informasi debug (nama variabel, nomor baris) tertanam di executable — tanpa flag itu, debugger hanya melihat alamat mesin:

bash
g++ -std=c++23 -g -Wall -Wextra analisis-penjualan.cpp -o analisis-penjualan

Sesi gdb Pertama #

gdb adalah debugger standar di dunia Linux. Tampilan berikut mengilustrasikan alur sesi yang akan Anda dapatkan di terminal (alamat dan angka detail bisa berbeda antar mesin dan versi gdb):

text Contoh tampilan sesi gdb (ilustrasi)
$ gdb -q ./analisis-penjualan
Reading symbols from ./analisis-penjualan...
(gdb) break 31
Breakpoint 1 at 0x11ef: file analisis-penjualan.cpp, line 31.
(gdb) run
Starting program: /home/budi/latihan/analisis-penjualan

Breakpoint 1, main () at analisis-penjualan.cpp:31
31	    for (const Transaksi& t : data) {
(gdb) next
32	        int persen = hitungDiskon(t.nominal);
(gdb) print t.nominal
$1 = 350000
(gdb) step
hitungDiskon (nominal=350000) at analisis-penjualan.cpp:17
17	    if (nominal > 500000) {
(gdb) next
20	    return 5;
(gdb) finish
Run till exit from #0  hitungDiskon (nominal=350000) at analisis-penjualan.cpp:20
0x00005555555551f0 in main () at analisis-penjualan.cpp:32
32	        int persen = hitungDiskon(t.nominal);
Value returned is $2 = 5
(gdb) continue
Continuing.
Budi: diskon 5%

Breakpoint 1, main () at analisis-penjualan.cpp:31
31	    for (const Transaksi& t : data) {
(gdb) print t.nominal
$3 = 500000

Perintah yang baru kita pakai, satu per satu:

  1. break 31: menaruh breakpoint di baris 31. Saat eksekusi mencapai baris itu, program berhenti dan kendali kembali ke gdb.
  2. run: menjalankan program dari awal sampai breakpoint pertama. gdb menampilkan baris yang akan dieksekusi berikutnya.
  3. next: mengeksekusi satu baris lalu berhenti lagi. Jika baris itu memanggil function, function dijalankan sampai tuntas tanpa kita masuk ke dalamnya.
  4. print t.nominal: menanyakan nilai ekspresi apa pun yang valid di frame saat ini — variabel lokal, anggota struct, bahkan aritmetika sederhana.
  5. step: seperti next, tetapi kalau baris memanggil function, kita masuk ke dalam function itu — perhatikan prompt berpindah ke hitungDiskon.
  6. finish: menjalankan sisa function saat ini sampai selesai, lalu melaporkan nilai kembaliannya (Value returned is $2 = 5).
  7. continue: melanjutkan eksekusi sampai breakpoint berikutnya — dan di situ t.nominal bernilai 500000, sementara kita sudah tahu dari langkah sebelumnya bahwa cabang if akan mengambil jalan return 5.

Momen kuncinya adalah hitungan detik terakhir: tanpa kompilasi ulang sedikit pun, kita menyaksikan dengan mata kepala sendiri bahwa nominal 500000 memasuki cabang yang salah. Dengan printf, kita akan menebak-nebak dulu baris mana yang patut dicetak.

Melihat Rantai Panggilan dengan backtrace #

Saat berhenti di dalam function yang dipanggil dari banyak tempat, kita perlu tahu “siapa yang memanggil saya”. Perintah backtrace (singkatnya bt) menampilkan stack frame dari frame terdalam ke luar:

text Contoh tampilan backtrace (ilustrasi)
(gdb) backtrace
#0  hitungDiskon (nominal=500000) at analisis-penjualan.cpp:17
#1  0x000055555555524d in main () at analisis-penjualan.cpp:32

Baris #0 adalah frame tempat kita berhenti sekarang; baris #1 adalah pemanggilnya. Pada program yang lebih dalam — misalnya function dipanggil dari method yang dipanggil dari callback — backtrace berlapis ini adalah peta pertama yang kita minta setiap kali program berhenti di tempat tak terduga. Perintah frame 1 atau up lalu memindahkan “titik pandang” gdb ke frame pemanggil agar variabel di sana bisa diperiksa.

Integrasi Debugger di Editor dan IDE #

Mengetik perintah di terminal adalah fondasi, tetapi pekerjaan sehari-hari biasanya memakai jembatan IDE. VS Code (melalui ekstensi C/C++ atau CodeLLDB), CLion, dan Visual Studio menyediakan tampilan grafis untuk konsep yang sama: klik pinggir baris untuk breakpoint, panel Variables menggantikan print, tombol Step Over/Step Into menggantikan next/step, dan panel Call Stack menggantikan backtrace. Yang berubah hanya antarmukanya — model berpikirnya (berhenti, inspeksi, melangkah) persis seperti yang kita latih di atas. Mengerti versi command-line-nya membuat Anda tidak tergantung pada satu IDE pun, dan tetap efektif saat terhubung ke server tanpa GUI.

Bug yang kita buru hari ini berhenti di satu baris tertentu. Pada chapter berikutnya kita memperlengkap kotak peralatan breakpoint: penempatan bersyarat, memantau variabel dengan watchpoint, dan strategi memilih titik pemberhentian yang paling hemat langkah.

Referensi: GCC — Debugging Options (-g) .