C tidak punya keyword private atau class seperti C++/Java, tetapi kita dapat mencapai encapsulation lewat pola opaque pointer. Ide dasarnya: di header publik, kita hanya menyebutkan nama struct tanpa body (typedef struct Buffer Buffer;). Konsumen hanya memegang pointer ke Buffer dan tidak dapat mengakses field-nya karena compiler tidak tahu layout struct tersebut. Definisi lengkap struct disimpan di file .c yang tidak di-include oleh konsumen. Pola ini dipakai oleh hampir semua library C besar (glibc, SQLite, libcurl, OpenSSL) karena tiga alasan: (1) menyembunyikan detail implementasi, (2) memungkinkan perubahan layout struct tanpa recompile konsumen, dan (3) memudahkan FFI karena konsumen hanya berurusan dengan pointer.
Pola Opaque Pointer: Header dan Implementasi #
Pola opaque pointer membagi kode menjadi dua: header publik (op_buffer.h) yang menyatakan kontrak API, dan file implementasi (op_buffer.c) yang berisi definisi struct dan kode fungsi.
/* File: op_buffer.h - hanya forward declaration, struct disembunyikan */
#ifndef BUFFER_H
#define BUFFER_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Forward declaration: nama struct tanpa body */
typedef struct Buffer Buffer;
/* API publik: konstruksi, destruksi, operasi */
Buffer *buffer_buat(size_t kapasitas_awal);
void buffer_hancurkan(Buffer *buf);
int buffer_tambah(Buffer *buf, unsigned char byte);
size_t buffer_panjang(const Buffer *buf);
size_t buffer_kapasitas(const Buffer *buf);
const unsigned char *buffer_data(const Buffer *buf);
#ifdef __cplusplus
}
#endif
#endif /* BUFFER_H *//* File: op_buffer.c - definisi lengkap struct hanya ada di sini */
#include "op_buffer.h"
#include <stdlib.h>
#include <string.h>
struct Buffer {
unsigned char *data;
size_t panjang;
size_t kapasitas;
};
Buffer *buffer_buat(size_t kapasitas_awal) {
Buffer *buf = (Buffer *) malloc(sizeof(Buffer));
if (!buf) return NULL;
buf->data = (unsigned char *) malloc(kapasitas_awal > 0 ? kapasitas_awal : 16);
if (!buf->data) { free(buf); return NULL; }
buf->panjang = 0;
buf->kapasitas = kapasitas_awal > 0 ? kapasitas_awal : 16;
return buf;
}
void buffer_hancurkan(Buffer *buf) {
if (!buf) return;
free(buf->data);
free(buf);
}
int buffer_tambah(Buffer *buf, unsigned char byte) {
if (!buf) return -1;
if (buf->panjang == buf->kapasitas) {
size_t baru = buf->kapasitas * 2;
unsigned char *nd = (unsigned char *) realloc(buf->data, baru);
if (!nd) return -1;
buf->data = nd;
buf->kapasitas = baru;
}
buf->data[buf->panjang++] = byte;
return 0;
}
size_t buffer_panjang(const Buffer *buf) {
return buf ? buf->panjang : 0;
}
size_t buffer_kapasitas(const Buffer *buf) {
return buf ? buf->kapasitas : 0;
}
const unsigned char *buffer_data(const Buffer *buf) {
return buf ? buf->data : NULL;
}Mengonsumsi API dari Main #
Dari sisi pengguna, struct Buffer adalah tipe yang tidak lengkap (incomplete type). Pengguna hanya memegang pointer dan memanggil fungsi API. Kompilasi berhasil tanpa pengguna perlu tahu field apa saja di dalam struct.
/* File: op_main.c - pengguna hanya mengenal pointer, bukan struct */
#include <stdio.h>
#include "op_buffer.h"
int main(void) {
Buffer *buf = buffer_buat(4); /* kapasitas awal 4 */
if (!buf) { fprintf(stderr, "buffer_buat gagal\n"); return 1; }
/* Tambah 10 byte; akan picu realloc internal */
for (int i = 0; i < 10; i++) {
buffer_tambah(buf, (unsigned char)('A' + i));
}
const unsigned char *data = buffer_data(buf);
printf("Panjang: %zu\n", buffer_panjang(buf));
printf("Kapasitas: %zu\n", buffer_kapasitas(buf));
printf("Isi: ");
for (size_t i = 0; i < buffer_panjang(buf); i++) {
printf("%c", data[i]);
}
printf("\n");
buffer_hancurkan(buf);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o op_main op_main.c op_buffer.c
./op_mainOutput yang Dihasilkan:
Panjang: 10
Kapasitas: 16
Isi: ABCDEFGHIJMari kita telaah baris-baris kode di atas:
- Baris 11 (
typedef struct Buffer Buffer;di header): Forward declaration tanpa body. Compiler mencatat bahwaBufferadalah struct, tetapi tidak tahu field atau ukurannya. Inilah inti opaque pointer: pengguna dapat menyimpan pointer tetapi tidak dapat dereference atau mengakses field. - Baris 14-15 (
Buffer *buffer_buat(size_t)): Fungsi pabrikan (factory) yang mengembalikan pointer keBufferbaru. Pengguna tidak tahu strukturBufferdiisi apa—hanya menerima pointer. Bila constructor gagal, kembalikanNULLagar pemanggil dapat menangani error. - Baris 17 (
void buffer_hancurkan(Buffer *buf)): Destructor yang membebaskan memori. Pola wajib: pengguna tidak dapatfree(buf)sendiri karena pengguna tidak tahuBufferpunya fielddatayang juga perlu di-free. Hanya implementasi yang tahu. - Baris 19 (
int buffer_tambah(Buffer *buf, unsigned char byte)): Operasi mutasi. Implementasi diop_buffer.cmenangani realokasi otomatis bila kapasitas penuh. Pengguna tidak tahu realokasi terjadi—API menyembunyikan detail. - Baris 21-23 (
buffer_panjang,buffer_kapasitas,buffer_data): Accessor yang membaca state internal. Pengguna hanya dapat membaca, tidak menulis langsung.buffer_datamengembalikanconst unsigned char *sehingga pengguna tidak dapat memodifikasi isi array tanpa lewatbuffer_tambah.
Verifikasi: Pengguna Tidak Dapat Akses Field #
Pola opaque pointer benar-benar mencegah pengguna mengakses field internal. Mari kita coba compile kode yang melanggar:
/* File: op_test_ilegal.c - mencoba akses field yang seharusnya tersembunyi */
#include "op_buffer.h"
int main(void) {
Buffer *buf = buffer_buat(4);
buf->panjang = 5; /* harusnya gagal compile */
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -c op_test_ilegal.cOutput yang Dihasilkan:
op_test_ilegal.c: In function 'main':
op_test_ilegal.c:5:8: error: invalid use of incomplete typedef 'Buffer'
5 | buf->panjang = 5; /* harusnya gagal compile */
| ^~Compiler menolak akses buf->panjang karena tipe Buffer adalah incomplete type di translation unit ini. Definisi lengkap struct Buffer { ... } hanya ada di op_buffer.c, dan tidak pernah dilihat oleh op_test_ilegal.c. Pesan invalid use of incomplete typedef membuktikan encapsulation bekerja.
Keuntungan Pola Opaque Pointer #
Tiga keuntungan utama menjadikan pola ini standar di library C berskala produksi:
- Stabilitas ABI: Bila kita menambah field baru ke
struct Buffer(misalnyasize_t max_growth), konsumen tidak perlu recompile. Mereka hanya memegang pointer; layout struct tidak terlihat dari header. Library dapat di-update tanpa memutus binary compatibility. - Enkapsulasi nyata: Konsumen tidak dapat membaca atau menulis field internal secara tidak sengaja. Hanya API publik yang dapat dipakai. Ini mengurangi bug akibat konsumen mengandalkan detail implementasi.
- FFI mudah: Konsumen lintas bahasa (Python ctypes, Rust
extern "C") hanya berurusan dengan pointer. Mereka tidak perlu mereplikasi layout struct di sisi mereka.