Saat sebuah program C tumbuh melewati 1.000 baris, menempatkan semua kode di satu file .c menjadi tidak realistis: kompilasi lambat, navigasi kode sulit, dan kolaborasi tim mustahil karena semua developer harus mengedit file yang sama. Pola standar C untuk mengatasi ini adalah memecah program menjadi modul—setiap modul adalah pasangan file .h (header, berisi deklarasi publik) dan .c (sumber, berisi implementasi). Bab ini membahas tiga aturan inti modular design di C: header sebagai interface, file sumber sebagai implementation, dan pemisahan simbol publik vs privat melalui keyword static. Kita juga akan melihat pola opaque type untuk menyembunyikan detail struct agar klien tidak dapat mengakses field internal secara langsung.

Aturan Dasar Pasangan .h/.c #

Setiap modul C mengikuti tiga aturan inti:

  1. Header (.h) berisi deklarasi publik: prototype fungsi, deklarasi tipe, makro, dan deklarasi variabel global jika benar-benar diperlukan.
  2. Sumber (.c) berisi implementasi fungsi-fungsi yang dideklarasikan di header, plus fungsi-fungsi helper yang ditandai static agar tidak terlihat di luar modul.
  3. Header harus dapat di-include berkali-kali tanpa error—oleh karena itu setiap header memakai include guard atau #pragma once.

Konvensi penamaan: nama file mencerminkan nama modul. Modul stack ditulis sebagai stack.h dan stack.c. Simbol publik diberi prefix modul: stack_push, stack_pop. Simbol internal (helper) ditandai static sehingga tetap di file objek dan tidak masuk ke tabel simbol global.

Contoh Modul Lengkap: Stack #

Berikut modul stack lengkap yang menerapkan ketiga aturan di atas. Detail implementasi struct disembunyikan dari header—klien hanya memegang pointer ke stack_t tanpa tahu layout field di dalamnya.

c
/* File: stack.h - API publik modul stack */
#ifndef STACK_H
#define STACK_H

#include <stddef.h>

/* Tipe publik: forward declaration ke struct yang detailnya
   disembunyikan di stack.c (opaque type, lihat bab khusus). */
typedef struct stack stack_t;

/* Konstruktor: alokasi stack baru. NULL jika gagal. */
stack_t *stack_buat(size_t kapasitas_awal);

/* Destruktor: bebaskan memori. Set *s = NULL setelah bebas. */
void stack_hancurkan(stack_t **s);

/* Push value ke puncak stack. 0=OK, -1=error (OOM atau NULL). */
int stack_push(stack_t *s, int value);

/* Pop value dari puncak stack. 0=OK, -1=stack kosong atau NULL. */
int stack_pop(stack_t *s, int *out_value);

/* Jumlah elemen saat ini. */
size_t stack_ukuran(const stack_t *s);

#endif /* STACK_H */
c
/* File: stack.c - Implementasi modul stack (private) */
#include "stack.h"
#include <stdlib.h>
#include <string.h>

/* Detail struct disembunyikan dari header. Hanya stack.c yang tahu
   layout lengkap stack_t. Klien hanya memegang pointer. */
struct stack {
    int    *data;
    size_t  kapasitas;
    size_t  ukuran;
};

stack_t *stack_buat(size_t kapasitas_awal)
{
    if (kapasitas_awal == 0) {
        kapasitas_awal = 8;
    }
    stack_t *s = malloc(sizeof(*s));
    if (s == NULL) return NULL;
    s->data = malloc(kapasitas_awal * sizeof(int));
    if (s->data == NULL) {
        free(s);
        return NULL;
    }
    s->kapasitas = kapasitas_awal;
    s->ukuran    = 0;
    return s;
}

void stack_hancurkan(stack_t **s)
{
    if (s == NULL || *s == NULL) return;
    free((*s)->data);
    free(*s);
    *s = NULL;
}

int stack_push(stack_t *s, int value)
{
    if (s == NULL) return -1;
    if (s->ukuran >= s->kapasitas) {
        size_t kap_baru = s->kapasitas * 2;
        int *p = realloc(s->data, kap_baru * sizeof(int));
        if (p == NULL) return -1;
        s->data = p;
        s->kapasitas = kap_baru;
    }
    s->data[s->ukuran++] = value;
    return 0;
}

int stack_pop(stack_t *s, int *out_value)
{
    if (s == NULL || out_value == NULL) return -1;
    if (s->ukuran == 0) return -1;
    *out_value = s->data[--s->ukuran];
    return 0;
}

size_t stack_ukuran(const stack_t *s)
{
    return (s == NULL) ? 0 : s->ukuran;
}
c
/* File: main_stack.c - Klien modul stack */
#include <stdio.h>
#include "stack.h"

int main(void)
{
    stack_t *s = stack_buat(2);
    if (s == NULL) {
        fprintf(stderr, "stack_buat gagal\n");
        return 1;
    }

    /* Push 5 elemen ke stack dengan kapasitas awal 2 - akan realoc */
    for (int i = 10; i <= 50; i += 10) {
        if (stack_push(s, i) != 0) {
            fprintf(stderr, "stack_push gagal pada %d\n", i);
            stack_hancurkan(&s);
            return 1;
        }
        printf("Push %d, ukuran sekarang %zu\n", i, stack_ukuran(s));
    }

    /* Pop semua elemen (LIFO) */
    int v;
    while (stack_pop(s, &v) == 0) {
        printf("Pop  %d, ukuran sekarang %zu\n", v, stack_ukuran(s));
    }

    stack_hancurkan(&s);
    printf("stack setelah hancurkan: %p\n", (void *)s);
    return 0;
}

Compile dengan dua file sumber menjadi satu executable:

bash
gcc -std=c11 -Wall -Wextra -o demo_stack stack.c main_stack.c
./demo_stack

Output yang Dihasilkan:

text
Push 10, ukuran sekarang 1
Push 20, ukuran sekarang 2
Push 30, ukuran sekarang 3
Push 40, ukuran sekarang 4
Push 50, ukuran sekarang 5
Pop  50, ukuran sekarang 4
Pop  40, ukuran sekarang 3
Pop  30, ukuran sekarang 2
Pop  20, ukuran sekarang 1
Pop  10, ukuran sekarang 0
stack setelah hancurkan: (nil)

Mari kita telaah baris-baris kode di atas:

  1. stack.h baris 4-5 (#ifndef STACK_H / #define STACK_H): Include guard klasik. Jika stack.h di-include dua kali (langsung maupun transitif melalui header lain), baris-baris antara #ifndef dan #endif hanya diproses sekali. Ini mencegah error “redefinition of struct” yang akan terjadi pada file header besar.
  2. stack.h baris 10 (typedef struct stack stack_t;): Forward declaration. Header mengatakan “ada struct bernama stack, dan namanya stack_t”, tanpa membongkar isi struct. Inilah inti opaque type—klien tidak bisa mengakses s->data atau s->kapasitas karena layout tidak terlihat di header.
  3. stack.h baris 13-25: Prototype fungsi publik. Setiap fungsi diberi prefix stack_ untuk menghindari bentrokan dengan modul lain. Konvensi penamaan parameter (kapasitas_awal, out_value) menjelaskan peran tanpa komentar tambahan.
  4. stack.c baris 9-13 (struct stack { ... };): Definisi lengkap struct hanya ada di file .c. Klien yang meng-include stack.h tidak tahu field apa saja di dalamnya. Jika kita mengubah layout struct (mis. menambah field peek_count untuk statistik), semua klien tidak perlu di-recompile—hanya stack.c yang perlu di-compile ulang.
  5. stack.c baris 16-28 (stack_buat): Konstruktor yang mengalokasikan struct dan buffer data. Pola dua tahap (malloc struct lalu malloc data) penting karena struct dan array biasanya punya lifetime berbeda—realloc pada array tidak boleh mengubah pointer struct.
  6. stack.c baris 31-36 (stack_hancurkan): Menerima stack_t ** (pointer ke pointer) agar dapat meng-set *s = NULL di pemanggil. Pola ini mencegah use-after-free: jika klien lupa bahwa s sudah dibebaskan, NULL dereference akan crash cepat alih-alih merusak silently.
  7. main_stack.c: Klien hanya memakai API publik. Tidak ada #include <stdlib.h> atau #include <string.h> di sini karena klien tidak perlu tahu bahwa implementasi stack_t memakai malloc.

Public vs Private API #

Pemisahan publik vs privat di C dicapai melalui tiga mekanisme:

MekanismeVisibilitasContoh
Deklarasi di headerPublik (dapat di-include siapa saja)stack_t, stack_push
Helper dengan staticPrivat (hanya di file .c itu)static int grow(stack_t*)
Macro tanpa staticBisa di-include tetapi tidak punya alamat#define ARR_LEN(arr) ...

Berikut demonstrasi tambahan yang menunjukkan bagaimana satu file tunggal dapat memakai pola header inline sederhana:

c
/* File: test_modul2.c - Pola include guard dan header library */
#include <stdio.h>

/* Contoh header inline yang menunjukkan include guard dan deklarasi */
#define VERSION_H
#ifdef VERSION_H
static const char *APP_VERSION = "1.2.3";
static const char *APP_NAME    = "demo-modul";
#endif

/* Fungsi pembungkus untuk menampilkan info modul */
static void tampilkan_info(void)
{
    printf("%s versi %s\n", APP_NAME, APP_VERSION);
    printf("Kompilasi: %s %s\n", __DATE__, __TIME__);
    printf("Standard C: %ld\n", __STDC_VERSION__);
}

int main(void)
{
    tampilkan_info();
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o test_modul2 test_modul2.c
./test_modul2

Output yang Dihasilkan:

text
demo-modul versi 1.2.3
Kompilasi: Oct 13 2026 09:23:34
Standard C: 201112

Nilai __DATE__, __TIME__, dan __STDC_VERSION__ adalah predefined macro yang di-substitusi preprocessor. __STDC_VERSION__ bernilai 201112L untuk C11, 201710L untuk C17, dan 202311L untuk C23. Macro ini berguna untuk logging versi build dan kompabilitas standar.

Aturan Tambahan untuk Modul yang Sehat #

Beberapa aturan praktis yang kami ikuti di proyek C:

  • Satu modul = satu file .c. Jangan menggabungkan dua modul (stack dan queue) ke file containers.c. Pemisahan fisik membantu navigasi dan build incremental.
  • Header tidak boleh berisi definisi fungsi kecuali static inline yang sengaja untuk optimasi. Definisi fungsi biasa di header menyebabkan multiple definition error saat linking dua file yang meng-include header tersebut.
  • Header tidak boleh #include header lain yang tidak strictly diperlukan. Mis. stack.h hanya butuh <stddef.h> untuk size_t; jangan #include <stdio.h> hanya karena stack.c memakainya. Hal ini mengurangi waktu kompilasi.
  • Header memakai include guard (#ifndef MODUL_H … #define MODUL_H … #endif) atau #pragma once. Standar C23 menambahkan #pragma once sebagai standar, tetapi compiler mayoritas sudah mendukungnya sejak lama.
  • File .c meng-include header sendiri pertama kali. Pola ini menjamin bahwa header bersifat self-contained—tidak menyembunyikan dependency ke file lain. Jika stack.h ternyata butuh #include <stdlib.h> tetapi lupa mendeklarasikannya, error akan muncul saat stack.c (yang meng-include stack.h duluan) dikompilasi.