Bug adalah bagian tak terhindarkan dari pemrograman, termasuk di C. Bahkan programmer berpengalaman pun menulis kode yang gagal compile atau menghasilkan output salah pada percobaan pertama. Yang membedakan programmer pemula dari yang berpengalaman adalah kemampuan membaca pesan kesalahan compiler dan menjelaskan isi variabel saat program berjalan. Pada Chapter ini kita akan mempraktikkan tiga teknik debugging dasar: printf debugging, assert, dan membaca pesan kesalahan compiler.

Membaca Pesan Kesalahan Compiler #

Saat compiler menemukan kesalahan sintaks, ia mencetak pesan yang sering kali terlihat menakutkan bagi pemula. Padahal, pesan tersebut selalu menyertakan tiga informasi penting: nama file, nomor baris, dan deskripsi kesalahan. Mari kita lihat contoh kode yang sengaja mengandung kesalahan:

c
#include <stdio.h>

int main(void) {
    int nilai = 10
    printf("Nilai = %d\n", nilai)
    return 0;
}

Kompilasi kode di atas akan gagal. Mari kita lihat pesan yang dihasilkan:

bash
gcc -std=c11 -Wall -Wextra -o error_demo error_demo.c

Output yang Dihasilkan:

text
error_demo.c: In function 'main':
error_demo.c:5:5: error: expected ',' or ';' before 'printf'
    5 |     printf("Nilai = %d\n", nilai)
      |     ^~~~~~
error_demo.c:4:9: warning: unused variable 'nilai' [-Wunused-variable]
    4 |     int nilai = 10
      |         ^~~~~

Mari kita telaah baris-baris pesan di atas:

  1. Baris 1 (error_demo.c: In function 'main':): compiler menyatakan sedang memeriksa fungsi main di file error_demo.c. Baris ini hanya penanda konteks.
  2. Baris 2 (error_demo.c:5:5: error: ...): format file:baris:kolom: error: pesan. Kesalahan berada di baris 5, kolom 5. Compiler mengharapkan tanda koma atau titik koma sebelum kata printf — yang sebenarnya berarti titik koma hilang di akhir baris 4.
  3. Baris 3-4: cuplikan kode sumber dengan penanda ^~~~~~ menunjuk lokasi tepatnya kesalahan ditemukan.
  4. Baris 5 (error_demo.c:4:9: warning: ...): peringatan tambahan bahwa variabel nilai tidak dipakai. Karena compiler berhenti setelah baris 4 (tidak ada titik koma), variabel nilai tidak pernah terbaca; compiler menandai ini sebagai unused variable.
  • Pesan kesalahan compiler sering kali menunjuk baris setelah lokasi kesalahan yang sebenarnya. Bila compiler mengeluh “expected ‘;’ before X” di baris N, periksa baris N-1: kemungkinan besar titik koma hilang di akhir baris N-1.
  • Peringatan (warning) tidak menghentikan kompilasi, tetapi kesalahan (error) menghentikannya. Selalu baca pesan secara berurutan dari atas ke bawah dan perbaiki kesalahan pertama terlebih dahulu; sering kali kesalahan berikutnya adalah efek domino dari kesalahan pertama.

Teknik printf Debugging #

Teknik debugging paling universal di C adalah menambahkan pemanggilan printf di sepanjang jalur eksekusi untuk memeriksa nilai variabel dan urutan pemanggilan fungsi. Teknik ini tidak memerlukan tool khusus dan bekerja di semua platform.

c
#include <stdio.h>

int faktorial(int n) {
    int hasil = 1;
    printf("[DEBUG] memanggil faktorial(%d)\n", n);
    for (int i = 1; i <= n; i++) {
        hasil *= i;
        printf("[DEBUG] i=%d, hasil=%d\n", i, hasil);
    }
    return hasil;
}

int main(void) {
    int f = faktorial(5);
    printf("5! = %d\n", f);
    return 0;
}

Output yang Dihasilkan:

text
[DEBUG] memanggil faktorial(5)
[DEBUG] i=1, hasil=1
[DEBUG] i=2, hasil=2
[DEBUG] i=3, hasil=6
[DEBUG] i=4, hasil=24
[DEBUG] i=5, hasil=120
5! = 120

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (printf("[DEBUG] memanggil faktorial(%d)\n", n);): mencetak nilai parameter n saat fungsi dipanggil; prefiks [DEBUG] memudahkan kita memfilter baris ini dari output normal.
  2. Baris 7-9 (for loop dengan printf di dalamnya): setiap iterasi mencetak nilai i dan hasil akumulasi; kita bisa melihat bagaimana hasil tumbuh dari 1 menjadi 120.
  3. Baris 12 (int f = faktorial(5);): pemanggilan fungsi; output debug muncul sebelum baris printf("5! = %d\n", f) karena pemanggilan faktorial dieksekusi lebih dulu.

Untuk proyek yang lebih besar, ganti printf langsung dengan makro #define DEBUG_PRINT(fmt, ...) printf("[DEBUG] " fmt "\n", __VA_ARGS__) agar seluruh debug bisa dimatikan dengan satu baris #undef DEBUG_PRINT tanpa harus menghapus setiap pemanggilan satu per satu.

Memeriksa dengan assert #

Macro assert dari header <assert.h> menghentikan program bila kondisi yang diberikan bernilai 0 (false). assert sangat berguna untuk memeriksa asumsi yang seharusnya selalu benar jika kode ditulis dengan benar — misalnya, indeks array dalam batas, atau parameter fungsi tidak negatif.

c
#include <stdio.h>
#include <assert.h>

int main(void) {
    int nilai = 10;
    assert(nilai > 0);
    printf("Assert lolos, nilai = %d\n", nilai);
    return 0;
}

Output yang Dihasilkan:

text
Assert lolos, nilai = 10

Bila kondisi assert tidak terpenuhi, program dihentikan dengan pesan ke standard error:

c
#include <stdio.h>
#include <assert.h>

int main(void) {
    int nilai = -5;
    assert(nilai > 0);
    printf("Tidak akan dicetak.\n");
    return 0;
}

Output yang Dihasilkan:

text
test_ch14_assert2: /tmp/test_ch14_assert2.c:6: main: Assertion `nilai > 0' failed.

Setelah pesan di atas muncul, program berakhir dengan exit code 134 (sinyal SIGABRT). Pesan assertion mencakup nama executable, nama file sumber, nomor baris, nama fungsi, dan ekspresi yang gagal — informasi ini sangat berharga untuk memperbaiki bug.

  • Macro assert dapat dimatikan dengan mendefinisikan makro NDEBUG sebelum menyertakan <assert.h>. Konfigurasi ini biasanya dipakai pada build produksi agar assertion check tidak memperlambat eksekusi.
  • Gunakan assert untuk kondisi yang menandakan bug di kode kita sendiri. Jangan pakai assert untuk validasi input dari pengguna — gunakan if eksplisit dan pesan kesalahan yang ramah untuk kasus tersebut.

Memperhatikan Peringatan Compiler #

Compiler GCC dengan flag -Wall -Wextra aktif mengeluarkan peringatan untuk kategori bug umum. Salah satu peringatan yang paling sering muncul adalah unused variable:

c
#include <stdio.h>

int main(void) {
    int nilai = 10;
    int tidak_dipakai = 99;
    printf("Nilai = %d\n", nilai);
    return 0;
}

Kompilasi dengan flag ketat:

bash
gcc -std=c11 -Wall -Wextra -o warn_demo warn_demo.c

Output yang Dihasilkan:

text
warn_demo.c: In function 'main':
warn_demo.c:5:9: warning: unused variable 'tidak_dipakai' [-Wunused-variable]
    5 |     int tidak_dipakai = 99;
      |         ^~~~~~~~~~~~~

Meskipun program masih berhasil di-compile dan jalan, peringatan ini menandakan ada variabel yang dibuat tetapi tidak pernah dipakai. Variabel tidak terpakai biasanya merupakan sisa refactoring yang lupa dibersihkan, dan bisa membingungkan pembaca kode. Hapus variabel tersebut untuk membuat kode lebih bersih:

c
#include <stdio.h>

int main(void) {
    int nilai = 10;
    printf("Nilai = %d\n", nilai);
    return 0;
}

Output yang Dihasilkan:

text
Nilai = 10

Strategi Debugging Bertahap #

Saat menemui bug, ikuti urutan berikut agar proses debugging lebih efisien:

  1. Baca pesan kesalahan pertama dari compiler. Banyak kesalahan berikutnya sering kali adalah efek domino; perbaiki kesalahan pertama lalu kompilasi ulang.
  2. Tambahkan printf di sekitar lokasi yang dicurigai. Cetak nilai variabel sebelum dan sesudah operasi yang mencurigakan.
  3. Kecilkan ruang bug. Bila bug muncul pada fungsi besar, coba komentari separuh kode dan lihat apakah bug masih muncul. Lakukan ini hingga bug terisolasi di blok kode yang kecil.
  4. Gunakan assert untuk asumsi. Bila kita yakin variabel index selalu kurang dari panjang array, tambahkan assert(index < panjang_array);. Bila asumsi ini salah, program berhenti tepat di lokasi yang melanggar.
  5. Untuk bug lebih kompleks, gunakan debugger interaktif seperti gdb atau lldb. Debugger memungkinkan kita menjalankan program baris per baris, memeriksa nilai variabel, dan memodifikasi memori secara langsung. Bab Tool Debugging Lanjutan (di Section selanjutnya) akan membahas gdb lebih dalam.
  • Jangan pernah mengabaikan peringatan compiler. Peringatan yang tampak sepele hari ini bisa menjadi bug serius esok hari. Sikap yang disarankan: tambahkan flag -Werror agar semua peringatan diperlakukan sebagai kesalahan, sehingga program tidak akan di-compile bila masih ada peringatan.
  • Untuk pengembangan C modern, pertimbangkan juga sanitizer -fsanitize=address,undefined yang menangkap bug memori (buffer overflow, use-after-free) dan perilaku tak terdefinisi pada saat program berjalan.