Membangun modul C berarti merancang satu unit .h + .c yang dapat dipakai ulang di banyak program. Modul yang baik memiliki interface publik yang jelas, implementation tersembunyi, manajemen sumber daya yang aman, dan kode yang dapat diuji secara terpisah. Setelah mempelajari include guard, separate compilation, linkage, dan extern pada chapter-chapter sebelumnya, sekarang saatnya menyatukan semuanya menjadi satu modul lengkap. Pada chapter ini kita akan merancang modul Stack dengan opaque pointer—pola idiomatik C untuk encapsulation penuh—lalu membangun modul tersebut menjadi executable.

Pola Opaque Pointer untuk Encapsulation #

Pola opaque pointer menyembunyikan detail struct dari header. Header hanya menampilkan typedef struct Stack Stack;—deklarasi forward tanpa definisi isi. Pengguna modul hanya memegang pointer ke Stack dan tidak dapat mengakses field internal karena compiler tidak tahu ukuran atau isi struct Stack. Definisi lengkap struct Stack { ... }; hanya muncul di file .c.

Keuntungan pola ini:

  1. Encapsulation penuh: pengguna tidak dapat mengakses field internal data, kapasitas, atau top dari luar file stack.c.
  2. Stabilitas ABI: jika kita mengubah isi struct Stack (mis. menambah field baru), pengguna modul tidak perlu dikompilasi ulang karena ukuran struct Stack* tetap sama (pointer).
  3. Fleksibilitas implementasi: kita dapat mengganti implementasi array dinamis dengan linked list tanpa mengubah header.

Mekanisme kerja di balik layar: pengguna modul memegang pointer ke Stack—pointer bertipe Stack*. Karena tipe Stack tidak lengkap di header, pengguna tidak dapat membuat variabel Stack s; secara langsung (compiler tidak tahu ukurannya). Pengguna wajib memanggil stack_create() untuk mendapatkan pointer ke objek yang dialokasikan di file stack.c, di mana definisi lengkap struct Stack terlihat.

Implementasi Modul Stack #

File stack.h:

c
#ifndef STACK_H
#define STACK_H

#include <stddef.h>
#include <stdbool.h>

/* Opaque handle -- detail implementasi tersembunyi */
typedef struct Stack Stack;

/* API publik */
Stack *stack_create(size_t kapasitas);
void   stack_destroy(Stack *s);
bool   stack_push(Stack *s, int nilai);
bool   stack_pop(Stack *s, int *out);
size_t stack_size(const Stack *s);

#endif /* STACK_H */

File stack.c:

c
#include "stack.h"
#include <stdlib.h>

/* Definisi tipe lengkap -- hanya terlihat di file ini */
struct Stack {
    int   *data;
    size_t kapasitas;
    size_t top;
};

Stack *stack_create(size_t kapasitas) {
    Stack *s = malloc(sizeof(Stack));
    if (!s) return NULL;

    s->data = malloc(kapasitas * sizeof(int));
    if (!s->data) {
        free(s);
        return NULL;
    }
    s->kapasitas = kapasitas;
    s->top = 0;
    return s;
}

void stack_destroy(Stack *s) {
    if (!s) return;
    free(s->data);
    free(s);
}

bool stack_push(Stack *s, int nilai) {
    if (!s || s->top >= s->kapasitas) return false;
    s->data[s->top++] = nilai;
    return true;
}

bool stack_pop(Stack *s, int *out) {
    if (!s || s->top == 0) return false;
    *out = s->data[--s->top];
    return true;
}

size_t stack_size(const Stack *s) {
    return s ? s->top : 0;
}

File main.c:

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

int main(void) {
    Stack *s = stack_create(5);
    if (!s) {
        fprintf(stderr, "Gagal membuat stack\n");
        return 1;
    }

    for (int i = 10; i <= 50; i += 10) {
        if (stack_push(s, i)) {
            printf("Push: %d (size=%zu)\n", i, stack_size(s));
        }
    }

    int nilai;
    while (stack_pop(s, &nilai)) {
        printf("Pop:  %d (size=%zu)\n", nilai, stack_size(s));
    }

    stack_destroy(s);
    return 0;
}

Kompilasi dan jalankan:

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

Output yang Dihasilkan:

text
Push: 10 (size=1)
Push: 20 (size=2)
Push: 30 (size=3)
Push: 40 (size=4)
Push: 50 (size=5)
Pop:  50 (size=4)
Pop:  40 (size=3)
Pop:  30 (size=2)
Pop:  20 (size=1)
Pop:  10 (size=0)

Perhatikan bahwa Stack berperilaku seperti tipe data abstrak: pengguna hanya berinteraksi melalui lima fungsi publik. Urutan pop bersifat LIFO (Last-In-First-Out)—elemen terakhir yang dimasukkan (50) adalah yang pertama keluar—sesuai dengan semantik struktur data stack.

Mari kita telaah baris-baris kunci di atas:

  1. Pada stack.h baris 8 (typedef struct Stack Stack;): opaque pointer pattern. Kita mendeklarasikan tipe Stack sebagai alias ke struct Stack yang tidak lengkap. Pengguna header hanya melihat tipe pointer Stack*—tidak bisa membuat variabel Stack s; karena ukuran struct Stack tidak diketahui.
  2. Pada stack.h baris 11-15 (API publik): lima fungsi yang membentuk kontrak modul. stack_create adalah factory yang mengalokasikan objek. stack_destroy adalah destructor yang membebaskan memori. stack_push dan stack_pop memodifikasi state. stack_size adalah query read-only (ditandai const Stack *).
  3. Pada stack.c baris 6-10 (struct Stack { ... };): definisi lengkap tipe. Field data adalah array dinamis, kapasitas adalah batas maksimum, top adalah indeks slot kosong berikutnya. Definisi ini hanya terlihat di file stack.c.
  4. Pada stack.c baris 12-22 (stack_create): factory function. Alokasi dua tahap—pertama malloc(sizeof(Stack)) untuk struct header, lalu malloc(kapasitas * sizeof(int)) untuk array data. Jika alokasi kedua gagal, kita bebaskan alokasi pertama untuk menghindari memory leak. Pola ini disebut two-phase allocation dan merupakan praktik wajib.
  5. Pada stack.c baris 24-28 (stack_destroy): destructor. Membebaskan array data dulu, lalu struct. Pemeriksaan if (!s) return; melindungi dari free(NULL) yang sebenarnya aman di C, tetapi pola defensif ini umum dipakai untuk kejelasan.
  6. Pada stack.c baris 30-34 (stack_push): penambahan elemen. Pemeriksaan s->top >= s->kapasitas mencegah buffer overflow. Jika berhasil, nilai disalin ke s->data[s->top] lalu s->top dinaikkan.
  7. Pada stack.c baris 36-40 (stack_pop): pengambilan elemen. Pre-decrement --s->top memindahkan indeks ke elemen paling atas. Out-parameter *out menerima nilai yang diambil, sehingga pemanggil dapat memeriksa keberhasilan via return value bool sekaligus menerima data via pointer.
  8. Pada main.c baris 7 (Stack *s = stack_create(5);): pengguna memegang pointer ke objek yang dialokasikan modul. Pengguna tidak tahu (dan tidak perlu tahu) bagaimana struct Stack terstruktur.
  9. Pada main.c baris 22 (stack_destroy(s);): pemanggilan destructor setelah selesai. Pola create-use-destroy adalah ciri khas API C berbasis opaque pointer.

Membangun Modul sebagai Static Library #

Setelah modul berfungsi, kita dapat membangunnya menjadi static library untuk dipakai ulang di banyak program tanpa kompilasi ulang. Alur kerjanya:

bash
# Kompilasi stack.c menjadi object file
gcc -std=c11 -Wall -Wextra -c stack.c -o stack.o

# Arsipkan ke static library libstack.a
ar rcs libstack.a stack.o

# Program lain dapat meng-link dengan libstack.a
gcc -std=c11 -Wall -Wextra main.c -L. -lstack -o app

Distribusi modul cukup memberikan dua file: stack.h (header publik) dan libstack.a (object file terkompilasi). Pengguna tidak perlu melihat stack.c; modul benar-benar encapsulated.

Praktik yang Membuat Modul Bertahan Lama #

Beberapa praktik yang membedakan modul C yang baik dari modul sekadar jalan:

  1. API minimal: hanya ekspos fungsi yang benar-benar dipakai pengguna. Helper internal wajib static.
  2. Dokumentasi kontrak: setiap fungsi publik wajib menjelaskan precondition, postcondition, dan ownership memori (siapa yang harus membebaskan hasil malloc).
  3. Error handling konsisten: gunakan bool/int return code untuk fungsi yang dapat gagal, dan NULL untuk factory yang gagal alokasi.
  4. Const-correctness: parameter const Stack * pada stack_size memberi sinyal fungsi tidak memodifikasi state.
  5. Tes terpisah: tulis file test_stack.c yang menguji API secara menyeluruh sebelum modul dirilis.