Tipe void * adalah pointer generik di C—pointer yang menampung alamat objek bertipe apa pun tanpa memedulikan tipenya. Berbeda dengan int * atau char *, void * tidak membawa informasi ukuran elemen sehingga tidak bisa di-dereference langsung atau dikenai pointer arithmetic tanpa cast eksplisit. Tipe ini menjadi tulang punggung API generik C: malloc mengembalikan void * (alamat blok mentah yang bisa di-cast ke tipe apa pun), qsort menerima comparator yang bekerja pada const void *, dan memcpy/memmove bekerja pada void * untuk menyalin byte apa pun. Pada chapter ini kita akan memahami cara kerja void *, kapan harus melakukan cast, dan bagaimana memakainya untuk menulis fungsi generik.

Apa Itu void * #

void * adalah tipe pointer yang menandai “alamat memori tanpa informasi tipe”. Standar C (ISO/IEC 9899 §6.3.2.3) mengatur: pointer ke tipe apa pun bisa dikonversi secara implisit ke void * dan sebaliknya tanpa kehilangan informasi alamat. Namun, void * tidak bisa di-dereference langsung—compiler tidak tahu berapa byte yang harus dibaca dan bagaimana menafsirkan byte-byte tersebut. Untuk membaca nilai, kita harus cast ke tipe pointer konkret seperti (int*)vp atau (double*)vp terlebih dahulu, lalu dereference hasilnya.

Mekanisme kerjanya: pointer void * menyimpan alamat dalam 8 byte (pada sistem 64-bit), sama seperti pointer bertipe lain. Perbedaannya, compiler menolak dereference langsung (*vp dengan vp bertipe void *) karena tidak ada informasi ukuran/tipe. Cast eksplisit (int*)vp mengubah tipe pointer dari void * ke int * tanpa mengubah nilai alamat yang tersimpan—cast hanya memengaruhi cara compiler menafsirkan byte di alamat tersebut.

c
#include <stdio.h>

int main(void) {
    int    i = 42;
    double d = 3.14;
    char   c = 'Z';

    /* void* menampung alamat tipe apa pun */
    void *vi = &i;
    void *vd = &d;
    void *vc = &c;

    printf("Alamat & ukuran:\n");
    printf("  vi -> %p, sizeof(int)    = %zu\n", vi, sizeof(i));
    printf("  vd -> %p, sizeof(double) = %zu\n", vd, sizeof(d));
    printf("  vc -> %p, sizeof(char)   = %zu\n", vc, sizeof(c));

    /* Dereference void* butuh cast eksplisit */
    int    ri = *(int*)vi;
    double rd = *(double*)vd;
    char   rc = *(char*)vc;
    printf("\nSetelah cast dan dereference:\n");
    printf("  *(int*)vi    = %d\n", ri);
    printf("  *(double*)vd = %f\n", rd);
    printf("  *(char*)vc   = %c\n", rc);

    return 0;
}

Output yang Dihasilkan:

text
Alamat & ukuran:
  vi -> 0x7fffd55c53a0, sizeof(int)    = 4
  vd -> 0x7fffd55c5398, sizeof(double) = 8
  vc -> 0x7fffd55c5397, sizeof(char)   = 1

Setelah cast dan dereference:
  *(int*)vi    = 42
  *(double*)vd = 3.140000
  *(char*)vc   = Z

Tiga variabel dengan tipe berbeda (int, double, char) bisa disimpan ke tiga variabel void * tanpa cast apa pun—konversi dari pointer bertipe ke void * bersifat implisit. Untuk membaca nilai asli, kita melakukan cast eksplisit: (int*)vi mengubah tipe vi dari void * ke int *, lalu dereference *(int*)vi membaca 4 byte int di alamat tersebut. Cast harus sesuai dengan tipe asli objek; cast ke tipe yang salah (misalnya *(double*)vi padahal vi menunjuk ke int) adalah undefined behavior.

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-6 (int i = 42; double d = 3.14; char c = 'Z';): tiga variabel bertipe berbeda yang akan kita simpan alamatnya di pointer generik.
  2. Baris 9-11 (void *vi = &i; ...): konversi implisit dari int *, double *, char * ke void *. Tidak perlu cast eksplisit karena standar mengizinkannya.
  3. Baris 18-20 (int ri = *(int*)vi; ...): cast eksplisit vi dari void * ke int *, lalu dereference membaca nilai int. Cast dan dereference dilakukan dalam satu ekspresi—operator * di luar, cast di dalam.
  4. Baris 24-26 (printf(" *(int*)vi = %d", ri); ...): konfirmasi bahwa cast mengembalikan nilai asli yang disimpan pada alamat.

void * untuk Fungsi Generik #

void * memungkinkan kita menulis fungsi yang bekerja pada data tipe apa pun. Contoh klasik adalah fungsi yang memeriksa byte pertama objek untuk tujuan debugging atau inspeksi memori. Fungsi cetak_byte_pertama berikut menerima alamat objek bertipe apa pun, cast ke const unsigned char *, dan membaca byte pertama.

c
#include <stdio.h>

/* Fungsi generik mencetak byte pertama objek */
void cetak_byte_pertama(const void *obj) {
    const unsigned char *byte = (const unsigned char*)obj;
    printf("  byte pertama = 0x%02x\n", *byte);
}

int main(void) {
    int    angka = 0x12345678;
    double pi    = 3.14;
    char   huruf = 'A';

    printf("int    = 0x%x, alamat %p\n", angka, (void*)&angka);
    cetak_byte_pertama(&angka);

    printf("double = %f, alamat %p\n", pi, (void*)&pi);
    cetak_byte_pertama(&pi);

    printf("char   = '%c' (0x%02x), alamat %p\n",
           huruf, (unsigned char)huruf, (void*)&huruf);
    cetak_byte_pertama(&huruf);

    return 0;
}

Output yang Dihasilkan:

text
int    = 0x12345678, alamat 0x7ffd21a1efac
  byte pertama = 0x78
double = 3.140000, alamat 0x7ffd21a1efa0
  byte pertama = 0x1f
char   = 'A' (0x41), alamat 0x7ffd21a1ef9f
  byte pertama = 0x41

Fungsi cetak_byte_pertama bekerja pada int, double, maupun char karena parameternya const void *—bisa menerima alamat tipe apa pun. Di dalam fungsi, kita cast ke const unsigned char * untuk membaca persis 1 byte pertama objek. Pada arsitektur little-endian seperti x86-64, byte paling tidak signifikan disimpan lebih dulu—makanya byte pertama dari 0x12345678 adalah 0x78 (bukan 0x12). Inilah kekuatan void *: satu fungsi dapat melayani berbagai tipe tanpa duplikasi kode.