Menu

Kompilasi Bersyarat di C: #ifdef, #ifndef, #if, dan -D

Kompilasi kode yang berbeda untuk build yang berbeda dengan #ifdef, #ifndef, #if, #elif, dan #else: saklar debug, cabang per platform, mendefinisikan macro dari command line dengan -D, dan memakai #if 0 untuk menonaktifkan kode.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Kompilasi bersyarat membuat satu file sumber bisa menjadi beberapa program yang berbeda. Preprocessor memutuskan, sebelum compiler berjalan, blok teks mana yang bertahan. Jadi kode di dalam cabang yang tidak terpenuhi bukan sekadar dilewati saat program berjalan, melainkan dihapus. Kode itu tidak pernah di-parse, sehingga tidak menyumbang apa pun ke executable dan bahkan boleh berisi konstruksi yang akan ditolak compiler saat ini.

Itulah bedanya dengan if biasa. Keduanya memilih di antara beberapa jalur; hanya satu yang memilih sebelum programnya ada.

#ifdef dan #ifndef

Pertanyaan paling sederhana adalah apakah sebuah macro sudah didefinisikan atau belum:

Hapus baris #define DEBUG dan setiap print debug hilang dari build: bukan dinonaktifkan, tetapi hilang. Jalankan sekali seperti apa adanya, lalu hapus baris itu dan jalankan lagi.

#ifndef adalah kebalikannya: pertahankan blok hanya kalau macro-nya tidak didefinisikan. Pemakaian yang paling umum adalah include guard yang dibahas di header file, dan ini juga cara memberikan nilai default yang bisa ditimpa oleh pemanggil:

Setiap #ifdef dan #ifndef harus ditutup dengan #endif. Memberi keterangan pada penutupnya di file yang panjang, misalnya #endif /* DEBUG */, sangat menghemat waktu nantinya.

#if, #elif, #else

#if menerima ekspresi integer konstan dan mempertahankan bloknya kalau hasilnya bukan nol. Itu memungkinkan perbandingan versi dan level:

Ubah LOG_LEVEL menjadi 0 atau 3 lalu jalankan ulang untuk melihat program itu sendiri berubah bentuk.

Ekspresinya dievaluasi oleh preprocessor, yang berarti hanya bisa memakai konstanta integer, operator aritmetika dan perbandingan, serta macro yang dijabarkan menjadi hal-hal itu. Preprocessor tidak bisa melihat sizeof, nilai enum, variabel, atau apa pun yang butuh compiler. Macro yang belum didefinisikan di dalam #if dievaluasi menjadi 0, bukan error, yang praktis tetapi sesekali mengejutkan:

#if FEATURE_X        /* FEATURE_X is never defined anywhere -> 0 -> block dropped */

defined(NAME) mengubah pertanyaan #ifdef menjadi sesuatu yang bisa dipakai di dalam #if, sehingga kamu bisa menggabungkan beberapa pengujian:

#if defined(DEBUG) && !defined(NDEBUG)
    /* debug build, and assertions are on */
#endif

#if defined(LINUX) || defined(BSD)
    /* either unix-like system */
#endif

#if defined(X) dan #ifdef X artinya sama; pilih yang pertama kalau kamu butuh &&, ||, atau !.

Mendefinisikan Macro dari Command Line

Kekuatan sebenarnya dari saklar-saklar ini adalah kode sumber sama sekali tidak perlu diubah. gcc -D mendefinisikan macro untuk seluruh kompilasi:

gcc -DDEBUG program.c -o program        # DEBUG defined as 1
gcc -DLOG_LEVEL=3 program.c -o program  # specific value
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNAME saja setara dengan #define NAME 1. -U NAME menghapus definisinya, yang penting kalau sebuah header mendefinisikan sesuatu yang ingin kamu matikan.

Jadi susunan yang biasa dipakai adalah: kode sumber berisi nilai default yang dijaga #ifndef, dan perintah build memilih konfigurasinya.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* quiet by default */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# development build
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# release build
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG sudah distandarkan: mendefinisikannya menonaktifkan setiap assert() di program, karena <assert.h> sendiri ditulis dengan kompilasi bersyarat. Itulah polanya dalam bentuk mini: sebuah header yang dikompilasi menjadi hal yang berbeda tergantung apa yang didefinisikan oleh build.

Saklar Platform

Compiler sudah mendefinisikan macro yang mengenali sistem target, jadi satu file sumber bisa memanggil API yang tepat di setiap sistem:

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Unsupported platform"
#endif

Yang umum dipakai: _WIN32 (didefinisikan di Windows 32-bit maupun 64-bit), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. Identitas compiler punya kumpulannya sendiri, yaitu __GNUC__, __clang__, _MSC_VER, begitu juga arsitekturnya, yaitu __x86_64__, __aarch64__.

Versi yang bisa dijalankan dan melaporkan untuk apa ia di-build:

#error layak diketahui tersendiri: ia menghentikan kompilasi dengan pesanmu. Mengakhiri rantai platform dengan #else / #error "Unsupported platform" mengubah build yang salah tanpa suara menjadi kegagalan yang jelas di bagian atas log build.

Untuk menampilkan setiap macro yang sudah didefinisikan compiler-mu:

gcc -dM -E - < /dev/null

Output itu adalah jawaban resmi untuk pertanyaan "apa saja yang bisa saya uji di mesin ini".

#if 0 untuk Menonaktifkan Kode

Menjadikan sebuah blok sebagai komentar dengan /* ... */ langsung gagal begitu blok itu berisi komentarnya sendiri, karena komentar C tidak bisa disarangkan: */ pertama di dalamnya mengakhiri komentar luar dan semua yang ada sesudahnya menjadi kode nyasar. #if 0 tidak punya masalah itu:

#if 0
    /* This whole region is removed, comments and all. */
    legacy_init();
    int n = old_calculation(42);  /* even this comment is fine */
    report(n);
#endif

Ubah menjadi #if 1 untuk mengembalikan kodenya. Editor tetap memberi highlight sebagai C, dan kamu bisa menyarangkan #if 0 di dalam #if lain.

Ini alat untuk debugging, bukan tempat penyimpanan. Kode yang berada di #if 0 tidak pernah dikompilasi, jadi ia membusuk diam-diam: kodenya sudah tidak bisa di-build saat ada yang menyalakan saklarnya. Gunakan selama mencari sumber masalah, lalu hapus bloknya dan biarkan version control yang mengingatnya.

Di Mana Kompilasi Bersyarat Bermasalah

Tiga jenis kegagalan menjadi sumber sebagian besar masalah.

Kode yang tidak pernah dikompilasi tidak pernah diperiksa. Salah ketik di dalam cabang #ifdef yang tidak aktif tidak terlihat sampai ada yang mem-build konfigurasi itu, mungkin berbulan-bulan kemudian, mungkin di CI pada platform yang tidak kamu miliki. Kalau sebuah proyek punya cabang platform yang penting, build semuanya secara rutin.

Mencampur #ifdef dengan alur kontrol cepat membuat kode sulit dibaca. Kode seperti ini sulit dinalar:

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

Kurung kurawal yang dibuka di satu cabang dan ditutup di cabang lain memang sah tetapi buruk sekali. Utamakan kondisi yang membungkus seluruh fungsi, lalu pilih di antara fungsi-fungsi itu:

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

if biasa saat program berjalan sering kali lebih baik. Kalau kedua cabang bisa dikompilasi di mana saja, if (debug_enabled) biasa menjaga kedua jalur tetap diperiksa tipenya, bisa diuji, dan bisa diganti tanpa build ulang. Simpan preprocessor untuk hal yang hanya bisa dilakukan olehnya: kode yang benar-benar tidak bisa dikompilasi di platform lain.

Untuk pola yang menggabungkan semua ini, yaitu header berisi nilai default, -D per build, dan cabang platform yang dibungkus include guard, lihat header file; untuk #define itu sendiri, lihat macro.

Pertanyaan yang sering diajukan

Apa itu kompilasi bersyarat di C?

Memakai direktif preprocessor untuk mempertahankan atau membuang blok kode sumber sebelum compiler berjalan. Kode di dalam #ifdef yang tidak terpenuhi dihapus seluruhnya dari teks: tidak pernah di-parse, tidak pernah dikompilasi, dan tidak pernah muncul di executable. Dengan cara inilah satu file sumber mendukung beberapa platform atau jenis build.

Apa perbedaan #ifdef dan #if?

#ifdef NAME hanya menanyakan apakah macro itu ada, berapa pun nilainya. #if expression mengevaluasi ekspresi integer konstan, jadi bisa membandingkan nilai: #if VERSION >= 3. Gunakan #ifdef untuk flag yang hanya ada atau tidak ada, dan #if kalau nilainya penting. #if defined(NAME) menggabungkan keduanya dan bisa disambung dengan && dan ||.

Bagaimana cara mendefinisikan macro dari command line dengan gcc?

Gunakan -D: gcc -DDEBUG program.c mendefinisikan DEBUG seolah-olah file itu diawali #define DEBUG 1, dan -DMAX=50 memberinya nilai tertentu. Dengan cara inilah build system menyalakan fitur tanpa mengubah kode sumber, dan -U NAME menghapus definisinya.

Kenapa memakai #if 0, bukan menjadikan kode sebagai komentar?

Karena komentar /* ... */ tidak bisa disarangkan: */ pertama di dalam bagian itu mengakhiri komentar lebih awal dan sisanya menjadi kode yang rusak. #if 0 ... #endif menghapus kode sebanyak apa pun yang berisi komentar sebanyak apa pun, tetap mempertahankan syntax highlighting, dan mudah sekali dikembalikan menjadi #if 1.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai