Menu

File header di C: .h vs .c, include guard, dan program multi-file

Cara membagi program C ke beberapa file: apa yang masuk ke .h, apa yang masuk ke .c, include guard yang mencegah include ganda, mengompilasi beberapa file sekaligus, dan berbagi variabel global dengan extern.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Satu file .c masih oke sampai panjangnya 2.000 baris. Membagi program ke beberapa file membuat setiap bagian bisa dikompilasi terpisah, dipakai ulang di program lain, dan dibaca sendiri. Masalahnya, C tidak punya sistem import. Yang ada hanyalah preprocessor yang menempelkan teks, ditambah linker yang menggabungkan potongan-potongan itu di akhir.

File header (.h) adalah kontrak bersama di antara potongan-potongan itu: file ini memberi tahu setiap file source apa saja yang ada di tempat lain, tanpa berisi implementasinya.

Deklarasi vs definisi

Seluruh desainnya bertumpu pada satu perbedaan.

Deklarasi mengatakan hal ini ada di suatu tempat dan beginilah bentuknya. Deklarasi tidak menghasilkan kode dan boleh muncul berapa kali pun:

int add(int a, int b);        /* function declaration (prototype) */
extern int error_count;       /* variable declaration */
struct Point { int x, y; };   /* type definition - safe to repeat per file */

Definisi benar-benar membuat hal itu. Definisi harus muncul tepat satu kali di seluruh program:

int add(int a, int b) { return a + b; }   /* function definition */
int error_count = 0;                      /* variable definition */

Header berisi deklarasi. File source berisi definisi. Kalau terbalik, linker akan mengeluh "multiple definition of ...": satu pesan error yang hampir pasti berarti ada definisi yang tersasar ke dalam header.

Program dengan dua file

Ini pembagian terkecil yang berguna. Sebuah header yang mendeklarasikan dua fungsi:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

File source yang mengimplementasikannya. Perhatikan bahwa file ini meng-include header-nya sendiri:

/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

Dan program yang memakainya:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

Kompilasi kedua file source sekaligus:

gcc main.c math_utils.c -o app
./app

Ada dua detail yang perlu diperhatikan. Pertama, math_utils.c meng-include header-nya sendiri, dan itu tidak berlebihan. Dengan begitu compiler memeriksa bahwa setiap definisi cocok dengan deklarasinya. Jadi kalau kamu mengubah prototipe di header dan lupa mengubah file .c, kamu langsung mendapat error, bukan ketidakcocokan saat linking.

Kedua, math_utils.h tidak ada di baris perintah gcc. Header tidak pernah dikompilasi; header ditempelkan ke file .c oleh #include. Memberikan .h ke compiler hanya menghasilkan file precompiled header yang nyasar dan tidak ada kode yang bisa di-link.

Program yang sama dipadatkan ke dalam satu file, supaya kamu bisa menjalankannya di sini:

Deklarasi sebelum main itulah yang disediakan header di versi sebenarnya. Karena itu prototipe fungsi dan header sebenarnya adalah ide yang sama dalam dua skala.

Include guard

#include menempelkan teks, dan teks yang ditempel dua kali berarti teks yang terduplikasi. Itu tidak masalah untuk prototipe fungsi, tapi fatal untuk struct:

/* shapes.h WITHOUT a guard */
struct Point { int x, y; };

Kalau main.c meng-include shapes.h dan canvas.h, sementara canvas.h juga meng-include shapes.h, compiler melihat struct Point didefinisikan dua kali dalam satu translation unit lalu berhenti dengan "redefinition of 'struct Point'". Di proyek nyata, rantai seperti ini bisa begitu dalam sampai kamu tidak bisa melacaknya dengan tangan.

Solusinya adalah include guard: makro yang mencatat "header ini sudah pernah ditempel".

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

Include pertama menemukan SHAPES_H belum terdefinisi, jadi isinya dipertahankan, dan sambil lewat ia mendefinisikan SHAPES_H. Setiap include berikutnya di file yang sama menemukan makro itu sudah terdefinisi dan langsung melompat ke #endif. Nama makronya harus unik di seluruh proyek; FILENAME_H yang diambil dari path adalah konvensi yang biasa.

Alternatif satu barisnya didukung oleh semua compiler utama:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once tidak mungkin bentrok nama dan tidak bisa rusak karena salah ketik di #endif. Kekurangannya hanya satu: ini bukan bagian dari standar C, jadi proyek yang harus bisa di-build di compiler yang tidak umum sebaiknya memakai bentuk #ifndef. Apa pun pilihannya, setiap header harus punya guard. Tanpa kecuali, termasuk header yang kamu kira tidak akan di-include oleh file lain.

Apa yang masuk ke header

Taruh di file .h:

  • Prototipe fungsi
  • Definisi struct, union, dan enum
  • Deklarasi typedef
  • Makro yang memang untuk dibagikan
  • Deklarasi extern untuk variabel global bersama
  • #include yang dibutuhkan header itu sendiri agar bisa berdiri sendiri

Jangan taruh di file .h:

  • Badan fungsi (kecuali memang sengaja static inline)
  • Definisi variabel: int counter; di header mendefinisikan variabel terpisah di setiap file yang meng-include-nya, atau menyebabkan error link, tergantung compiler-nya
  • #include untuk header yang tidak dibutuhkan header itu sendiri: ketergantungan itu jadi terdorong ke semua file di hilir

"Berdiri sendiri" layak dijadikan aturan: header harus tetap bisa dikompilasi saat di-include paling pertama, sebelum apa pun. Kalau shapes.h memakai size_t, header itu meng-include <stddef.h> sendiri, bukan berharap file yang meng-include-nya sudah melakukannya.

Header yang lengkap dan tersusun baik:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* for size_t, used below */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* shared across the program, defined once in inventory.c */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

Berbagi variabel global dengan extern

Variabel global harus didefinisikan tepat di satu file .c dan dideklarasikan di semua tempat lain. extern-lah yang membuatnya jadi deklarasi:

/* inventory.h  - declaration, no storage */
extern int item_count;
/* inventory.c  - the one definition */
#include "inventory.h"
int item_count = 0;
/* main.c - uses it, via the header */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d items\n", item_count);
    return 0;
}

Hapus extern dari header, dan setiap file yang meng-include-nya akan mendefinisikan item_count miliknya sendiri. Paling baik hasilnya error link "multiple definition", paling buruk dua penghitung yang saling terpisah.

Kebutuhan sebaliknya sama seringnya: variabel atau fungsi pembantu yang sebaiknya tetap privat di satu file .c. static di scope file melakukan itu: nama tersebut mendapat internal linkage, tidak terlihat oleh linker dan karena itu juga tidak terlihat oleh file lain:

/* inventory.c */
static Item storage[256];          /* private to this file */
static int  find_slot(const char *name);   /* private helper */

Dua file masing-masing boleh punya static int counter; tanpa bentrok. Inilah versi C dari member privat, dan sebaiknya ini jadi pilihan bawaanmu: hanya yang benar-benar dibutuhkan file lain yang masuk ke header.

Mengompilasi program yang lebih besar

Menulis setiap file di baris perintah memang bekerja, tapi lambat, karena setiap file dikompilasi ulang setiap kali:

gcc main.c inventory.c report.c -o app

Bentuk yang bisa diperbesar adalah mengompilasi setiap source menjadi file objek lalu me-link-nya:

gcc -c main.c        # produces main.o
gcc -c inventory.c   # produces inventory.o
gcc -c report.c      # produces report.o
gcc main.o inventory.o report.o -o app

Sekarang kalau report.c berubah, kamu hanya perlu gcc -c report.c lalu link ulang. Pencatatan seperti inilah yang diotomatiskan oleh make:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

Kalau header-mu ada di subdirektori, -Iinclude menambahkannya ke path pencarian untuk tanda kurung sudut.

Error dan artinya

Kedua jenis kegagalan mudah dibedakan begitu kamu tahu tahap mana yang menghasilkannya.

"undefined reference to 'add'": error linker. Deklarasinya ditemukan, definisinya tidak. Mungkin kamu lupa menulis file .c di baris perintah, atau fungsinya static, atau namanya salah ketik di salah satu dari dua tempat itu.

"multiple definition of 'item_count'": juga error linker, kebalikan dari yang tadi. Ada definisi yang masuk ke header, atau ke dua file source. Pindahkan ke satu file .c dan tinggalkan deklarasi extern di header.

"redefinition of 'struct Item'": error compiler, artinya sebuah header ditempel dua kali ke dalam satu file. Tambahkan include guard.

"implicit declaration of function 'add'": peringatan compiler (error di mode C99 dan sesudahnya) yang artinya prototipenya tidak pernah terlihat. Kamu lupa #include, atau header-nya tidak mendeklarasikan fungsi itu.

Begitu program tersebar di beberapa file, hal berikutnya yang biasanya kamu inginkan adalah mengatur apa yang dikompilasi per platform atau per jenis build, yaitu kompilasi bersyarat.

Pertanyaan yang sering diajukan

Apa yang masuk ke file .h dan apa yang masuk ke file .c?

Header berisi deklarasi: prototipe fungsi, definisi struct dan typedef, enum, makro, dan deklarasi extern untuk variabel global bersama. File .c berisi definisi: badan fungsi dan variabel yang sebenarnya. Pedomannya: header menyebutkan apa yang ada, file source menjelaskan apa yang dilakukannya.

Apa itu include guard dan kenapa perlu dipakai?

#include menempelkan teks, jadi meng-include sebuah header dua kali berarti menempelkan isinya dua kali. Akibatnya setiap struct dan typedef di dalamnya terdefinisi ulang dan kompilasi gagal. Include guard membungkus header dengan #ifndef MYHEADER_H / #define MYHEADER_H / #endif, sehingga include kedua melihat makronya sudah terdefinisi dan melewati isinya.

Bagaimana cara mengompilasi program C yang terdiri dari beberapa file?

Tulis setiap file .c di baris perintah: gcc main.c math_utils.c -o app. Jangan pernah menaruh file .h di sana: header ditempelkan oleh #include, bukan dikompilasi sendiri. Untuk proyek yang lebih besar, kompilasi ke file objek (gcc -c main.c) lalu link semuanya, dan itulah yang diotomatiskan oleh Makefile.

Sebaiknya pakai #pragma once atau include guard #ifndef?

Keduanya bekerja. #pragma once hanya satu baris dan tidak mungkin bentrok nama, dan semua compiler utama mendukungnya, tapi itu bukan bagian dari standar C. Bentuk #ifndef/#define/#endif standar dan bekerja di mana saja. Pilih salah satu dan pakai secara konsisten dalam satu proyek; untuk portabilitas maksimal, pilih bentuk #ifndef.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai