Header File dan Proyek Multi-Berkas

Pembagian kode ke berkas .h dan .c, mekanisme #include, dan kompilasi banyak berkas.

Daftar isi

Program yang tumbuh melampaui beberapa ratus baris menjadi sulit dinavigasi apabila seluruhnya ditempatkan dalam satu berkas. C menyediakan pola pembagian yang mapan, yaitu berkas .h untuk antarmuka dan berkas .c untuk implementasi. Topik ini membangun proyek kecil berisi tiga berkas, membahas mekanisme #include, lalu menutupnya dengan dua cara kompilasi yang menghasilkan program yang sama.

Antarmuka dan implementasi#

Berkas dengan ekstensi .h, yang lazim disebut header file, menyimpan deklarasi yang ingin dibagikan, sedangkan berkas .c memuat definisi lengkapnya. Pembagian ini memisahkan apa yang ditawarkan dari cara pengerjaannya, sehingga pengguna fungsi cukup membaca header tanpa membuka implementasinya. Manfaat langsungnya meliputi navigasi yang lebih mudah, kompilasi ulang yang lebih hemat, dan pemakaian ulang kode antarprogram.

Proyek contoh pada topik ini adalah konversi suhu yang terbagi menjadi tiga berkas. Berkas konversi.h menyimpan dua prototipe fungsi, berkas konversi.c menyimpan definisinya, dan berkas main.c memakai keduanya. Ketiga berkas inilah yang menjadi bahan seluruh pembahasan pada topik ini.

c konversi.h
// tidak-dijalankan
#ifndef KONVERSI_H
#define KONVERSI_H

/* Prototipe yang dipakai bersama oleh seluruh program. */
double celsius_ke_fahrenheit(double c);
double fahrenheit_ke_celsius(double f);

#endif

Tiga baris pertama dan baris terakhir pada konversi.h merupakan include guard yang mencegah isi header tersalin dua kali, dan cara kerjanya dibahas pada bagian akhir. Berkas header juga lazim memuat konstanta #define dan deklarasi tipe yang dipakai bersama.

c konversi.c
// tidak-dijalankan
#include "konversi.h"

double celsius_ke_fahrenheit(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

double fahrenheit_ke_celsius(double f)
{
    return (f - 32.0) * 5.0 / 9.0;
}

Berkas konversi.c memuat header miliknya sendiri agar definisi fungsinya diperiksa terhadap prototipe yang dijanjikan. Kesalahan ketidakcocokan tipe kini tertangkap pada saat kompilasi, bukan pada saat pengaitan yang pesannya jauh lebih sulit dibaca.

c main.c
// tidak-dijalankan
#include <stdio.h>
#include "konversi.h"

int main(void)
{
    printf("25.0 C = %.1f F\n", celsius_ke_fahrenheit(25.0));
    printf("98.6 F = %.1f C\n", fahrenheit_ke_celsius(98.6));
    return 0;
}

Berkas main.c memuat dua header sekaligus, yaitu stdio.h untuk printf dan konversi.h untuk kedua fungsi konversi. Bagian implementasi tidak pernah dilihat oleh main.c, sehingga rumus konversi bebas diubah tanpa menyentuh satu baris pun di berkas pemanggilnya.

Mekanisme #include#

Arahan #include dikerjakan oleh tahap preproses dengan cara yang sangat sederhana, yaitu menyalin seluruh isi berkas yang dirujuk ke posisi arahannya. Tidak ada pencocokan nama atau penyesuaian otomatis, melainkan sekadar penempelan teks. Bentuk <stdio.h> mencari berkas pada direktori sistem, sedangkan bentuk "konversi.h" mencari berkas pada direktori berkas sumber yang memuat arahannya terlebih dahulu. Konvensi penulisannya pun jelas, yaitu sudut untuk header pustaka standar dan tanda kutip untuk header milik proyek sendiri.

Diagram berikut merangkum perjalanan ketiga berkas sumber hingga menjadi satu program utuh.

berkas sumberberkas objekmain.ckonversi.ckonversi.hmain.okonversi.okonversiprogram utuhpreprosesdan kompilasipengaitan
Perjalanan berkas sumber dari preproses, kompilasi, hingga pengaitan

Perhatikan peran konversi.h pada kolom pertama. Berkas itu tidak pernah dikompilasi sendiri, melainkan ditempelkan ke setiap berkas yang memuatnya pada tahap preproses. Akibatnya, isi header terdistribusi ke dua unit kompilasi yang berbeda, dan keduanya mengenal prototipe yang sama persis.

Kompilasi banyak berkas#

Kompilasi proyek dua berkas hampir sama dengan program tunggal, yaitu seluruh berkas .c disebutkan dalam satu perintah. Kompilator menjalankan tahap preproses, kompilasi, dan perakitan untuk masing-masing berkas, lalu tahap pengaitan menyatukan hasilnya. Kedua cara yang dibahas di bawah menghasilkan program yang sama, dan pilihannya biasanya ditentukan kebiasaan mengetik, bukan oleh hasilnya.

bash
# mengompilasi kedua berkas sumber sekaligus
gcc main.c konversi.c -o konversi

# menjalankan program hasil pengaitan
./konversi
text Keluaran
25.0 C = 77.0 F
98.6 F = 37.0 C

Cara kedua memisahkan kompilasi setiap berkas. Keluaran tiap tahap menjadi berkas objek berekstensi .o, yang belum dapat dijalankan sampai dihubungkan menjadi satu. Nama konversi.o dan main.o muncul otomatis mengikuti nama sumbernya.

bash
# kompilasi per berkas: setiap berkas .c menjadi berkas objek
gcc -c konversi.c
gcc -c main.c

# mengaitkan berkas objek menjadi program utuh
gcc konversi.o main.o -o konversi
./konversi
text Keluaran
25.0 C = 77.0 F
98.6 F = 37.0 C

Cara kedua inilah yang menjadi dasar build proyek besar. Apabila hanya konversi.c yang berubah, cukup berkas itu yang dikompilasi ulang, lalu pengaitan diulang dengan berkas objek yang sudah ada. Untuk proyek yang terus tumbuh, pengulangan perintah semacam ini diserahkan kepada alat seperti make agar hanya bagian yang berubah yang dikerjakan ulang. Nama berkas objek yang mengikuti nama sumbernya pun sudah cukup untuk proyek sekecil ini.

Include guard#

Header dapat dimuat lebih dari satu kali tanpa disadari, misalnya main.c memuat dua header yang masing-masing juga memuat konversi.h. Deklarasi fungsi yang sama memang boleh diulang, tetapi definisi tipe seperti struct dan typedef tidak boleh, sehingga penyertaan ganda berakhir sebagai error. Include guard mencegah kejadian itu dengan bantuan makro preproses.

c Contoh: include guard pada header
// tidak-dijalankan
#ifndef KONVERSI_H        /* apabila makro belum terdefinisi */
#define KONVERSI_H

/* seluruh isi header ditulis di antara keduanya */

#endif

Pemuatan pertama menemukan KONVERSI_H belum terdefinisi, mendefinisikannya, lalu menyalin seluruh isi header. Pemuatan kedua menemukan makro yang sudah ada, sehingga seluruh isi antara #ifndef dan #endif dilewati begitu saja. Alternatif modern berupa #pragma once memberikan hasil yang sama dan didukung GCC, tetapi bukan bagian dari standar C, sehingga include guard tetap menjadi bentuk yang dijamin standar.

Nama makro pada guard lazim diambil dari nama berkasnya dengan huruf besar dan garis bawah, sehingga konversi.h menjadi KONVERSI_H. Kebiasaan ini mencegah dua header memakai nama makro yang sama secara tidak sengaja. Apabila dua guard bertabrakan, header kedua akan diam-diam terlewat, sehingga keunikan nama makronya perlu dijaga sejak awal.

Kesalahan umum#

Pesan error pada proyek multi-berkas muncul pada tahap pengaitan, dan empat pola berikut menyumbang sebagian besar kejadiannya.

  • Mendefinisikan fungsi di dalam header. Setiap berkas .c yang memuat header ikut menerima salinan definisinya, lalu pengaitan berhenti dengan pesan multiple definition. Header hanya menyimpan prototipe, sedangkan definisi ditempatkan pada satu berkas .c.
  • Lupa mengompilasi salah satu berkas sumber. Kompilasi tiap berkas berhasil, tetapi pengaitan melaporkan undefined reference untuk fungsi yang tidak pernah didefinisikan. Sebutkan semua berkas .c pada perintah kompilasi atau gunakan flag -c untuk setiap berkasnya.
  • Menaruh definisi variabel global di header. Pola ini memicu masalah yang sama dengan definisi fungsi ganda. Bagikan variabel global melalui deklarasi extern di header dan letakkan definisinya di satu berkas .c.
  • Mengira header ikut dikompilasi sebagai berkas sumber. Header hanya disalin ke berkas .c yang memuatnya, sehingga kompilasi selalu dilakukan terhadap berkas .c, bukan terhadap .h.

Ringkasan#

  • Berkas .h menyimpan prototipe yang dipakai bersama, sedangkan berkas .c menyimpan definisi lengkapnya.
  • #include menyalin isi berkas yang dirujuk pada tahap preproses, dengan <...> untuk header sistem dan "..." untuk header proyek.
  • Kompilasi dilakukan sekaligus dalam satu perintah atau terpisah per berkas objek yang kemudian dikaitkan.
  • Include guard mencegah isi header tersalin dua kali melalui pemeriksaan makro preproses.

Referensi#