Menu

Preprocessor C: #include, #define, dan apa yang berjalan sebelum kompilasi

Preprocessor mengedit teks sumbermu sebelum compiler melihatnya: menempelkan header, mengganti makro, dan menyalakan atau mematikan bagian kode. Ini seluruh keluarga direktifnya dan cara melihat output-nya.

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

Setiap file C yang pernah kamu kompilasi diawali dengan baris seperti #include <stdio.h>, dan baris itu bukan C. Itu instruksi untuk program terpisah, yaitu preprocessor, yang berjalan lebih dulu, menulis ulang teks sumbermu, lalu menyerahkan hasilnya ke compiler.

Memahami pembagian dua tahap ini menjelaskan banyak perilaku C: kenapa header ditempel, bukan diimpor, kenapa makro yang salah menghasilkan error di baris yang kelihatan baik-baik saja, dan kenapa file source yang sama bisa dikompilasi menjadi program yang berbeda di sistem yang berbeda.

Apa yang sebenarnya terjadi sebelum kompilasi

Mengompilasi file .c bukan satu langkah. Kurang lebih ada empat:

  1. Preprocessing: jalankan setiap baris yang diawali #, menghasilkan satu teks sumber besar yang sudah diekspansi (sebuah translation unit).
  2. Kompilasi: ubah teks itu menjadi assembly, lalu menjadi file objek.
  3. Assembling: hasilkan kode mesin.
  4. Linking: gabungkan file-file objek dengan library menjadi file yang bisa dieksekusi.

Preprocessor bekerja murni pada teks. Ia tidak tahu apa itu variabel, apa itu tipe, atau apakah kurung kurawalmu seimbang. Ia melihat karakter dan token, mengganti sebagian dengan yang lain, lalu lanjut. Semua keanehan makro berasal dari satu fakta itu.

Saat compiler membaca program ini, sudah tidak ada GREETING di mana pun. Preprocessor sudah menggantinya dengan literal "Hello from a macro", persis seolah kamu mengetiknya sendiri.

Keluarga direktif

Setiap direktif preprocessor diawali # sebagai karakter pertama yang bukan spasi di barisnya. Tidak ada titik koma, dan direktif berakhir di akhir baris kecuali kamu menyambungnya dengan backslash.

DirektifFungsinya
#includeMenempelkan isi file lain
#defineMendefinisikan makro (sebuah substitusi teks)
#undefMenghapus definisi makro
#ifdef, #ifndefMempertahankan kode berikutnya hanya kalau sebuah makro (tidak) terdefinisi
#if, #elif, #else, #endifMempertahankan kode berdasarkan ekspresi konstan
#errorMenghentikan kompilasi dengan sebuah pesan
#pragmaInstruksi khusus compiler, seperti #pragma once
#lineMengubah nomor baris yang dilaporkan (jarang)

Tiga di antaranya punya halaman sendiri: halaman makro membahas #define secara mendalam, kompilasi bersyarat membahas keluarga #if, dan file header membahas bagaimana #include dipakai untuk menyusun program multi-file.

#include: kurung sudut vs tanda kutip

#include hanya melakukan satu hal: mengganti barisnya sendiri dengan seluruh isi file yang disebutkan. File yang di-include itu juga diproses preprocessor, jadi baris #include di dalamnya ikut diekspansi.

#include <stdio.h>     /* search the system include directories */
#include "config.h"    /* search this file's directory first */

Bedanya ada pada urutan pencarian:

  • <angle brackets> mencari di direktori include standar milik compiler: /usr/include, folder milik toolchain, ditambah apa pun yang kamu tambahkan dengan -I. Ini untuk header library.
  • "quotes" mencari lebih dulu di direktori file yang melakukan include, lalu kembali ke daftar yang sama dengan kurung sudut. Ini untuk header yang kamu tulis sendiri.

Di kebanyakan compiler, kedua bentuk bekerja untuk kedua jenis header, tapi konvensinya punya makna: kurung sudut berarti "ini header milik orang lain", tanda kutip berarti "ini milikku". Mencampuradukkannya adalah cara sebuah proyek berakhir meng-include header sistem yang basi, bukan header miliknya sendiri.

Karena include adalah penempelan teks, meng-include header yang sama dua kali berarti menempelkannya dua kali. Karena itulah header butuh include guard, dan itu hal pertama yang diperbaiki di halaman file header.

#define: substitusi teks, tidak lebih

#define NAME replacement memberi tahu preprocessor: mulai dari sini, di mana pun token NAME muncul, taruh replacement sebagai gantinya.

Perhatikan apa yang tidak terjadi di sini. MAX_USERS tidak punya tipe. Ia tidak disimpan di mana pun. Ia tidak bisa diperiksa di debugger. Ia hanyalah aturan cari dan ganti, dan setelah preprocessing programnya benar-benar berisi printf("%s allows %d users\n", "Coddy", 100);.

Artinya substitusinya juga buta. Kode ini bisa dikompilasi, dan melakukan sesuatu yang mengejutkan:

#define SIZE 5 + 1

int arr[SIZE];          /* fine: int arr[5 + 1]; */
int total = SIZE * 2;   /* 5 + 1 * 2 == 7, not 12 */

Perbaikannya, yaitu tanda kurung di sekeliling semuanya, adalah aturan utama di halaman makro, bersama makro yang menerima argumen.

#define tanpa teks pengganti mendefinisikan nama itu sebagai "ada tapi kosong". Itu tidak berguna sebagai substitusi, tapi penting sebagai flag:

#define DEBUG          /* defined, expands to nothing */

Kode lalu bisa menanyakan apakah DEBUG ada dengan #ifdef.

Melihat output preprocessor

Cara terbaik untuk membangun intuisi adalah melihat apa yang sebenarnya dihasilkan preprocessor. gcc -E berhenti setelah preprocessing dan mencetak hasilnya:

gcc -E hello.c

Untuk file yang meng-include <stdio.h>, hasilnya 700 sampai 30.000 baris tergantung sistemmu, dan hampir semuanya isi header itu sendiri. Untuk melihat bagianmu saja, ambil ekornya:

gcc -E hello.c | tail -20

Coba dengan file seperti ini:

#define SQUARE(x) ((x) * (x))
#define LIMIT 10

int main(void) {
    int n = SQUARE(LIMIT);
    return n;
}

Bagian akhir output-nya menunjukkan:

int main(void) {
    int n = ((10) * (10));
    return n;
}

Setiap makro sudah hilang; yang tersisa hanya teks hasil substitusi. Saat makro berperilaku aneh, perintah ini memberitahumu alasannya dalam hitungan detik, dan itu jauh lebih baik daripada menebak. Ada dua pendamping yang berguna: gcc -dM -E - < /dev/null menampilkan semua makro yang sudah didefinisikan compiler-mu, dan gcc -E -P file.c menghilangkan penanda baris yang mengganggu.

Kenapa error menunjuk ke baris yang salah

Karena compiler melihat teks yang sudah diekspansi, kesalahan di dalam makro dilaporkan di tempat makro itu dipakai, bukan di tempat makro itu ditulis:

#define HALF(x) (x / 2

int main(void) {
    int y = HALF(8);   /* error reported here */
    return 0;
}

Tanda kurung yang hilang ada di baris #define, tapi compiler mengeluhkan baris yang berisi HALF(8), sering kali dengan pesan tentang token tak terduga yang tidak masuk akal dalam konteksnya. Saat sebuah error kelihatan mustahil, ekspansikan file-nya dengan gcc -E dan baca baris yang sebenarnya.

Compiler modern membantu: GCC dan clang mencetak catatan "in expansion of macro" yang menunjuk balik ke definisinya. Kompilasi dengan -Wall -Wextra supaya kamu benar-benar melihat catatan itu.

Makro bawaan

Preprocessor menyediakan beberapa makro sendiri. Makro ini sangat berguna untuk diagnostik:

__FILE__ dan __LINE__ diekspansi menjadi nama file dan nomor baris saat ini, dan begitulah makro assertion dan logging melaporkan di mana ada yang salah. (__func__ sedikit berbeda: itu identifier sungguhan yang disediakan compiler, bukan makro preprocessor, tapi dipakai dengan cara yang sama.)

Compiler juga sudah mendefinisikan makro platform seperti __linux__, _WIN32, dan __APPLE__. Kode yang harus berbeda per sistem mengujinya dengan #ifdef, dan itu topik kompilasi bersyarat.

Yang perlu diingat

Preprocessor itu kecil dan bodoh, dan keduanya disengaja. Ia memberimu tiga kemampuan, yaitu menarik masuk sebuah file, mengganti teks, dan menyalakan atau mematikan kode, tanpa pemeriksaan tipe sama sekali.

Karena pertukaran itulah saran standarnya adalah memakai fitur bahasa lebih dulu: pakai const int atau enum alih-alih #define untuk konstanta kalau bisa, dan fungsi sungguhan alih-alih makro seperti fungsi. Di tempat preprocessor memang alat yang tepat, seperti header, saklar portabilitas, dan konfigurasi waktu kompilasi, ia tidak tergantikan.

Selanjutnya, halaman makro membahas #define dengan serius: argumen, aturan tanda kurung, dan jebakan yang datang saat mengganti teks di dalam kode yang tidak kamu tulis.

Pertanyaan yang sering diajukan

Apa itu preprocessor di C?

Preprocessor adalah tahap pengolahan teks yang berjalan sebelum kompilasi. Ia menjalankan baris-baris yang diawali #: menempelkan isi file header di tempat #include, mengganti makro yang didefinisikan dengan #define, dan menghapus atau mempertahankan kode sesuai #if/#ifdef. Compiler hanya pernah melihat hasilnya, tidak pernah file aslimu.

Apa bedanya #include <stdio.h> dan #include "myfile.h"?

Kurung sudut mencari di direktori include sistem milik compiler, tempat header library standar berada. Tanda kutip mencari di direktori file saat ini lebih dulu, lalu kembali ke path sistem. Pakai kurung sudut untuk header library dan tanda kutip untuk header yang kamu tulis sendiri.

Bagaimana cara melihat apa yang dihasilkan preprocessor?

Jalankan gcc -E file.c untuk berhenti setelah preprocessing dan mencetak kode sumber yang sudah diekspansi. Untuk file yang meng-include <stdio.h>, output-nya ribuan baris, jadi alirkan lewat pipe: gcc -E file.c | tail -30 hanya menampilkan kodemu sendiri dengan setiap makro yang sudah diganti.

Apakah #include adalah pernyataan C?

Bukan. Direktif bukan bagian dari bahasa C itu sendiri: direktif punya sintaks berbasis baris sendiri, tidak memakai titik koma, dan sudah hilang saat compiler mem-parse programmu. Karena itulah kesalahan di sebuah makro muncul sebagai error yang membingungkan di baris yang kelihatan baik-baik saja.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai