struct member biasanya menempati memori sesuai tipe: int 4 byte, char 1 byte. Namun ada situasi ketika kita hanya perlu menyimpan sedikit bit—sebuah flag 1 bit, sebuah mode 2 bit (0..3), sebuah versi 4 bit (0..15). Mengalokasikan int penuh untuk flag 1 bit jelas boros. C menyediakan bit-fields: member struct yang lebarnya ditentukan dalam bit. Bit-fields bermanfaat untuk merepresentasikan register perangkat keras, header protokol jaringan, atau sekadar menghemat memori pada struktur data berukuran raksasa. Pada chapter ini kita akan mendefinisikan bit-fields, mengamati rentang nilainya, dan melihat perilaku saat nilai melebihi kapasitas.

Apa Itu Bit-field #

Bit-field adalah member struct yang dideklarasikan dengan sintaks tipe nama : lebar;. Tipe harus int, unsigned int, signed int, atau (sejak C23) tipe bilangan bulat lain seperti bool dan unsigned char. Lebar adalah jumlah bit, harus berupa konstanta non-negatif. Member seperti unsigned b : 3; hanya menyimpan 3 bit, sehingga rentang nilainya adalah 0..7 (untuk unsigned) atau -4..3 (untuk signed 3-bit).

Mekanisme kerja di balik layar: compiler mengemas beberapa bit-field ke dalam satu unit penyimpanan (biasanya int) sebanyak mungkin, dan mengocor ke unit berikutnya bila tidak muat. Tata letak bit-field (urutan MSB-first atau LSB-first, aturan packing) bersifat implementation-defined—bisa berbeda antara GCC, Clang, dan MSVC, serta antara arsitektur little-endian dan big-endian. Karena itu, bit-field tidak boleh dipakai untuk membaca/menulis data biner lintas-platform tanpa dokumentasi yang hati-hati.

Mendefinisikan struct dengan Bit-fields #

Kita mulai dengan contoh dasar: struct Flags dengan tiga bit-field berbeda lebar. Tujuannya adalah mengamati rentang nilai dan perilaku saat assignment melebihi kapasitas.

c
#include <stdio.h>

/* struct dengan bit-fields */
struct Flags {
    unsigned a : 1;   /* 1 bit:  rentang 0..1   */
    unsigned b : 3;   /* 3 bit:  rentang 0..7   */
    unsigned c : 4;   /* 4 bit:  rentang 0..15  */
};

int main(void) {
    struct Flags f = {0};
    printf("sizeof(struct Flags) = %zu byte\n", sizeof(struct Flags));

    /* Mengisi nilai valid */
    f.a = 1;          /* max untuk 1 bit */
    f.b = 7;          /* max untuk 3 bit */
    f.c = 15;         /* max untuk 4 bit */
    printf("a=%u b=%u c=%u (semua nilai maksimum)\n", f.a, f.b, f.c);

    /* Mencoba nilai di luar rentang: hanya low bits yang dipakai */
    f.b = 9;          /* 9 dalam biner = 1001, hanya 001 yang tersimpan */
    printf("Setelah f.b = 9  -> a=%u b=%u c=%u\n", f.a, f.b, f.c);

    f.b = 13;         /* 13 = 1101, tersimpan 101 = 5 */
    printf("Setelah f.b = 13 -> a=%u b=%u c=%u\n", f.a, f.b, f.c);

    /* Cek apakah bit ke-x set */
    f.b = 6;          /* biner 110 */
    printf("f.b = %u, bit terendah: %d, bit tengah: %d, bit tertinggi: %d\n",
           f.b, (f.b & 1) != 0, (f.b & 2) != 0, (f.b & 4) != 0);

    return 0;
}

Output yang Dihasilkan:

text
sizeof(struct Flags) = 4 byte
a=1 b=7 c=15 (semua nilai maksimum)
Setelah f.b = 9  -> a=1 b=1 c=15
Setelah f.b = 13 -> a=1 b=5 c=15
f.b = 6, bit terendah: 0, bit tengah: 1, bit tertinggi: 1

Mari kita telaah baris-baris kode di atas:

  1. Baris 5-9 (struct Flags { unsigned a : 1; ... };): tiga bit-field dengan total 1 + 3 + 4 = 8 bit. Compiler mengemas ketiganya ke dalam satu unit penyimpanan, sehingga ukuran struct Flags hanya 4 byte—lebih hemat daripada tiga unsigned int terpisah (12 byte). Lebih besar dari 1 byte karena alignment minimum unit penyimpanan.
  2. Baris 13 (struct Flags f = {0};): inisialisasi aggregate. Member a, b, c semua diisi 0. Inisialisasi awal diperlukan karena bit-field tidak boleh diambil alamatnya (&f.b adalah compile error), sehingga kita tidak bisa memakai pointer untuk membersihkannya.
  3. Baris 16-19 (f.a = 1; f.b = 7; f.c = 15;): assignment nilai maksimum untuk masing-masing lebar. 1 (1 bit) cocok untuk a (rentang 0..1); 7 (3 bit) cocok untuk b (rentang 0..7); 15 (4 bit) cocok untuk c (rentang 0..15). Semua assignment bersih tanpa kehilangan data.
  4. Baris 22 (f.b = 9;): nilai 9 butuh 4 bit (biner 1001) tetapi b hanya 3 bit. Compiler menyimpan 3 bit terendah (001 = 1) dan membuang bit sisanya. Maka f.b menjadi 1—bukan 9. Compiler juga memancarkan warning -Woverflow untuk assignment seperti ini, yang sengaja kita pertahankan untuk mendemonstrasikan perilaku truncation.
  5. Baris 25 (f.b = 13;): nilai 13 (biner 1101) tersimpan sebagai 3 bit terendah (101 = 5). Sekali lagi, kelebihan bit dibuang tanpa runtime error—hanya warning saat kompilasi.
  6. Baris 28-30 ((f.b & 1), (f.b & 2), (f.b & 4)): tes bit individual dengan operasi AND. Untuk f.b = 6 (biner 110), bit terendah (LSB) bernilai 0, bit tengah bernilai 1, bit tertinggi (dari tiga bit) bernilai 1.

Bit-fields untuk Header dan Status Perangkat #

Salah satu penggunaan klasik bit-fields adalah merepresentasikan header protokol jaringan atau status perangkat keras yang tata letaknya sudah ditentukan per bit.

c
#include <stdio.h>

/* bit-fields sering dipakai untuk merepresentasikan register hardware
   atau header protokol jaringan dengan tata letak bit presisi. */

struct HeaderTCP {
    unsigned short src_port : 16;
    unsigned short dst_port : 16;
    unsigned int   seq_num  : 32;
};

/* Bendera status perangkat */
struct DeviceStatus {
    unsigned power_on  : 1;
    unsigned error     : 1;
    unsigned standby   : 1;
    unsigned mode      : 2;   /* 4 mode: 0-3 */
    unsigned reserved  : 3;
};

int main(void) {
    struct HeaderTCP hdr = {8080, 80, 12345};
    printf("TCP Header:\n");
    printf("  src_port = %u\n", hdr.src_port);
    printf("  dst_port = %u\n", hdr.dst_port);
    printf("  seq_num  = %u\n", hdr.seq_num);
    printf("  size     = %zu byte\n", sizeof(struct HeaderTCP));

    struct DeviceStatus dev = {0};
    dev.power_on = 1;
    dev.mode     = 3;
    dev.error    = 0;
    dev.standby  = 0;

    printf("\nDeviceStatus:\n");
    printf("  power_on = %u\n", dev.power_on);
    printf("  mode     = %u\n", dev.mode);
    printf("  size     = %zu byte\n", sizeof(struct DeviceStatus));

    /* Mode hanya 2 bit -> batas 0..3 */
    dev.mode = 5;  /* 5 = 101 biner, hanya 01 tersimpan = 1 */
    printf("  mode setelah = 5 -> tersimpan %u\n", dev.mode);

    return 0;
}

Output yang Dihasilkan:

text
TCP Header:
  src_port = 8080
  dst_port = 80
  seq_num  = 12345
  size     = 8 byte

DeviceStatus:
  power_on = 1
  mode     = 3
  size     = 4 byte
  mode setelah = 5 -> tersimpan 1

Pada contoh di atas, struct HeaderTCP memodelkan bagian awal header TCP: 16-bit source port, 16-bit destination port, 32-bit sequence number. Total 64 bit atau 8 byte—ukuran yang sama dengan dua unsigned int (atau empat unsigned short), tetapi tata letaknya kini eksplisit per member. struct DeviceStatus memadatkan tujuh flag (1+1+1+2+3 = 8 bit) ke dalam satu unit 4 byte; pada akhir contoh, dev.mode = 5 kembali mendemonstrasikan truncation: 5 (biner 101) tersimpan sebagai 1 (biner 01) karena hanya 2 bit yang tersedia. Compiler memancarkan warning yang sama seperti sebelumnya.