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.

c
/* 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 */
c
/* 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.

c
/* 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:

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

Output yang Dihasilkan:

text
Panjang:   10
Kapasitas: 16
Isi:       ABCDEFGHIJ

Mari kita telaah baris-baris kode di atas:

  1. Baris 11 (typedef struct Buffer Buffer; di header): Forward declaration tanpa body. Compiler mencatat bahwa Buffer adalah struct, tetapi tidak tahu field atau ukurannya. Inilah inti opaque pointer: pengguna dapat menyimpan pointer tetapi tidak dapat dereference atau mengakses field.
  2. Baris 14-15 (Buffer *buffer_buat(size_t)): Fungsi pabrikan (factory) yang mengembalikan pointer ke Buffer baru. Pengguna tidak tahu struktur Buffer diisi apa—hanya menerima pointer. Bila constructor gagal, kembalikan NULL agar pemanggil dapat menangani error.
  3. Baris 17 (void buffer_hancurkan(Buffer *buf)): Destructor yang membebaskan memori. Pola wajib: pengguna tidak dapat free(buf) sendiri karena pengguna tidak tahu Buffer punya field data yang juga perlu di-free. Hanya implementasi yang tahu.
  4. Baris 19 (int buffer_tambah(Buffer *buf, unsigned char byte)): Operasi mutasi. Implementasi di op_buffer.c menangani realokasi otomatis bila kapasitas penuh. Pengguna tidak tahu realokasi terjadi—API menyembunyikan detail.
  5. Baris 21-23 (buffer_panjang, buffer_kapasitas, buffer_data): Accessor yang membaca state internal. Pengguna hanya dapat membaca, tidak menulis langsung. buffer_data mengembalikan const unsigned char * sehingga pengguna tidak dapat memodifikasi isi array tanpa lewat buffer_tambah.

Verifikasi: Pengguna Tidak Dapat Akses Field #

Pola opaque pointer benar-benar mencegah pengguna mengakses field internal. Mari kita coba compile kode yang melanggar:

c
/* 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:

bash
gcc -std=c11 -Wall -Wextra -c op_test_ilegal.c

Output yang Dihasilkan:

text
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:

  1. Stabilitas ABI: Bila kita menambah field baru ke struct Buffer (misalnya size_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.
  2. 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.
  3. FFI mudah: Konsumen lintas bahasa (Python ctypes, Rust extern "C") hanya berurusan dengan pointer. Mereka tidak perlu mereplikasi layout struct di sisi mereka.