Chapter sebelumnya membagi kode ke header dan source file, dan kita sempat menyinggung kata “deklarasi” dan “definisi” seolah keduanya jelas. Padahal di sinilah letak sumber error build yang paling sering menghantui pemula: bila garis batas keduanya kabur, program bisa gagal dikompilasi, gagal saat linking, atau — lebih licik lagi — menyimpan dua salinan data yang seharusnya satu. Pada Chapter ini kita presisikan kedua istilah itu, berkenalan dengan extern untuk variabel yang dibagi antar file, menyentuh aturan ODR (One Definition Rule) yang mengatur semuanya, dan menutup dengan dua kasus khusus yang sering membingungkan: konstanta const di header dan fungsi static.

Deklarasi: Mendaftarkan Nama #

Deklarasi memperkenalkan sebuah nama beserta tipenya ke compiler — cukup untuk membuat compiler tahu bentuk panggilan tanpa tahu isinya. Definisi menyediakan implementasi atau alokasi memori yang sesungguhnya; definisi juga sekaligus berlaku sebagai deklarasi. Tabel ini merangkum pasangannya:

DeklarasiDefinisi
Fungsiint tambah(int a, int b);int tambah(int a, int b) { return a + b; }
Variabelextern std::string APLIKASI;std::string APLIKASI = "Pengukur Suhu";
Kelas/struct(tidak ada konsep terpisah)struct Kotak { double sisi; };
Boleh berapa kaliBerkali-kali, asal konsistenSekali untuk seluruh program (fungsi dan variabel biasa)

Baris terakhir tabel itu adalah inti ODR: satu program hanya boleh memiliki satu definisi untuk setiap fungsi non-inline dan variabel global, sedangkan deklarasi boleh diulang sepuasnya di banyak file. Karena header bisa di-include oleh banyak file, maka aturan praktisnya yang sudah kita pakai Chapter lalu terasa sekarang: header berisi deklarasi, source file berisi definisi.

Variabel extern: Satu Definisi, Banyak Deklarasi #

Fungsi mudah karena deklarasinya alami di header. Variabel global yang ingin dibagi antar file butuh bantuan keyword extern: ia menandai “ini deklarasi saja — definisinya ada di tempat lain”. Kita praktikkan pada modul pengukuran tiga file:

cpp pengukuran.h
#include <string>

// Konstanta di header: internal linkage — tiap file yang include
// mendapat salinannya sendiri, tidak bertabrakan saat linking.
const int BATAS_AMAN = 100;

// Variabel yang dibagi antar file: deklarasi extern di header,
// definisi TUNGGAL di pengukuran.cpp.
extern std::string APLIKASI;

// Fungsi: deklarasi di sini, definisi di pengukuran.cpp.
double konversiCelsiusKeFahrenheit(double celsius);
bool dalamBatas(double nilai);

Bedah baris demi baris:

  1. Baris 1 (#include <string>) — header memakai std::string, maka ia sendiri yang meng-include-kan, sesuai prinsip include minimal.
  2. Baris 6 (const int BATAS_AMAN = 100;) — definisi konstanta langsung di header; sah dan aman, alasannya di H2 berikutnya.
  3. Baris 10 (extern std::string APLIKASI;) — deklarasi extern: compiler diberi tahu bentuk variabelnya, tetapi tidak ada memori yang dialokasikan di sini.
  4. Baris 14-15 — deklarasi dua fungsi; pola yang sama seperti modul matematika.

Di source file, definisinya dituliskan tanpa extern (menuliskan extern sekaligus mengisi nilai di source file juga sah, tetapi gaya tanpa extern lebih lazim):

cpp pengukuran.cpp
#include "pengukuran.h"

// Definisi variabel extern — satu-satunya di seluruh program.
std::string APLIKASI = "Pengukur Suhu";

// Fungsi static: internal linkage — namanya tidak keluar dari file ini.
static bool dalamRentang(double nilai, double batas) {
    return nilai >= 0.0 && nilai <= batas;
}

double konversiCelsiusKeFahrenheit(double celsius) {
    return celsius * 9.0 / 5.0 + 32.0;
}

bool dalamBatas(double nilai) {
    return dalamRentang(nilai, BATAS_AMAN);
}
cpp utama.cpp
#include <iostream>
#include "pengukuran.h"

int main() {
    std::cout << "Aplikasi   : " << APLIKASI << "\n";
    std::cout << "25 C       = " << konversiCelsiusKeFahrenheit(25.0) << " F\n";
    std::cout << "45.5 aman? : " << std::boolalpha << dalamBatas(45.5) << "\n";
    std::cout << "BATAS_AMAN : " << BATAS_AMAN << "\n";
    return 0;
}
bash
g++ -std=c++23 -Wall -Wextra utama.cpp pengukuran.cpp -o utama
./utama

Output yang Dihasilkan:

text
Aplikasi   : Pengukur Suhu
25 C       = 77 F
45.5 aman? : true
BATAS_AMAN : 100

Satu catatan desain: APLIKASI sengaja kita pakai sebagai contoh mekanisme, bukan ajakan. Variabel global yang bisa diubah dari mana-mana mempersulit pelacakan bug — pada Section Modularity dan Software Design nanti kita akan melihat cara yang lebih terkurung untuk berbagi state.

ODR: Pelanggaran dan Pesannya #

Apa yang terjadi bila definisi ditulis dua kali? Kita uji secara nyata: utama-odr.cpp mendefinisikan APLIKASI sendiri di scope global — tanpa extern, padahal pengukuran.cpp juga mendefinisikannya:

cpp utama-odr.cpp — versi bermasalah
#include <iostream>
#include <string>
#include "pengukuran.h"

// PELANGGARAN: definisi tambahan untuk nama yang sudah didefinisikan
// di pengukuran.cpp — dua definisi untuk satu variabel global.
std::string APLIKASI = "Salinan Lain";

int main() {
    std::cout << APLIKASI << "\n";
    return 0;
}

Kompilasi per file lolos mulus — compiler masing-masing hanya melihat satu definisi di depannya. Kesalahan baru tersingkap saat linker menyambungkan kedua object file:

bash
g++ -std=c++23 -Wall -Wextra -c utama-odr.cpp -o utama-odr.o
g++ -std=c++23 -Wall -Wextra -c pengukuran.cpp -o pengukuran.o
g++ utama-odr.o pengukuran.o -o odr
text
/usr/bin/ld: pengukuran.o:(.data.rel.local+0x0): multiple definition of `APLIKASI[abi:cxx11]'; utama-odr.o:(.data.rel.local+0x0): first defined here
collect2: error: ld returned 1 exit status

Pesan multiple definition ini adalah pelanggaran ODR yang paling klasik. Perhatikan tiga hal darinya: ia datang dari ld (linker, bukan compiler), ia menyebut dua object file, dan ia menunjuk definisi “pertama” berdasarkan urutan file di perintah link. Ketika nanti Anda menjumpai pesan ini pada proyek sungguhan, hampir selalu penyebabnya definisi non-inline yang salah tempat — paling sering di header tanpa inline atau tanpa extern.

Konstanta const di Header dan Fungsi static #

Dua kasus khusus menutup Chapter ini, keduanya tentang linkage — siapa saja yang berhak melihat sebuah nama.

Pertama, kenapa const int BATAS_AMAN = 100; aman didefinisikan di header padahal header di-include banyak file? Karena di scope global (namespace), const secara bawaan ber-internal linkage: setiap file yang meng-include header mendapat salinan konstanta masing-masing, dan linker tidak menganggap itu duplikat. Harganya kecil (compiler biasanya menyelipkan nilai langsung tanpa memori sama sekali), tetapi konsekuensinya nyata: jangan menyimpan alamat konstanta header dan membandingkannya antar file. Untuk C++, kebiasaan yang lebih modern adalah constexpr — mekanisme yang sama, dengan jaminan ekstra bahwa nilainya diketahui saat kompilasi.

Kedua, fungsi static seperti dalamRentang di pengukuran.cpp. Keyword static pada fungsi (atau variabel) di scope file menandai internal linkage: namanya tidak diekspor keluar file — linker bahkan tidak melihatnya. Fungsi seperti ini adalah detail implementasi modul, dan bila utama.cpp mencoba memanggil dalamRentang, hasilnya undefined reference saat linking, bukan saat kompilasi. Kebiasaannya baik: jadikan static semua fungsi bantu yang tidak perlu diketahui file lain; API modul Anda otomatis mengecil dan makin jujur.

Deklarasi di header aman dari aturan ini karena compiler berhenti membandingkan — tetapi header menyimpan jebakan lain: apa yang terjadi bila sebuah header ter-include dua kali pada file yang sama? Chapter berikutnya membahas include guards, pelindung wajib setiap header.

Referensi: Storage duration dan linkage di cppreference .