GCC dan Clang menerima ratusan flag command-line. Mayoritas flag tersebut jarang dipakai, tetapi segelintir di antaranya wajib kita kuasai karena hampir setiap proyek C menggunakannya. Pada chapter ini kita akan membahas flag yang paling sering muncul di Makefile dan CMakeLists.txt: -std untuk memilih versi bahasa C, -Wall dan -Wextra untuk mengaktifkan peringatan, -O untuk tingkat optimasi, -g untuk informasi debug, serta -I, -L, dan -l untuk mencari header dan library. Memahami flag-flag ini membuat kita bisa membaca konfigurasi build proyek orang lain tanpa kebingungan.

Memilih Standar Bahasa: -std=c11 #

GCC secara default menggunakan standar gnu17 (dialek GCC dari C17) — bukan C murni. Untuk memilih standar tertentu, kita gunakan flag -std=. Nilai yang umum: c99, c11, c17, dan c23.

c
#include <stdio.h>

int sum_to(int n) {
    int total = 0;
    for (int i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}

int main(void) {
    int hasil = sum_to(100);
    printf("sum 1..100 = %d\n", hasil);
    return 0;
}

Kompilasi dengan standar C11 dan jalankan:

bash
gcc -std=c11 demo.c -o demo && ./demo

Output yang Dihasilkan:

text
sum 1..100 = 5050

Peringatan: -Wall dan -Wextra #

Flag -Wall mengaktifkan banyak peringatan yang seharusnya selalu diaktifkan (meski bukan semua peringatan — namanya menyesatkan). Flag -Wextra menambahkan peringatan ekstra yang tidak masuk -Wall, misalnya parameter fungsi yang tidak dipakai atau perbandingan signed/unsigned. Praktik baik industri: selalu pakai keduanya, dan perlakukan warning sebagai error dengan -Werror bila perlu.

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

Output yang Dihasilkan:

text
(sum 1..100 = 5050 saat dijalankan; kompilasi bersih tanpa peringatan)

Bila kita sengaja membuat bug kecil seperti variabel tidak dipakai, -Wextra akan menunjukkannya:

c
int main(void) {
    int x;            /* tidak dipakai */
    printf("halo\n");
    return 0;
}
bash
gcc -std=c11 -Wall -Wextra unused.c -o unused

Output yang Dihasilkan:

text
unused.c: In function 'main':
unused.c:2:9: warning: unused variable 'x' [-Wunused-variable]
    2 |     int x;            /* tidak dipakai */
      |         ^

Mari kita telaah baris-baris output di atas:

  1. Baris pertama (unused.c: In function 'main':): GCC menunjukkan file dan fungsi tempat warning ditemukan, sehingga kita langsung tahu konteksnya.
  2. Baris kedua (unused.c:2:9: warning: unused variable 'x' [-Wunused-variable]): format file:baris:kolom: warning: pesan [nama-warning]. Kolom 9 menunjuk tepat ke posisi variabel x. Nama -Wunused-variable bisa dipakai untuk menonaktifkan warning tertentu secara selektif dengan -Wno-unused-variable.
  3. Baris ketiga (2 | int x;): GCC menampilkan ulang baris kode sumber dengan nomor baris.
  4. Baris keempat ( | ^): tanda ^ menunjuk ke kolom ke-9, di bawah variabel x.

Tingkat Optimasi: -O0, -O1, -O2, -O3 #

Flag -O (huruf O besar) mengatur seberapa agresif compiler mengoptimasi kode. Tingkat yang umum:

  • -O0 (default): tidak ada optimasi. Cocok untuk debugging karena variabel tetap di stack dan urutan eksekusi persis seperti tertulis di kode.
  • -O1: optimasi ringan, mengurangi ukuran kode dan waktu eksekusi tanpa mengorbankan waktu kompilasi.
  • -O2: optimasi standar rilis. Mayoritas proyek memakai tingkat ini.
  • -O3: optimasi agresif (loop unrolling, vectorization). Kode menjadi lebih cepat tetapi ukuran binary membengkak.
  • -Os: optimasi untuk ukuran kode terkecil.
  • -Og: optimasi ringan yang ramah debugging (sejak GCC 4.8).

Mari kita lihat bagaimana tingkat optimasi mempengaruhi kode yang dihasilkan:

c
#include <stdio.h>

int sum_array(const int *a, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += a[i];
    }
    return total;
}

int main(void) {
    int a[1000];
    for (int i = 0; i < 1000; i++) {
        a[i] = i + 1;
    }
    printf("sum 1..1000 = %d\n", sum_array(a, 1000));
    return 0;
}
bash
for opt in O0 O1 O2 O3; do
    gcc -$opt -S opt_demo.c -o opt_demo_$opt.s
    lines=$(wc -l < opt_demo_$opt.s)
    echo "-$opt: $lines baris assembly"
done

Output yang Dihasilkan:

text
-O0: 84 baris assembly
-O1: 62 baris assembly
-O2: 108 baris assembly
-O3: 148 baris assembly

Tampak bahwa -O1 menurunkan jumlah baris (compiler menghapus operasi stack yang tidak perlu), sementara -O2 dan -O3 justru menaikkan jumlah baris karena loop unrolling, alignment directive, dan vectorization memperbesar kode meski kode tersebut biasanya berjalan lebih cepat.

Mari kita bandingkan loop inti fungsi sum_array pada -O0 dan -O2:

text
# -O0
.L3:
    movl    -8(%rbp), %eax       ; load i dari stack
    cltq
    leaq    0(,%rax,4), %rdx     ; alamat a[i] dihitung dari awal setiap iterasi
    movq    -24(%rbp), %rax      ; load pointer a dari stack
    addq    %rdx, %rax
    movl    (%rax), %eax         ; load a[i]
    addl    %eax, -4(%rbp)       ; tambahkan ke total di stack
    addl    $1, -8(%rbp)         ; i++
.L2:
    movl    -8(%rbp), %eax
    cmpl    -28(%rbp), %eax      ; bandingkan i dengan n (load dari stack)
    jl      .L3
text
# -O2
.L3:
    addl    (%rdi), %eax         ; total += *p
    addq    $4, %rdi             ; p++
    cmpq    %rdx, %rdi           ; bandingkan p dengan end (register)
    jne     .L3
    ret

Versi -O0 membaca/menulis i, total, dan pointer a dari stack di setiap iterasi — empat kali akses memori per loop. Versi -O2 menggunakan register eax untuk akumulator, rdi untuk pointer berjalan, dan rdx untuk end pointer; tidak ada akses stack di dalam loop. Inilah inti perbedaan optimasi: compiler menyusun ulang akses memori agar sesedikit mungkin menyentuh RAM.

Informasi Debug: -g #

Flag -g menyisipkan informasi debug format DWARF ke dalam binary. Informasi ini dipakai oleh gdb, valgrind, dan profiler untuk memetakan balik instruksi mesin ke baris kode C sumber. Tanpa -g, debugger hanya bisa menampilkan alamat memori, bukan nama variabel atau nomor baris.

bash
gcc -g -std=c11 demo.c -o demo_debug
ls -l demo_debug | awk '{print $5, $9}'

Output yang Dihasilkan:

text
17360 demo_debug

Bandingkan dengan binary tanpa -g:

bash
gcc -std=c11 demo.c -o demo_nodebug
ls -l demo_nodebug | awk '{print $5, $9}'

Output yang Dihasilkan:

text
15984 demo_nodebug

Selisih 1376 byte adalah tabel DWARF yang berisi mapping baris, tabel simbol lokal, dan tipe data. Untuk build rilis, kita biasanya memakai -O2 -g sekaligus lalu memangkas informasi debug dengan strip bila ingin binary yang lebih ramping.

Mencari Header dan Library: -I, -L, -l #

Ketiga flag ini mengontrol pencarian header dan library:

  • -I<path>: tambahkan path ke daftar direktori pencarian header untuk #include <...>.
  • -L<path>: tambahkan path ke daftar direktori pencarian library.
  • -l<name>: link dengan library bernama lib<name>.so atau lib<name>.a.

Mari kita demonstrasikan -I dengan header kustom:

bash
mkdir -p include
echo '#define VERSION "1.0.0-demo"' > include/myheader.h
cat > use_header.c <<'EOF'
#include <stdio.h>
#include <myheader.h>

int main(void) {
    printf("Versi: %s\n", VERSION);
    return 0;
}
EOF
# Tanpa -Iinclude (harus gagal):
gcc -std=c11 use_header.c -o use_header_noI
# Dengan -Iinclude (harus sukses):
gcc -std=c11 -Wall -Iinclude use_header.c -o use_header && ./use_header

Output yang Dihasilkan:

text
# Tanpa -Iinclude:
use_header.c:2:10: fatal error: myheader.h: No such file or directory
    2 | #include <myheader.h>
      |          ^~~~~~~~~~~~
compilation terminated.

# Dengan -Iinclude:
Versi: 1.0.0-demo

Untuk demonstrasi -L dan -l, kita akan melihatnya secara konkret pada chapter Static Library dan Shared Library.