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:
/* 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:
- Klien dapat langsung menulis
a->saldo = -1000;untuk mengeset saldo negatif, melewati validasi yang seharusnya ada diakun_tarik. - Jika kita ingin mengubah tipe
saldodaridoublemenjadilong(untuk akurasi), semua klien yang mengaksesa->saldoharus diubah dan di-compile ulang. sizeof(akun_t)bocor ke klien. Jika ada yang mengalokasikanakun_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.
/* 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 *//* 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;
}/* 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:
gcc -std=c11 -Wall -Wextra -o demo_akun akun.c main_akun.c
./demo_akunOutput yang Dihasilkan:
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:
akun.hbaris 10 (typedef struct akun akun_t;): Forward declaration. Header mengatakan “ada struct bernamaakun, dan namanyaakun_t”, tetapi tidak membuka isi struct. Klien dapat mendeklarasikanakun_t *a;(pointer) tetapi tidakakun_t a;(obyek nilai) atausizeof(akun_t)(kompilasi error: incomplete type).akun.cbaris 9-12 (struct akun { ... };): Definisi lengkap struct hanya di file.c. Compiler mengisisizeof(akun_t)di sini tetapi nilai ini tidak bocor ke file lain. Jika kita menambah fieldint transaksi_count;untuk audit, klien tidak perlu di-compile ulang—akun.csaja yang di-compile.akun.cbaris 33-39 (akun_hancurkan): Destruktor menerimaakun_t **(pointer ke pointer) agar dapat meng-set*a = NULLdi pemanggil. Pola ini mencegah use-after-free—jika klien mencobaakun_saldo(a)setelahakun_hancurkan(&a), kode crash dengan NULL deref yang langsung terlihat di debugger, bukan corrupting heap secara silent.main_akun.c: Klien hanya memakai fungsi API. Komentar baris 24 menunjukkan bahwaa->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.
/* 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:
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->namaadalah syntax error karenah1bertipeint. Encapsulation lebih kuat daripada opaque pointer. - Reusable: ketika
person_close(h1)dipanggil, slot 0 di tabel kembali kosong.person_openberikutnya 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.