Sejauh ini kita telah melihat banyak cara memakai struct, union, dan enum. Pada chapter penutup ini, kita akan membahas prinsip merancang struktur data yang baik: keterbacaan, efisiensi memori, dan enkapsulasi. Merancang struct bukan sekadar menyatukan beberapa member—urutan member, pemilihan tipe, dan pemisahan antara deklarasi publik dan detail internal ikut menentukan kualitas kode. Praktik buruk seperti urutan member yang acak atau tereksposnya detail implementasi dapat memicu bug yang sulit ditelusuri, terutama pada proyek besar yang dikerjakan banyak orang. Kita akan menelusuri tiga prinsip utama dengan contoh konkret yang bisa dijalankan.

Prinsip 1: Kelompokkan Member yang Berkaitan #

Saat sebuah struct punya banyak member yang bisa dikelompokkan secara logis, sebaiknya pecah menjadi nested struct. Misalnya, alamat (jalan, kota, kode pos) adalah satu kesatuan—bila tiga member tersebut menempel pada struct Mahasiswa tanpa grouping, kode sulit dibaca dan rentan duplikasi bila ada struct Dosen yang juga punya alamat.

c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* Versi BURUK: member tersebar tanpa grouping */
struct MahasiswaBuruk {
    char  nama[50];
    char  jalan[60];
    int   umur;
    char  kota[30];
    float ipk;
    int   kode_pos;
};

/* Versi BAIK: member dikelompokkan via nested struct */
struct Alamat {
    char jalan[60];
    char kota[30];
    int  kode_pos;
};

struct MahasiswaBaik {
    char  nama[50];
    int   umur;
    float ipk;
    struct Alamat alamat;
};

/* Versi BURUK: urutan member tidak rapi -> banyak padding */
struct Buruk {
    char  a;
    long  b;
    char  c;
    int   d;
    char  e;
};

/* Versi BAIK: urutan member diatur supaya padding minimum */
struct Baik {
    long  b;
    int   d;
    char  a;
    char  c;
    char  e;
};

/* Versi dengan opaque pointer: detail struct disembunyikan */
typedef struct Konfig Konfig;   /* forward declaration */

/* Hanya deklarasi yang terekspos ke pengguna */
Konfig *konfig_buat(const char *nama_file);
void    konfig_hancurkan(Konfig *k);

/* Implementasi struct lengkap ada di file .c */
struct Konfig {
    char nama[64];
    int  versi;
    int  flag;
};

Konfig *konfig_buat(const char *nama_file) {
    Konfig *k = malloc(sizeof(Konfig));
    if (k == NULL) return NULL;
    strncpy(k->nama, nama_file, sizeof(k->nama) - 1);
    k->nama[sizeof(k->nama) - 1] = '\0';
    k->versi = 1;
    k->flag = 0;
    return k;
}

void konfig_hancurkan(Konfig *k) {
    free(k);
}

int main(void) {
    /* Perbandingan ukuran */
    printf("Buruk: sizeof(struct MahasiswaBuruk) = %zu\n",
           sizeof(struct MahasiswaBuruk));
    printf("Baik : sizeof(struct MahasiswaBaik)  = %zu\n",
           sizeof(struct MahasiswaBaik));

    printf("\nPadding demo:\n");
    printf("  sizeof(struct Buruk) = %zu (member tersebar)\n",
           sizeof(struct Buruk));
    printf("  sizeof(struct Baik)  = %zu (member dirapihkan)\n",
           sizeof(struct Baik));

    /* Demo opaque pointer */
    Konfig *k = konfig_buat("app.cfg");
    if (k != NULL) {
        printf("\nKonfig dibuat: nama=\"%s\", versi=%d\n", k->nama, k->versi);
        konfig_hancurkan(k);
    }

    return 0;
}

Output yang Dihasilkan:

text
Buruk: sizeof(struct MahasiswaBuruk) = 156
Baik : sizeof(struct MahasiswaBaik)  = 156

Padding demo:
  sizeof(struct Buruk) = 32 (member tersebar)
  sizeof(struct Baik)  = 16 (member dirapihkan)

Konfig dibuat: nama="app.cfg", versi=1

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-13 (struct MahasiswaBuruk): enam member tersebar tanpa grouping. Member jalan, kota, kode_pos bercampur dengan nama, umur, ipk. Pembaca kode harus menyaring sendiri mana yang berkaitan.
  2. Baris 16-20 (struct Alamat): tipe terpisah untuk alamat. Tipe ini bisa dipakai ulang untuk MahasiswaBaik, Dosen, Pelanggan, dan siapa pun yang punya alamat.
  3. Baris 22-27 (struct MahasiswaBaik): member alamat disatukan ke dalam satu instance struct Alamat. Ukuran total masih 156 byte—sama dengan versi buruk—tetapi keterbacaan jauh lebih baik, dan tipe Alamat bisa dipakai ulang.
  4. Baris 30-36 (struct Buruk): lima member dengan alignment berbeda ditulis tanpa urutan yang rapi. char (alignment 1) berdekatan dengan long (alignment 8), menyebabkan padding banyak.
  5. Baris 39-45 (struct Baik): member yang sama disusun ulang dengan member ber-alignment besar diletakkan dulu (long, int), lalu member kecil (char) di akhir. Hasilnya 16 byte—setengah dari versi buruk.
  6. Baris 48-55 (typedef struct Konfig Konfig;): opaque pointer pattern. Header hanya menyatakan typedef struct Konfig Konfig; tanpa member. Pengguna tidak bisa langsung mengakses k->nama—mereka hanya bisa memakai API konfig_buat dan konfig_hancurkan. Detail implementasi (member lengkap) ada di file .c. Pemisahan ini mencegah pengguna bergantung pada detail internal yang bisa berubah.
  7. Baris 64-72 (konfig_buat, konfig_hancurkan): konstruktor dan destruktor yang mengelola alokasi. Pola ini membebaskan pengguna dari keharusan memanggil malloc/free langsung, sehingga pengelolaan memori terkonsolidasi di satu tempat.

Prinsip 2 dan 3: Enkapsulasi dan API yang Bersih #

Pada struct yang merepresentasikan struktur data (seperti linked list atau stack), sebaiknya member internal disembunyikan dan hanya operasi lewat fungsi yang terekspos. Pola ini disebut enkapsulasi manual di C—tidak ada kata kunci private seperti di C++, tetapi disiplin penempatan deklarasi bisa mencapai efek yang sama.

c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* Linked list node sebagai contoh struktur data sederhana */
typedef struct Node {
    int data;
    struct Node *next;
} Node;

/* API hanya menyediakan operasi, bukan akses langsung ke field */
typedef struct {
    Node *head;
    int   count;
} List;

void list_init(List *l) {
    l->head = NULL;
    l->count = 0;
}

void list_push_front(List *l, int val) {
    Node *n = malloc(sizeof(Node));
    if (n == NULL) return;
    n->data = val;
    n->next = l->head;
    l->head = n;
    l->count++;
}

void list_print(const List *l) {
    printf("[count=%d] ", l->count);
    Node *cur = l->head;
    while (cur != NULL) {
        printf("%d -> ", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

void list_clear(List *l) {
    Node *cur = l->head;
    while (cur != NULL) {
        Node *next = cur->next;
        free(cur);
        cur = next;
    }
    l->head = NULL;
    l->count = 0;
}

int main(void) {
    List L;
    list_init(&L);

    list_push_front(&L, 10);
    list_push_front(&L, 20);
    list_push_front(&L, 30);
    list_print(&L);

    list_clear(&L);
    list_print(&L);

    return 0;
}

Output yang Dihasilkan:

text
[count=3] 30 -> 20 -> 10 -> NULL
[count=0] NULL

Pada contoh di atas, struct List punya dua member: head (pointer ke node pertama) dan count (jumlah elemen). Walaupun member ini masih terlihat di header (C tidak punya mekanisme private), disiplin API membuat pengguna tidak perlu menyentuh kedua member tersebut secara langsung. Fungsi list_init, list_push_front, list_print, dan list_clear adalah satu-satunya cara resmi untuk berinteraksi dengan List. Keuntungannya: bila implementasi internal berganti (misalnya dari singly-linked list menjadi doubly-linked list), pengguna tidak perlu mengubah kode mereka—selama API tetap sama. Pola ini disebut encapsulation by convention dan banyak dipakai di pustaka C standar (lihat FILE * di <stdio.h> yang detailnya tidak terekspos ke pengguna).