Header dan proyek banyak berkas

Pemisahan .h dan .cpp, include guard, serta One Definition Rule.

Daftar isi

Proyek C++ yang sesungguhnya hampir selalu terdiri atas banyak berkas, dan pemisahan yang benar antara berkas header .h dan berkas sumber .cpp menjadi fondasi kebersihannya. Topik ini merangkai pembahasan modul ini menjadi satu proyek utuh, dengan include guard sebagai pelindung header dan One Definition Rule sebagai aturan yang menjaga penyatuan berkas-berkas hasil kompilasi.

Anatomi proyek banyak berkas#

Konvensi yang lazim membagi tugas menjadi tiga jenis berkas. Berkas header memuat antarmuka, yaitu deklarasi fungsi, definisi kelas, dan kebutuhan pustakanya. Berkas .cpp memuat implementasi dari deklarasi tersebut, sedangkan berkas lain yang memakai fungsinya cukup menyertakan header tanpa mengetahui detail implementasinya. Program statistik nilai ujian berikut menerapkan pembagian itu.

cpp statistik.h
// tidak-dijalankan (butuh build multi-berkas; perintah ada di bawah)
#ifndef STATISTIK_H
#define STATISTIK_H

#include <vector>

double rataRata(const std::vector<int>& nilai);
int nilaiTertinggi(const std::vector<int>& nilai);

#endif
cpp statistik.cpp
// tidak-dijalankan (butuh build multi-berkas; perintah ada di bawah)
#include "statistik.h"

double rataRata(const std::vector<int>& nilai) {
    double jumlah = 0.0;
    for (int n : nilai) {
        jumlah += n;
    }
    return jumlah / nilai.size();
}

int nilaiTertinggi(const std::vector<int>& nilai) {
    int tertinggi = nilai[0];
    for (int n : nilai) {
        if (n > tertinggi) {
            tertinggi = n;
        }
    }
    return tertinggi;
}
cpp main.cpp
// tidak-dijalankan (butuh build multi-berkas; perintah ada di bawah)
#include <iostream>
#include "statistik.h"

int main() {
    std::vector<int> nilaiUjian = {70, 85, 90, 65, 80};
    std::cout << "Rata-rata: " << rataRata(nilaiUjian) << '\n';
    std::cout << "Nilai tertinggi: " << nilaiTertinggi(nilaiUjian) << '\n';
    return 0;
}

Header statistik.h hanya menyatakan dua fungsi dan kebutuhan pustakanya, sehingga perubahan implementasi tidak merambat ke berkas yang memakainya. Perhatikan bahwa header ikut menyertakan <vector> karena tanda tangan kedua fungsinya memakai std::vector. Setiap berkas .cpp yang menyertakan header menjadi unit penerjemahan tersendiri, dan linker kemudian mencocokkan panggilan rataRata maupun nilaiTertinggi dengan definisinya di statistik.cpp.

bash
# kompilasi kedua unit penerjemahan menjadi satu program
g++ main.cpp statistik.cpp -o aplikasi-statistik
# jalankan program hasil kompilasi
./aplikasi-statistik
text Keluaran
Rata-rata: 78
Nilai tertinggi: 90

Compiler memperlakukan setiap berkas sumber secara terpisah, dan urutan berkas pada perintah g++ tidak mengubah hasil akhirnya. Opsi -c menghentikan proses pada berkas objek .o, sehingga proyek besar dapat mengompilasi unit-unitnya secara terpisah lalu menghubungkannya di akhir. Pemisahan ini menjadi dasar alat build seperti Make dan CMake yang hanya mengompilasi ulang berkas yang berubah.

Include guard#

Perintah #include menyalin isi header secara harfiah, dan satu header dapat tersisipkan dua kali tanpa disadari. Contohnya, main.cpp menyertakan dua header yang masing-masing menyertakan konversi.h, sehingga isi konversi.h muncul dua kali dalam satu unit penerjemahan dan compiler menolaknya sebagai deklarasi ganda. Include guard mencegah kejadian itu dengan praprosesor.

Bentuk klasiknya memakai tiga perintah berpasangan, yaitu #ifndef, #define, dan #endif. Makro STATISTIK_H belum didefinisikan saat header dibaca pertama kali, sehingga seluruh isinya disisipkan dan makro itu didefinisikan. Apabila header yang sama dibaca untuk kedua kalinya, kondisi #ifndef sudah tidak terpenuhi dan seluruh isi dilewati. Nama makro diambil dari nama berkasnya agar keunikan terjaga, dan kesepakatan umumnya menuliskan nama itu dengan huruf kapital semua.

Bentuk modern menggantikan tiga perintah tersebut dengan #pragma once pada baris pertama header. Direktif ini bukan bagian standar C++, tetapi didukung compiler utama dan menghilangkan kebutuhan menuliskan nama makro beserta risiko salah pengetikannya. Keduanya melayani tujuan yang sama, dan pilihan yang penting adalah memastikan setiap header punya pelindung, apa pun bentuknya.

Pada proyek yang besar, nama makro diberi awalan nama proyek atau modulnya, misalnya FUNGSIMODUL_STATISTIK_H, untuk menurunkan peluang bentrokan dengan pustaka lain. Pelindung yang tidak konsisten antara dua header serupa lebih berbahaya daripada gaya yang dipilih, karena satu header terlindungi dan yang lain tidak. Buat kebiasaan menyalin pola pelindung yang sama ke setiap header baru.

One Definition Rule#

One Definition Rule (ODR) adalah aturan inti yang mengatur kebolehan definisi berulang. Fungsi non-inline dan variabel non-const pada lingkup global hanya boleh didefinisikan satu kali di seluruh program, sedangkan definisi kelas, fungsi inline, dan variabel constexpr boleh muncul di banyak unit penerjemahan dengan syarat seluruh salinannya identik.

Pelanggaran yang paling sering adalah menuliskan badan fungsi di header tanpa inline. Header yang disertakan dua berkas .cpp menumbuhkan dua definisi rataRata, dan linker menghentikan build dengan pesan multiple definition. Pemisahan yang benar menempatkan definisi di .cpp dan hanya prototipe di header, kecuali fungsinya kecil dan sengaja ditandai inline sebagaimana dibahas pada topik fungsi inline.

Pelanggaran lain bekerja diam-diam pada tipe. Definisi kelas yang berbeda antarunit penerjemahan lolos kompilasi di setiap unit, tetapi hasil penyatuan berisi dua tipe yang sebenarnya berbeda, dan perilakunya tidak terdefinisi. Karena itu kelas dan fungsi inline yang hidup di header wajib identik di seluruh proyek, dan perubahan pada header selalu menuntut kompilasi ulang semua unit yang menyertainya.

Pustaka yang seluruh isinya berada di header, gaya yang disebut header-only, bertumpu pada pengecualian ODR ini. Fungsinya ditandai inline, definisi kelasnya identik di setiap unit penerjemahan, dan template memakai kaidah serupa. Tanpa pengecualian tersebut, pustaka semacam ini tidak mungkin dikirim dalam bentuk berkas teks semata.

Kesalahan umum#

Menyertakan berkas .cpp dengan #include adalah kesalahan yang paling menular. Penyertaan itu menyalin implementasi ke setiap pemakainya, sehingga linker melaporkan multiple definition begitu dua berkas menyertakan .cpp yang sama. Hanya header yang disertakan dengan #include, sedangkan berkas .cpp cukup dicantumkan pada perintah build. Kebiasaan ini juga menjaga build tetap ringan karena setiap implementasi hanya dikompilasi satu kali.

Header tanpa include guard sering lolos pada proyek kecil dan baru bermasalah belakangan, saat dua header mulai saling menyertakan. Pesan errornya berupa deklarasi ganda pada baris-baris yang tampaknya benar. Membiasakan include guard sejak baris pertama setiap header baru menghilangkan seluruh kelas error ini. Pelindung ganda berupa include guard yang dipasang bersama #pragma once tidak berbahaya, meskipun anjuran tetap memilih satu gaya.

Header yang disertakan tetapi isinya tidak dipakai juga layak dibersihkan. Penyertaan yang tidak perlu menambah waktu kompilasi setiap unit dan menutupi ketergantungan yang sesungguhnya. Pemeriksaan berkala atas daftar #include di setiap berkas menjaga ketergantungan tetap minimal.

Ringkasan#

  • Header memuat antarmuka dan berkas .cpp memuat implementasi, sehingga perubahan implementasi tidak merambat ke pemakainya.
  • Include guard berbentuk #ifndef, #define, #endif atau #pragma once mencegah isi header disisipkan dua kali dalam satu unit penerjemahan.
  • One Definition Rule mengizinkan satu definisi fungsi non-inline di seluruh program dan banyak salinan identik untuk definisi kelas dan fungsi inline.

Referensi#