Undefined behavior (UB) dalam ekspresi adalah situasi di mana program melanggar batasan standar C sedemikian rupa sehingga compiler dibebaskan dari kewajiban menghasilkan perilaku tertentu. Hasil bisa benar pada compiler A, salah pada compiler B, crash pada compiler C, atau bahkan berbeda pada saat pengoptimalan diaktifkan. Pada Chapter ini kita akan menelaah kategori UB yang paling sering menjebak pemula di C: unsequenced modification seperti i++ + i++, signed integer overflow seperti INT_MAX + 1, unsequenced function calls dengan efek samping pada variabel global, serta akses array di luar batas. Setiap contoh sengaja ditandai sebagai UB agar pembaca tahu apa yang harus dihindari. Kode pada Chapter ini dikompilasi dengan flag -Wno-sequence-point agar compiler tidak menolak ekspresi yang dimaksudkan untuk demonstrasi.
Unsequenced Modification pada Variabel yang Sama #
Pada C, ekspresi seperti i++ + i++ atau i = i++ memodifikasi variabel i lebih dari sekali tanpa sequence point di antara modifikasi tersebut. Standar C menetapkan bahwa hal ini adalah UB: compiler bebas mengevaluasi operand kiri atau kanan lebih dulu, atau bahkan menggabungkan efek samping dengan cara tak terduga. Bahkan jika hasil tampak konsisten pada satu compiler, optimisasi seperti -O2 dapat mengubah hasilnya.
#include <stdio.h>
int main(void) {
int i = 1;
/* UB: i dimodifikasi dan dibaca tanpa sequence point */
int hasil = i++ + i++;
printf("i++ + i++ = %d, i = %d\n", hasil, i);
i = 1;
/* UB: urutan side effect tidak terdefinisi */
int hasil2 = i++ * i++;
printf("i++ * i++ = %d, i = %d\n", hasil2, i);
i = 1;
/* UB: i++ dan ++i pada i yang sama */
int hasil3 = i++ + ++i;
printf("i++ + ++i = %d, i = %d\n", hasil3, i);
return 0;
}Output yang Dihasilkan (dengan gcc -std=c11 -Wall -Wextra -Wno-sequence-point):
i++ + i++ = 3, i = 3
i++ * i++ = 2, i = 3
i++ + ++i = 4, i = 3Pada baris ke-6, ekspresi i++ + i++ menghasilkan 3 pada GCC 14.2 dengan i akhir 3. Walaupun hasil ini terlihat konsisten, ini hanyalah salah satu dari sekian banyak interpretasi yang diperbolehkan standar. Compiler lain dapat menghasilkan 2 atau 4 tanpa melanggar standar. Demikian pula pada baris ke-11 dan ke-16, pola i++ * i++ dan i++ + ++i keduanya UB. Compiler GCC mengeluarkan warning -Wsequence-point untuk ekspresi semacam ini; warning tersebut selalu harus diperlakukan sebagai error karena menandai UB nyata.
Signed Integer Overflow #
Aritmetika pada bilangan bertanda (signed integer) di C berbeda dari kebanyakan bahasa lain: overflow adalah UB, bukan wrap-around yang well-defined. Compiler modern memanfaatkan UB ini untuk optimisasi agresif. Misalnya, compiler dapat mengasumsikan a + b > a selalu benar untuk b > 0 karena overflow dianggap mustahil terjadi. Akibatnya, loop yang bergantung pada overflow untuk berhenti bisa berputar selamanya.
#include <stdio.h>
#include <limits.h>
int main(void) {
/* UB: signed integer overflow */
int maks = INT_MAX; /* biasanya 2147483647 */
printf("INT_MAX = %d\n", maks);
int overflow = maks + 1; /* UB */
printf("INT_MAX + 1 = %d (UB! bisa berbeda di compiler lain)\n", overflow);
/* UB: negasi INT_MIN */
int min = INT_MIN;
printf("INT_MIN = %d\n", min);
int negasi = -min; /* UB */
printf("-INT_MIN = %d (UB!)\n", negasi);
/* UB: left-shift ke bit tanda */
int shifter = 1 << 31; /* UB: geser 1 ke bit tanda */
printf("1 << 31 = %d (UB!)\n", shifter);
/* Yang aman: 1U << 31 (unsigned) */
/* UB: shift lebih besar dari width */
int big = 1 << 32; /* UB: shift >= width of int */
printf("1 << 32 = %d (UB!)\n", big);
return 0;
}Output yang Dihasilkan:
INT_MAX = 2147483647
INT_MAX + 1 = -2147483648 (UB! bisa berbeda di compiler lain)
INT_MIN = -2147483648
-INT_MIN = -2147483648 (UB!)
1 << 31 = -2147483648 (UB!)
1 << 32 = 0 (UB!)Mari kita telaah baris-baris kode di atas:
- Baris 8 (
int overflow = maks + 1;): penjumlahanINT_MAX + 1meluap; pada GCC dengan optimisasi default, hasil tampak sebagai-2147483648(efek wrap-around two’s complement). Namun karena ini UB, compiler berhak menghasilkan apa pun—bahkan crash atau nilai acak—ketika optimisasi-O2diaktifkan. - Baris 14 (
int negasi = -min;): negasiINT_MINsecara matematis adalah2147483648, yang melebihiINT_MAX. Karenainttidak bisa merepresentasikan nilai ini, hasilnya UB; pada GCC hasilnya-2147483648(nilai tidak berubah) karena efek two’s complement, namun ini tidak dijamin. - Baris 19 (
int shifter = 1 << 31;): literal1bertipeint(signed); menggeser1ke bit ke-31 menghasilkan pola bit yang merupakan trap representation pada beberapa platform dan UB pada platform lain. Solusi aman:1U << 31karena1Ubertipeunsigned int. - Baris 24 (
int big = 1 << 32;): geser sebesar 32 bit padaint32-bit adalah UB karena shift count harus lebih kecil dari width tipe. Compiler GCC mengeluarkan warning-Wshift-count-overflowdan menghasilkan0—namun ini juga tidak dijamin.
Unsequenced Function Calls dengan Efek Samping #
Pada ekspresi f_a() + f_b(), standar C tidak menentukan apakah f_a dipanggil lebih dulu atau f_b. Jika kedua fungsi memodifikasi variabel global yang sama, hasil akhir variabel global tersebut menjadi UB karena unsequenced modification. Pola ini sering muncul pada argumen printf yang mengeksekusi ekspresi dengan efek samping.
#include <stdio.h>
int global = 0;
int fungsi_a(void) {
global = 10;
return 1;
}
int fungsi_b(void) {
global = 20;
return 2;
}
int main(void) {
/* UB: urutan evaluasi operand tidak terdefinisi */
/* Bisa 10 atau 20, tergantung compiler */
int hasil = fungsi_a() + fungsi_b();
(void)hasil;
printf("global setelah f_a() + f_b() = %d (UB: bisa 10 atau 20)\n", global);
global = 0;
/* UB: argumen printf tidak terurut */
printf("argumen 1: %d, argumen 2: %d\n",
(global = 100, global),
(global = 200, global));
printf("global akhir = %d (UB)\n", global);
return 0;
}Output yang Dihasilkan:
global setelah f_a() + f_b() = 20 (UB: bisa 10 atau 20)
argumen 1: 100, argumen 2: 200
global akhir = 100 (UB)Pada baris ke-17, ekspresi fungsi_a() + fungsi_b() memicu kedua fungsi yang masing-masing menulis ke variabel global. Karena urutan evaluasi operand tidak terdefinisi, nilai global setelah ekspresi bisa 10 atau 20; pada GCC 14.2, fungsi_b dievaluasi terakhir sehingga global = 20. Pada baris ke-23, argumen (global = 100, global) dan (global = 200, global) menggunakan operator koma dalam tanda kurung; namun urutan evaluasi kedua argumen printf tetap tidak terurut, sehingga nilai global akhir bisa 100 atau 200. Pola (global = X, global) ini sering dipakai untuk demonstrasi efek samping; pada kode produksi, hindari efek samping pada argumen fungsi variadic.
Akses Array di Luar Batas #
#include <stdio.h>
int main(void) {
/* UB: a[i] = i++ */
int arr[] = {0, 0, 0, 0, 0};
int i = 1;
arr[i] = i++; /* UB: i dibaca untuk indeks, dimodifikasi oleh i++ */
printf("arr = [%d, %d, %d, %d, %d], i = %d\n",
arr[0], arr[1], arr[2], arr[3], arr[4], i);
/* UB: mengakses array di luar batas */
int kecil[3] = {10, 20, 30};
int oob = kecil[5]; /* UB: indeks di luar batas */
printf("kecil[5] = %d (UB!)\n", oob);
/* UB: dereferensi pointer NULL */
int *p = NULL;
(void)p; /* gunakan untuk suppress warning */
printf("(null deref dan div-by-zero tidak dieksekusi di sini)\n");
return 0;
}Output yang Dihasilkan (dengan gcc -std=c11 -Wall -Wextra -Wno-sequence-point):
arr = [0, 0, 1, 0, 0], i = 2
kecil[5] = 1 (UB!)
(null deref dan div-by-zero tidak dieksekusi di sini)Pada baris ke-6, ekspresi arr[i] = i++ memodifikasi i (melalui i++) sekaligus membaca i (untuk indeks array) tanpa sequence point di antaranya. Pada GCC, arr[1] diisi 1 (nilai i sebelum increment), kemudian i menjadi 2. Pada baris ke-11, akses kecil[5] pada array berukuran 3 adalah UB; compiler tidak memeriksa batas array pada runtime, sehingga kita membaca memori di luar array. Nilai yang dikembalikan (1 pada contoh di atas) adalah sampah memori yang kebetulan ada di sana; pada kasus lain, ekspresi semacam ini dapat menyebabkan segmentation fault atau memory corruption.
Cara Menghindari UB #
Daftar berikut merangkum praktik terbaik untuk menghindari UB di ekspresi C:
- Pisahkan modifikasi variabel ke pernyataan terpisah; jangan menggabungkan
i++denganidalam satu ekspresi yang sama. - Gunakan tipe
unsigneduntuk aritmetika yang sengaja bergantung pada wrap-around. - Gunakan
1U << n(bukan1 << n) untuk operasi shift yang mungkin menyentuh bit tanda. - Verifikasi rentang input sebelum melakukan operasi aritmetika; gunakan
__builtin_add_overflow(GCC/Clang) atau pustaka seperti<inttypes.h>untuk aritmetika aman. - Hindari efek samping pada argumen
printfatau fungsi variadic lainnya. - Selalu kompilasi dengan
-Wall -Wextradan perlakukan warning sebagai error dengan-Werrorpada development. - Aktifkan AddressSanitizer dan UBSan (
-fsanitize=address,undefined) selama pengujian untuk mendeteksi UB secara otomatis. Flag-fsanitize=undefinedkhususnya mengaktifkan UndefinedBehaviorSanitizer yang mendeteksi banyak kategori UB saat runtime: signed overflow, shift overflow, null pointer dereference, akses array di luar batas (terbatas pada stack), dan lainnya. Flag ini sangat murah performanya dan ideal dipakai selama testing; untuk produksi, sanitasi biasanya dimatikan untuk performa puncak.
Dengan demikian kita menutup Section operator dan ekspresi dengan kesadaran penuh akan batas standar C. Pemahaman UB adalah tanda kedewasaan programmer C: kita tidak hanya tahu apa yang valid, tetapi juga mengapa beberapa hal yang tampak “berhasil” sebenarnya rapuh dan harus ditulis ulang secara eksplisit.