Header kita sejauh ini bertahan tanpa insiden, tetapi itu karena kebetulan: belum ada satu pun header yang dimasukkan dua kali ke file yang sama. Pada proyek nyata kejadian itu biasa saja — utama.cpp meng-include dua header, dan ternyata keduanya meng-include header ketiga yang sama. Karena #include menyalin isi file verbatim, struct atau fungsi yang sama terdaftar dua kali, dan compiler menolak dengan error redefinition. Chapter ini membahas penyebabnya dan pola pelindung standar — include guards — yang wajib ada di setiap header yang Anda tulis mulai hari ini.

Jebakan: Header yang Masuk Dua Kali #

Kita buat jebakannya secara sengaja dan amati pesan aslinya. Tiga file, tanpa pelindung apa pun. Header kotak-salah.h mendefinisikan struct Kotak:

cpp kotak-salah.h — header tanpa pelindung
struct Kotak {
    double sisi;
};

double volume(const Kotak& k);

Header paket-salah.h memakai Kotak dan meng-include header tadi:

cpp paket-salah.h — ikut meng-include kotak-salah.h
#include <string>
#include "kotak-salah.h"

struct Paket {
    Kotak isi;
    std::string label;
};

double beratTotal(const Paket& p);

Dan utama-salah.cpp meng-include keduanya — pola yang sangat wajar, karena utama memang memakai Kotak langsung dan memakai Paket:

cpp utama-salah.cpp — kotak-salah.h masuk dua kali
#include <iostream>
#include "kotak-salah.h"
#include "paket-salah.h"

int main() {
    Kotak k {3.0};
    std::cout << volume(k) << "\n";
    return 0;
}

Kompilasi utama-salah.cpp gagal dengan pesan nyata berikut dari lingkungan praktikum:

text
In file included from paket-salah.h:2,
                 from utama-salah.cpp:3:
kotak-salah.h:1:8: error: redefinition of ‘struct Kotak’
    1 | struct Kotak {
      |        ^~~~~
In file included from utama-salah.cpp:2:
kotak-salah.h:1:8: note: previous definition of ‘struct Kotak’
    1 | struct Kotak {
      |        ^~~~~

Pesan itu mudah dibaca sekali kita pahami mekanismenya. Preprocessor menyalin kotak-salah.h pertama kali saat memproses baris 2 dari utama-salah.cpp — struct Kotak terdefinisi. Lalu saat memproses baris 3, paket-salah.h menarik kotak-salah.h yang sama untuk kedua kalinya, dan compiler menemukan definisi struct Kotak kedua — pelanggaran yang sudah kita kenal pada Chapter Declaration vs Definition, kali ini terjadi di satu file yang sama, sebelum urusan linker pun dimulai.

Pola Include Guard: ifndef, define, endif #

Pelahnya, solusinya sudah berumur puluhan tahun dan hanya butuh tiga baris preprocessor. Setiap header kita bungkus seperti ini — inilah versi aman dari modul tadi:

cpp kotak.h — dengan include guard
#ifndef KOTAK_H
#define KOTAK_H

struct Kotak {
    double sisi;
};

double volume(const Kotak& k);

#endif
cpp paket.h — header kedua punya guard-nya sendiri
#ifndef PAKET_H
#define PAKET_H

#include <string>
#include "kotak.h"

struct Paket {
    Kotak isi;
    std::string label;
};

double beratTotal(const Paket& p);

#endif

Mekanismenya berlapis tiga. Baris #ifndef KOTAK_H bertanya “apakah makro KOTAK_H belum didefinisikan?” — jika belum, seluruh isi header diproses dan baris #define KOTAK_H mendaftarkan makro itu. Ketika header yang sama masuk untuk kedua kalinya, KOTAK_H sudah terdaftar, #ifndef gagal, dan preprocessor melewati seluruh isi sampai #endif — definisi kedua tidak pernah sampai ke compiler. Guard bekerja pada level preprocessor, jadi biayanya nol saat program berjalan.

Dengan pelindung di kedua header, kompilasi tiga file lengkap berjalan mulus:

bash
g++ -std=c++23 -Wall -Wextra utama.cpp kotak.cpp paket.cpp -o utama
./utama

Output yang Dihasilkan:

text
Volume kotak : 27
Berat paket  : 13.5 kg
Label        : Buku Tugas
cpp kotak.cpp
#include "kotak.h"

double volume(const Kotak& k) {
    return k.sisi * k.sisi * k.sisi;
}
cpp paket.cpp
#include "paket.h"

double beratTotal(const Paket& p) {
    return volume(p.isi) * 0.5;  // berat = setengah volume (kg per m3)
}
cpp utama.cpp
#include <iostream>
#include "kotak.h"
#include "paket.h"

int main() {
    Kotak k {3.0};
    Paket p {k, "Buku Tugas"};

    std::cout << "Volume kotak : " << volume(k) << "\n";
    std::cout << "Berat paket  : " << beratTotal(p) << " kg\n";
    std::cout << "Label        : " << p.label << "\n";
    return 0;
}

Menamai Guard dengan Benar #

Kekuatan pola ini berdiri di satu premis: guard tiap header harus unik di seluruh proyek. Konvensi yang umum mengikuti posisi file — KOTAK_H untuk kotak.h, atau lebih panjang bila proyek punya banyak folder: SISWA_KELULUSAN_REPORT_H untuk include/kelulusan/report.h. Bila dua header tak sengaja memakai guard sama, header kedua akan dilewati diam-diam: kompilasi sukses, tetapi tipe yang Anda kira tersedia justru tidak pernah masuk — bug yang menyamar sebagai “compiler hilang ingatan”. Karena nama guard hanya dilihat oleh preprocessor, bentuknya bebas; yang penting konsisten dan mudah dikaitkan dengan filenya.

Dengan guard, header sudah aman untuk saling meng-include. Meski begitu, menulis tiga baris pelindung untuk setiap header tetap kerja manual — dan ada satu baris tunggal yang bisa menggantikannya. Chapter berikutnya membahas #pragma once: alternatif yang lebih pendek, kelebihan dan kekurangannya, serta posisinya di standar bahasa.

Referensi: Preprocessor di cppreference , Conditional inclusion di cppreference .