Topik ini membedah perjalanan kode sumber C++ sampai menjadi program yang dapat dijalankan. Peralihan itu dilalui dalam empat tahap, yaitu preprocess, compile, assemble, dan link. Setiap tahap diperagakan dengan proyek kecil yang terdiri atas tiga berkas, lengkap dengan keluaran nyata dari setiap perintah. Pemahaman atas tahapan ini memudahkan pembacaan pesan error, karena setiap pesan selalu berasal dari salah satu tahap tersebut.
Proyek contoh tiga berkas#
Proyek contoh dipisah menjadi dua unit terjemahan (translation unit), yaitu main.cpp dan pesan.cpp, yang masing-masing diolah terpisah sebelum disatukan. Berkas pesan.h menjadi jembatannya dengan memuat deklarasi fungsi, sehingga main.cpp cukup tahu bentuk pemanggilannya tanpa perlu melihat implementasinya.
// tidak-dijalankan (header, bukan program utuh; perintah build ada di bawah)
#ifndef PESAN_H
#define PESAN_H
// Deklarasi fungsi; implementasinya berada di pesan.cpp
void tampilkan_salam(const char* nama);
#endif// tidak-dijalankan (butuh build dengan berkas lain; perintah ada di bawah)
#include <iostream>
#include "pesan.h"
void tampilkan_salam(const char* nama) {
std::cout << "Halo, " << nama << "! Selamat belajar C++.\n";
}// tidak-dijalankan (butuh build dengan berkas lain; perintah ada di bawah)
#include "pesan.h"
int main() {
tampilkan_salam("Citra");
return 0;
}Komentar tidak-dijalankan pada baris pertama ketiga berkas menandai bahwa programnya hanya dapat dibangun lewat perintah build yang muncul di bagian selanjutnya. Perhatikan pula bahwa main.cpp hanya menyertakan pesan.h dan bukan pustaka standar. Pilihan ini sengaja dibuat agar keluaran tahap preprocess tetap pendek dan mudah dibaca.
Tahap 1: preprocessor menyatukan header#
Preprocessor menyalin isi setiap berkas header yang disertakan ke dalam kode sumber, menguraikan makro, dan membuang baris komentar. Perintah g++ -E menjalankan hanya tahap ini, lalu hasilnya berupa kode C++ murni yang sudah bebas dari arahan #include.
# jalankan preprocessor pada main.cpp, hasilnya ditulis ke /tmp/main.i
g++ -E main.cpp -o /tmp/main.i
# tampilkan seluruh isi berkas hasil preprocessing
cat /tmp/main.i# 0 "main.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "main.cpp"
# 1 "pesan.h" 1
void tampilkan_salam(const char* nama);
# 3 "main.cpp" 2
int main() {
tampilkan_salam("Citra");
return 0;
}Baris yang diawali tanda # dan diikuti nama berkas adalah line marker, yaitu penanda asal baris yang dicetak preprocessor dan bukan kode C++. Tampak jelas bahwa isi pesan.h, yaitu deklarasi tampilkan_salam, kini berada di tengah berkas hasil preprocessing. Bila main.cpp menyertakan <iostream>, berkas yang sama membengkak menjadi puluhan ribu baris karena seluruh pustaka tersebut ikut disalin.
Include guard pada berkas header#
Baris #ifndef PESAN_H sampai #endif pada pesan.h disebut include guard, yaitu mekanisme yang mencegah satu header disalin dua kali ke dalam unit terjemahan yang sama. Arahan #ifndef memeriksa apakah makro PESAN_H sudah didefinisikan, sehingga penyertaan kedua dan ketiga tidak melakukan apa-apa. Tanpa pelindung ini, header yang berisi definisi akan menyebabkan error ketika disertakan dua kali. Percobaan berikut memicu error tersebut dengan header kecil yang sengaja ditulis tanpa pelindung.
# header kecil tanpa include guard, berisi satu definisi variabel
cat > /tmp/tanpa-guard.h << 'EOF'
int jumlahUji = 4;
EOF
# berkas sumber yang menyertakan header itu dua kali
cat > /tmp/ujiguard.cpp << 'EOF'
#include "tanpa-guard.h"
#include "tanpa-guard.h"
int main() {
return 0;
}
EOF
# kompilasi memunculkan error redefinition
g++ /tmp/ujiguard.cpp -o /tmp/ujiguardIn file included from /tmp/ujiguard.cpp:2:
/tmp/tanpa-guard.h:1:5: error: redefinition of ‘int jumlahUji’
1 | int jumlahUji = 4;
| ^~~~~~~~~
In file included from /tmp/ujiguard.cpp:1:
/tmp/tanpa-guard.h:1:5: note: ‘int jumlahUji’ previously defined here
1 | int jumlahUji = 4;
| ^~~~~~~~~Definisi jumlahUji yang disalin dua kali bertabrakan, dan compiler menolak berkasnya. Proyek banyak berkas mudah memicu pola ini karena satu header sering disertakan oleh header lain tanpa diketahui penulisnya. Pelindung pada pesan.h mencegah keadaan itu, sehingga jumlah penyertaannya boleh berapa pun tanpa mengubah isi unit terjemahan.
Tahap 2: compiler menerjemahkan ke kode rakitan#
Compiler menerjemahkan hasil preprocessing menjadi kode rakitan (assembly), yaitu teks berisi instruksi tingkat rendah yang dimengerti prosesor. Opsi -S memerintahkan g++ berhenti tepat setelah tahap ini, sehingga hasilnya berupa berkas teks berakhiran .s. Potongan berikut menampilkan bagian awal kode rakitan milik main.cpp.
# terjemahkan main.cpp menjadi kode rakitan di /tmp/main.s
g++ -S main.cpp -o /tmp/main.s
# tampilkan 21 baris pertama kode rakitan
head -n 21 /tmp/main.s .file "main.cpp"
.text
.section .rodata
.LC0:
.string "Citra"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rax
movq %rax, %rdi
call _Z15tampilkan_salamPKc@PLT
movl $0, %eax
popq %rbpBaris call _Z15tampilkan_salamPKc@PLT adalah instruksi pemanggilan fungsi tampilkan_salam. Namanya tidak lagi terbaca seperti aslinya karena compiler menerapkan name mangling, yaitu penyandian nama fungsi beserta tipe parameternya menjadi satu simbol unik. Sandi ini memungkinkan dua fungsi bernama sama dengan parameter berbeda dapat dibedakan pada tahap linking.
Tahap 3: assembler menghasilkan berkas objek#
Kode rakitan selanjutnya dirakit menjadi berkas objek (object file), yaitu versi biner dari satu unit terjemahan. Opsi -c mengerjakan tahap compile dan assemble sekaligus tanpa melanjutkan ke linking, sehingga hasilnya berakhiran .o. Utilitas nm mendaftar simbol yang dikandung sebuah berkas objek, dan daftar itu sangat membantu saat menyelidiki kegagalan linking.
# rakit kedua kode sumber menjadi berkas objek
g++ -c main.cpp -o /tmp/main.o
g++ -c pesan.cpp -o /tmp/pesan.o
# daftar simbol yang dikandung main.o
nm /tmp/main.o U _Z15tampilkan_salamPKc
0000000000000000 T mainHuruf U berarti simbol unresolved, yaitu nama yang dipanggil tetapi belum punya definisi di berkas objek ini, sedangkan huruf T berarti simbol yang memang didefinisikan di sini. main.o hanya tahu bahwa ia memanggil _Z15tampilkan_salamPKc, tetapi alamat tubuh fungsinya baru terisi pada tahap berikutnya.
Tahap 4: linker menyatukan berkas objek#
Linker menggabungkan seluruh berkas objek, mencocokkan setiap simbol U dengan definisinya, lalu menyisipkan kode pustaka standar yang dipakai. Hasil akhirnya berupa program yang utuh dan dapat dieksekusi.
# hubungkan kedua berkas objek menjadi satu program
g++ /tmp/main.o /tmp/pesan.o -o /tmp/salam
# jalankan program hasil linking
/tmp/salamHalo, Citra! Selamat belajar C++.Apabila satu berkas objek tertinggal, linker tidak dapat menemukan definisi simbol yang dibutuhkan dan prosesnya gagal. Kesalahan ini paling sering terjadi pada proyek banyak berkas, sehingga keluaran aslinya layak dilihat.
# coba hubungkan hanya main.o tanpa pesan.o
g++ /tmp/main.o -o /tmp/salam-gagal/usr/bin/ld: /tmp/main.o: in function `main':
main.cpp:(.text+0xf): undefined reference to `tampilkan_salam(char const*)'
collect2: error: ld returned 1 exit statusPesan undefined reference to 'tampilkan_salam(char const*)' berarti simbol itu dipanggil tetapi tidak pernah didefinisikan di berkas objek mana pun yang diberikan kepada linker. Perbaikannya sesederhana memasukkan pesan.o ke dalam perintah, sebagaimana dilakukan pada percobaan pertama.
Pintasan satu perintah#
Mengetik empat perintah untuk setiap perubahan kode tidak praktis. Dalam pekerjaan sehari-hari, g++ menerima beberapa berkas sumber sekaligus dan menjalankan keempat tahap secara otomatis, dari preprocessing sampai linking.
# cara sehari-hari: seluruh tahap dalam satu perintah
g++ main.cpp pesan.cpp -o /tmp/salam
# jalankan programnya
/tmp/salamHalo, Citra! Selamat belajar C++.Perintah singkat ini persis setara dengan rangkaian -E, -S, -c, dan linking manual pada bagian sebelumnya. Opsi tingkat tahap tetap berguna saat perlu memeriksa keluaran perantara atau saat menyusun proses build yang lebih rapi.
Standar bahasa dan opsi -std#
Setiap rilis standar C++ menambah fitur baru, sehingga compiler perlu diberi tahu standar mana yang dipakai. g++ 14.2 pada sandbox ini memilih gnu++17 sebagai standar defaultnya, yaitu C++17 ditambah beberapa ekstensi milik GCC. Seluruh contoh pada jalur ini sengaja berada dalam batas C++17, sehingga kompilasinya berjalan tanpa opsi tambahan. Bila suatu program kelak membutuhkan fitur C++20 atau C++23, opsi -std=c++23 menaikkan standarnya secara eksplisit.
# mengompilasi ulang proyek contoh dengan standar C++23
g++ -std=c++23 main.cpp pesan.cpp -o /tmp/salam23
# program hasilnya berperilaku sama
/tmp/salam23Halo, Citra! Selamat belajar C++.Perbedaan c++23 dan gnu++23 terletak pada ekstensi GNU, misalnya array berpanjang variabel yang tidak dimiliki standar C++. Pada konfigurasi default GCC, ekstensi itu diterima di kedua mode, dan baru ditolak ketika mode ketat dipasangkan dengan opsi seperti -pedantic-errors. Bentuk c++23 dipilih ketika kode harus tetap portabel di luar GCC, sedangkan standar default gnu++ memudahkan eksplorasi cepat. Opsi ini juga berguna untuk memastikan perilaku compiler konsisten ketika pustaka atau panduan lain meminta standar tertentu.
Ringkasan#
- Pipeline kompilasi terdiri atas empat tahap, yaitu preprocess yang menyatukan header, compile yang menerjemahkan C++ ke kode rakitan, assemble yang menghasilkan berkas objek, dan link yang menyatukan berkas objek beserta pustaka standar.
- Opsi
-E,-S, dan-cmenghentikang++pada batas tahap tertentu sehingga keluaran perantaranya dapat diperiksa. - Name mangling menyandikan nama fungsi beserta tipe parameternya menjadi satu simbol unik yang dipakai linker.
- Pesan
undefined referenceberasal dari linker yang tidak menemukan definisi simbol, dan penyebab paling umum adalah berkas objek yang tidak disertakan. - Perintah
g++ main.cpp pesan.cpp -o /tmp/salammenjalankan seluruh tahap sekaligus dan menjadi bentuk pemanggilan yang paling sering dipakai.
Referensi#
- Introduction to the compiler, linker, and libraries — LearnCpp — Penjelasan peran compiler, linker, dan pustaka dalam mengubah kode sumber menjadi program.
- Preprocessor Options — GCC
— Dokumentasi opsi
-Edan perilaku preprocessor pada GCC. - Standards — GCC — Daftar standar bahasa yang didukung GCC beserta mode defaultnya.
- Link Options — GCC — Dokumentasi opsi linking yang dilakukan compiler GNU.
- Preprocessor — cppreference — Ringkasan fasilitas preprocessor C++ beserta arahan yang dikenalinya.