Pemisahan file .c (source) dan .h (header) adalah konvensi inti dalam proyek C berskala menengah ke atas. File .c berisi definisi—implementasi fungsi, variabel global, dan kode program. File .h berisi deklarasi—kontrak publik yang dilihat pengguna modul. Memahami peran kedua jenis file ini penting karena pola yang sama dipakai oleh hampir seluruh pustaka C, mulai dari standar pustaka sampai framework besar seperti OpenSSL atau SQLite. Pada chapter ini kita akan membedakan tugas kedua jenis file, melihat peran static helper yang disembunyikan di file .c, dan menyusun satu modul teks lengkap dengan API publik dan helper internal.

Peran File .c dan .h #

File .c adalah tempat kode program berada. Setiap fungsi yang dipakai pengguna modul maupun fungsi helper internal didefinisikan di sini. File .c boleh berisi variabel global yang dapat diakses lintas file (dengan bantuan extern di header), tetapi harus didefinisikan di satu tempat saja. Setiap file .c adalah satu translation unit terpisah yang dikompilasi menjadi satu object file (.o).

File .h adalah interface publik. File ini berisi deklarasi fungsi publik, definisi tipe publik, dan macro publik. File .h tidak boleh berisi definisi fungsi non-inline atau definisi variabel karena akan menyebabkan multiple definition saat di-include oleh banyak file .c. File .h juga tempat mekanisme include guard atau `#pragma once* berada.

Mekanisme kerja kedua jenis file terhubung saat compiler memproses file .c: preprocessor menyisipkan isi header yang di-include ke dalam translation unit file .c, lalu compiler memprosesnya menjadi object file. Linker kemudian menyatukan object file dari berbagai file .c menjadi satu executable. Karena header tidak dikompilasi terpisah, perubahan di header memaksa rekompilasi semua file .c yang meng-include header tersebut.

Demonstrasi: Modul teks #

File teks.h berisi deklarasi tiga fungsi publik. File teks.c berisi definisi fungsi-fungsi tersebut plus sebuah helper internal cetak_dengan_prefix yang tidak pernah diekspos ke pengguna modul.

File teks.h:

c
#ifndef TEKS_H
#define TEKS_H

/* Deklarasi publik -- kontrak interface */
void cetak_tebal(const char *s);
void cetak_miring(const char *s);
int  panjang(const char *s);

#endif

File teks.c:

c
#include <stdio.h>
#include "teks.h"

/* Helper internal (static, tidak keluar file) */
static void cetak_dengan_prefix(const char *prefix, const char *s) {
    printf("[%s] %s\n", prefix, s);
}

void cetak_tebal(const char *s) {
    cetak_dengan_prefix("BOLD", s);
}

void cetak_miring(const char *s) {
    cetak_dengan_prefix("ITALIC", s);
}

int panjang(const char *s) {
    int n = 0;
    while (s[n] != '\0') n++;
    return n;
}

File main.c:

c
#include <stdio.h>
#include "teks.h"

int main(void) {
    const char *pesan = "Halo Dunia";
    cetak_tebal(pesan);
    cetak_miring(pesan);
    printf("panjang(\"%s\") = %d\n", pesan, panjang(pesan));
    return 0;
}

Kompilasi dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -I. -o app main.c teks.c && ./app

Output yang Dihasilkan:

text
[BOLD] Halo Dunia
[ITALIC] Halo Dunia
panjang("Halo Dunia") = 10

Perhatikan bahwa main.c hanya meng-include teks.h, bukan <stdio.h>—kita sengaja meng-include <stdio.h> di main.c karena main.c memakai printf langsung. Implementasi cetak_dengan_prefix di teks.c juga memakai printf, tetapi <stdio.h> hanya di-include di teks.c, bukan di header. Pemisahan ini menjaga teks.h tetap minimal.

Mari kita telaah baris-baris kunci di atas:

  1. Baris 1-3 pada teks.c (#include <stdio.h> dan #include "teks.h"): urutan import biasanya header sistem dulu, lalu header lokal. teks.c membutuhkan <stdio.h> untuk printf dan "teks.h" agar tanda tangan fungsi yang didefinisikan cocok dengan deklarasi publik di header.
  2. Baris 6-8 pada teks.c (static void cetak_dengan_prefix(...)): fungsi helper internal. Keyword static di sini memberi fungsi tersebut internal linkage—artinya tidak terlihat dari file .c lain saat linking. Fungsi cetak_dengan_prefix tidak dideklarasikan di teks.h, sehingga pengguna modul tidak dapat memanggilnya.
  3. Baris 10-12 (void cetak_tebal(const char *s)): definisi fungsi publik. Tanda tangan harus persis cocok dengan deklarasi di header. Jika tidak cocok, compiler melaporkan error konflik tipe.
  4. Baris 16-20 (int panjang(...)): implementasi sederhana menghitung panjang string. Fungsi ini tidak membutuhkan <stdio.h> secara langsung—tidak ada printf di dalamnya. Jika kita memisah panjang ke file teks_util.c yang terpisah, kita bisa saja memilih untuk tidak meng-include <stdio.h> di file tersebut.
  5. Pada main.c baris 2 (#include "teks.h"): main.c hanya melihat tiga fungsi publik. Ia tidak tahu adanya cetak_dengan_prefix. Pemisahan ini memberi encapsulation ala OOP: detail implementasi tersembunyi.

Membuktikan static Menyembunyikan Helper #

Untuk memperjelas efek static, mari kita coba memanggil cetak_dengan_prefix dari file lain. Karena fungsi tidak dideklarasikan di header, compiler akan melaporkan implicit declaration error:

c
#include <stdio.h>
#include "teks.h"

int main(void) {
    /* Mencoba memanggil helper internal -- seharusnya error */
    cetak_dengan_prefix("X", "Y");
    return 0;
}

Output yang Dihasilkan saat kompilasi:

text
bad_main.c:6:5: error: implicit declaration of function 'cetak_dengan_prefix'
   6 |     cetak_dengan_prefix("X", "Y");
      |     ^~~~~~~~~~~~~~~~~~~

Compiler menolak karena cetak_dengan_prefix tidak dideklarasikan di header mana pun. Inilah cara C mencapai information hiding: fungsi atau variabel static hanya terlihat dalam translation unit tempat ia didefinisikan. Bahkan jika kita menambahkan deklarasi cetak_dengan_prefix secara manual di bad_main.c, linker akan melaporkan undefined reference karena internal linkage menghalangi simbol dilihat dari object file lain.

Praktik Penamaan dan Struktur Direktori #

Untuk proyek dengan beberapa modul, konvensi umum adalah menempatkan semua file .c dan .h di direktori terpisah. Misalnya:

text
myapp/
├── include/        # header publik (diakses oleh pengguna modul)
│   ├── teks.h
│   └── math_ops.h
├── src/            # source file (implementasi)
│   ├── teks.c
│   └── math_ops.c
└── main.c          # entry point

Saat kompilasi, flag -Iinclude memberitahu GCC untuk mencari header di folder include/. Struktur seperti ini umum dijumpai di proyek open-source dan memudahkan distribusi: pengguna hanya perlu mengakses include/ dan berkas object file (atau static library .a), tanpa harus membaca src/.