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.
#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:
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 = ZTiga 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:
- 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. - Baris 9-11 (
void *vi = &i; ...): konversi implisit dariint *,double *,char *kevoid *. Tidak perlu cast eksplisit karena standar mengizinkannya. - Baris 18-20 (
int ri = *(int*)vi; ...): cast eksplisitvidarivoid *keint *, lalu dereference membaca nilaiint. Cast dan dereference dilakukan dalam satu ekspresi—operator*di luar, cast di dalam. - 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.
#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:
int = 0x12345678, alamat 0x7ffd21a1efac
byte pertama = 0x78
double = 3.140000, alamat 0x7ffd21a1efa0
byte pertama = 0x1f
char = 'A' (0x41), alamat 0x7ffd21a1ef9f
byte pertama = 0x41Fungsi 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.