Di C, encapsulation dicapai dengan menyembunyikan definisi struct dari header publik. Teknik ini disebut opaque type: header hanya berisi forward declaration typedef struct nama nama_t;, sementara definisi lengkap struct nama { ... }; hanya ada di file .c. Klien yang meng-include header tidak dapat mengakses field internal maupun menghitung sizeof(nama_t)—mereka hanya pegang pointer dan memanipulasi objek melalui fungsi API. Bab ini menunjukkan dua pola opaque yang umum di C: struct pointer dengan layout tersembunyi, dan handle integer yang dipakai POSIX untuk file descriptor dan socket. Kita juga akan melihat tiga keuntungan konkret dari opaque: encapsulation, ABI stability, dan faster incremental build.

Mengapa Opaque Type Bernilai #

Bayangkan sebuah modul akun yang langsung membuka field ke klien:

c
/* Header non-opaque (BURUK untuk encapsulation) */
typedef struct {
    char   *nama;
    double  saldo;
} akun_t;

Deklarasi ini mempublikasikan dua field ke seluruh kode yang meng-include header. Akibatnya:

  1. Klien dapat langsung menulis a->saldo = -1000; untuk mengeset saldo negatif, melewati validasi yang seharusnya ada di akun_tarik.
  2. Jika kita ingin mengubah tipe saldo dari double menjadi long (untuk akurasi), semua klien yang mengakses a->saldo harus diubah dan di-compile ulang.
  3. sizeof(akun_t) bocor ke klien. Jika ada yang mengalokasikan akun_t di_stack[10];, ukuran array menjadi dependen pada layout internal.

Opaque type menghindari ketiga masalah ini. Mari kita lihat implementasinya.

Pola 1: Forward Declaration di Header, Definisi di .c #

Berikut modul akun dengan opaque type. Detail struct hanya ada di akun.c—klien hanya melihat forward declaration di akun.h.

c
/* File: akun.h - Header publik dengan opaque type */
#ifndef AKUN_H
#define AKUN_H

#include <stddef.h>

/* Forward declaration - detail struct disembunyikan.
   Klien hanya pegang pointer; tidak bisa akses field internal. */
typedef struct akun akun_t;

/* Konstruktor: buat akun baru. NULL jika gagal. */
akun_t *akun_buat(const char *nama, double saldo_awal);

/* Destruktor: bebaskan akun. Set *a = NULL setelah dibebaskan. */
void akun_hancurkan(akun_t **a);

/* Mutator: setor tarik. 0=OK, -1=error (saldo negatif). */
int akun_setor(akun_t *a, double jumlah);
int akun_tarik(akun_t *a, double jumlah);

/* Aksesor: baca saldo dan nama (tidak bisa diubah). */
double       akun_saldo(const akun_t *a);
const char   *akun_nama(const akun_t *a);

#endif /* AKUN_H */
c
/* File: akun.c - Implementasi dengan struct lengkap (private) */
#include "akun.h"
#include <stdlib.h>
#include <string.h>

/* Detail struct hanya ada di file .c. Header hanya forward-declare.
   Klien yang #include "akun.h" tidak bisa mengakses field nama/saldo. */
struct akun {
    char   *nama;
    double  saldo;
};

akun_t *akun_buat(const char *nama, double saldo_awal)
{
    if (nama == NULL || saldo_awal < 0) return NULL;
    akun_t *a = malloc(sizeof(*a));
    if (a == NULL) return NULL;
    a->nama = malloc(strlen(nama) + 1);
    if (a->nama == NULL) {
        free(a);
        return NULL;
    }
    strcpy(a->nama, nama);
    a->saldo = saldo_awal;
    return a;
}

void akun_hancurkan(akun_t **a)
{
    if (a == NULL || *a == NULL) return;
    free((*a)->nama);
    free(*a);
    *a = NULL;
}

int akun_setor(akun_t *a, double jumlah)
{
    if (a == NULL || jumlah <= 0) return -1;
    a->saldo += jumlah;
    return 0;
}

int akun_tarik(akun_t *a, double jumlah)
{
    if (a == NULL || jumlah <= 0) return -1;
    if (a->saldo < jumlah) return -1;
    a->saldo -= jumlah;
    return 0;
}

double akun_saldo(const akun_t *a)
{
    return (a == NULL) ? 0.0 : a->saldo;
}

const char *akun_nama(const akun_t *a)
{
    return (a == NULL) ? NULL : a->nama;
}
c
/* File: main_akun.c - Klien memakai akun_t tanpa tahu layout struct */
#include <stdio.h>
#include "akun.h"

int main(void)
{
    akun_t *a = akun_buat("Budi", 100000.0);
    if (a == NULL) {
        fprintf(stderr, "akun_buat gagal\n");
        return 1;
    }
    printf("Awal: %-6s saldo %.2f\n", akun_nama(a), akun_saldo(a));

    akun_setor(a, 50000.0);
    printf("Setor 50000 -> saldo %.2f\n", akun_saldo(a));

    if (akun_tarik(a, 30000.0) == 0) {
        printf("Tarik 30000 -> saldo %.2f\n", akun_saldo(a));
    }
    if (akun_tarik(a, 999999.0) != 0) {
        printf("Tarik 999999 ditolak (saldo kurang)\n");
    }

    /* Baris berikut tidak akan compile karena a->saldo tidak terlihat:
       a->saldo = 0;  // error: dereferencing pointer to incomplete type
    */

    akun_hancurkan(&a);
    printf("Setelah hancurkan: %p\n", (void *)a);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o demo_akun akun.c main_akun.c
./demo_akun

Output yang Dihasilkan:

text
Awal: Budi   saldo 100000.00
Setor 50000 -> saldo 150000.00
Tarik 30000 -> saldo 120000.00
Tarik 999999 ditolak (saldo kurang)
Setelah hancurkan: (nil)

Mari kita telaah baris-baris kode di atas:

  1. akun.h baris 10 (typedef struct akun akun_t;): Forward declaration. Header mengatakan “ada struct bernama akun, dan namanya akun_t”, tetapi tidak membuka isi struct. Klien dapat mendeklarasikan akun_t *a; (pointer) tetapi tidak akun_t a; (obyek nilai) atau sizeof(akun_t) (kompilasi error: incomplete type).
  2. akun.c baris 9-12 (struct akun { ... };): Definisi lengkap struct hanya di file .c. Compiler mengisi sizeof(akun_t) di sini tetapi nilai ini tidak bocor ke file lain. Jika kita menambah field int transaksi_count; untuk audit, klien tidak perlu di-compile ulang—akun.c saja yang di-compile.
  3. akun.c baris 33-39 (akun_hancurkan): Destruktor menerima akun_t ** (pointer ke pointer) agar dapat meng-set *a = NULL di pemanggil. Pola ini mencegah use-after-free—jika klien mencoba akun_saldo(a) setelah akun_hancurkan(&a), kode crash dengan NULL deref yang langsung terlihat di debugger, bukan corrupting heap secara silent.
  4. main_akun.c: Klien hanya memakai fungsi API. Komentar baris 24 menunjukkan bahwa a->saldo = 0; tidak akan compile—enkapsulasi dipaksakan oleh compiler, bukan diserahkan ke disiplin programmer.

Pola 2: Handle Integer (POSIX Style) #

Pola kedua yang umum di C adalah handle: klien tidak memegang pointer sama sekali, melainkan integer yang menjadi index ke tabel internal modul. POSIX memakai pola ini untuk file descriptor (int fd) dan socket (int sock). Keuntungannya: handle tidak bisa di-dereference (integer tidak punya field), dan tabel dapat direlokasi tanpa memutus klien.

c
/* File: test_opaque2.c - Opaque dengan handle pattern (id integer) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Pola handle: alih-alih pointer ke struct opaque, klien pegang
   integer index ke tabel internal. Pola ini dipakai POSIX (int fd
   untuk file descriptor, int socket untuk socket). */

#define MAX_FD 8

typedef struct {
    int   used;
    char  nama[32];
    int   umur;
} person_t;

static person_t g_table[MAX_FD];

/* "Buka" person baru - return handle (>=0) atau -1 jika tabel penuh */
static int person_open(const char *nama, int umur)
{
    for (int i = 0; i < MAX_FD; i++) {
        if (!g_table[i].used) {
            strncpy(g_table[i].nama, nama, sizeof(g_table[i].nama) - 1);
            g_table[i].nama[sizeof(g_table[i].nama) - 1] = '\0';
            g_table[i].umur = umur;
            g_table[i].used = 1;
            return i;
        }
    }
    return -1;
}

static void person_close(int h)
{
    if (h >= 0 && h < MAX_FD) {
        g_table[h].used = 0;
    }
}

static const char *person_nama(int h)
{
    if (h < 0 || h >= MAX_FD || !g_table[h].used) return NULL;
    return g_table[h].nama;
}

static int person_umur(int h)
{
    if (h < 0 || h >= MAX_FD || !g_table[h].used) return -1;
    return g_table[h].umur;
}

int main(void)
{
    int h1 = person_open("Siti", 28);
    int h2 = person_open("Budi", 32);

    printf("h1=%d nama=%s umur=%d\n", h1, person_nama(h1), person_umur(h1));
    printf("h2=%d nama=%s umur=%d\n", h2, person_nama(h2), person_umur(h2));

    person_close(h1);
    printf("Setelah close h1, nama h1 = %s\n",
           person_nama(h1) == NULL ? "(NULL)" : person_nama(h1));

    person_close(h2);
    return 0;
}

Output yang Dihasilkan:

text
h1=0 nama=Siti umur=28
h2=1 nama=Budi umur=32
Setelah close h1, nama h1 = (NULL)

Pola handle memberi beberapa keuntungan tambahan dibandingkan pointer opaque:

  • Tidak bisa di-dereference: h1->nama adalah syntax error karena h1 bertipe int. Encapsulation lebih kuat daripada opaque pointer.
  • Reusable: ketika person_close(h1) dipanggil, slot 0 di tabel kembali kosong. person_open berikutnya dapat memakai slot yang sama. Pointer opaque tidak otomatis ter-recycle.
  • Validasi mudah: cek if (h >= 0 && h < MAX_FD && g_table[h].used) cukup untuk memastikan handle valid. Pointer opaque memerlukan tracking metadata tambahan untuk membedakan pointer valid dari pointer dangling.

Kerugiannya: tabel global membatasi jumlah objek aktif ke MAX_FD. Untuk skala besar, dapat di-upgrade ke tabel hash atau dynamic array tanpa memutus API klien—pola handle tetap integer.