Pointer ke int menyimpan alamat int, pointer ke double menyimpan alamat double, sedangkan tipe void * menyimpan alamat tanpa mengikat diri pada tipe apa pun. Sifat generik inilah yang menjadikan void * bahasa komunikasi antara kode dan pustaka yang tipe datanya ditentukan oleh pemanggilnya, sebagaimana terlihat pada malloc dan qsort. Topik ini membahas sifat void *, perannya di kedua fungsi tersebut, dan aturan konversinya.
Satu alamat untuk semua tipe#
Variabel void * dapat menampung alamat objek apa pun tanpa cast, dan konversi sebaliknya dari void * ke tipe pointer mana pun juga berlangsung secara implisit dalam bahasa C. void * tidak memberikan pengetahuan tentang ukuran objek yang ditunjuk, sehingga pointer ini tidak dapat didereference dan tidak dapat dikenai aritmetika menurut standar C. Program berikut menggunakan satu variabel void * yang sama untuk tiga tipe berbeda, dan setiap pembacaan kembali menyebut tipe yang benar.
#include <stdio.h>
int main(void)
{
int angka = 65;
double harga = 12.5;
char huruf = 'Z';
void *g = &angka;
printf("angka lewat int * : %d\n", *(int *)g);
g = &harga;
printf("harga lewat double *: %.1f\n", *(double *)g);
g = &huruf;
printf("huruf lewat char * : %c\n", *(char *)g);
return 0;
}angka lewat int * : 65
harga lewat double *: 12.5
huruf lewat char * : ZVariabel g tidak pernah berubah tipe sepanjang program, yang berubah hanyalah alamat yang ditampungnya. Pada setiap pembacaan, cast seperti *(int *)g memulihkan tipe aslinya sehingga kompilator tahu berapa byte yang harus dibaca. Kewajiban mengingat tipe asli inilah konsekuensi utama dari pointer generik, karena kompilator tidak lagi memeriksa kecocokannya.
Larangan dereference pada void * berakar pada informasi yang hilang, yaitu kompilator tidak tahu berapa byte yang harus dibaca maupun cara menafsirkan isinya. Tipe void sendiri merupakan tipe tak lengkap yang tidak memiliki ukuran, sehingga tidak ada operasi baca-tulis yang bermakna di atasnya. Konsekuensinya konsisten, yaitu setiap pembacaan kembali wajib melewati cast yang memulihkan tipe aslinya.
void * di balik malloc#
Fungsi malloc tidak mengetahui jenis data yang hendak disimpan pemanggilnya, karena pekerjaannya hanya menyediakan blok memori sebesar yang diminta. Hasilnya pun dikembalikan sebagai void *, sehingga dapat ditampung oleh pointer bertipe apa pun tanpa cast. Ukuran yang diminta biasanya ditulis dengan idiom sizeof *data, yang otomatis mengikuti tipe data tanpa menuliskan namanya dua kali.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n = 5;
int *data = malloc(n * sizeof *data);
if (data == NULL) {
return 1;
}
for (int i = 0; i < n; i++) {
data[i] = (i + 1) * (i + 1);
}
for (int i = 0; i < n; i++) {
printf("%d ", data[i]);
}
printf("\n");
free(data);
return 0;
}1 4 9 16 25 Penampungan int *data = malloc(...) bekerja tanpa satu cast pun, dan inilah gaya yang dianjurkan dalam bahasa C. Cast eksplisit seperti (int *)malloc(...) sah, tetapi hanya menyembunyikan error tipe apabila lupa menyertakan <stdlib.h>. Memori hasil alokasi kemudian diperlakukan seperti array biasa, dan free menerima kembali pointer yang sama karena tipe parameternya memang void *.
Keluarga alokasi pada <stdlib.h> mengikuti konvensi yang sama, yaitu calloc dan realloc juga mengembalikan void * yang ditampung tanpa cast. Perbedaan ketiganya terletak pada cara memori disiapkan, misalnya calloc mengosongkan bloknya terlebih dahulu, sedangkan realloc mengatur ulang ukuran blok yang sudah ada. Seluruh keluarga ini dibahas pada modul Memori Dinamis beserta pemeriksaan kegagalannya.
qsort: mengurutkan apa saja#
Fungsi qsort pada <stdlib.h> adalah contoh generik yang lebih lengkap, karena fungsi ini harus mengurutkan array bertipe apa pun tanpa mengetahuinya sebelumnya. Tanda tangannya menerima alamat awal sebagai void *, ukuran satu elemen sebagai size_t, serta pointer fungsi pembanding yang juga bekerja dengan const void *. Di dalam pembanding, parameter dikonversi kembali ke tipe aslinya sebelum nilainya dibandingkan.
#include <stdio.h>
#include <stdlib.h>
int banding_int(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int banding_double(const void *a, const void *b)
{
double x = *(const double *)a;
double y = *(const double *)b;
return (x > y) - (x < y);
}
int main(void)
{
int nilai[] = {42, 7, 19, 3, 25};
qsort(nilai, 5, sizeof nilai[0], banding_int);
printf("int :");
for (int i = 0; i < 5; i++) {
printf(" %d", nilai[i]);
}
printf("\n");
double suhu[] = {36.5, 37.2, 35.9, 38.0};
qsort(suhu, 4, sizeof suhu[0], banding_double);
printf("double:");
for (int i = 0; i < 4; i++) {
printf(" %.1f", suhu[i]);
}
printf("\n");
return 0;
}int : 3 7 19 25 42
double: 35.9 36.5 37.2 38.0Satu fungsi qsort melayani dua tipe berbeda hanya dengan mengganti ukuran elemen dan fungsi pembandingnya. Bentuk kembalian (x > y) - (x < y) menghasilkan 1, 0, atau -1 sesuai urutan, dan dipilih karena selisih x - y pada bilangan bulat dapat meluap apabila kedua nilainya berjauhan ekstrem.
Kontrak pembanding qsort menuntut hasil yang konsisten. Pembanding yang sama harus selalu memberi jawaban sama untuk pasangan nilai yang sama. Pembanding yang bertentangan dengan dirinya sendiri membuat urutan hasil menjadi tidak pasti, sekalipun program tidak berhenti mendadak. Kontrak inilah yang memungkinkan qsort bekerja hanya dengan ukuran elemen dan pointer pembanding, tanpa perlu mengetahui tipe datanya sama sekali.
Aturan konversi yang perlu diingat#
Konversi antara void * dan pointer objek mana pun berlangsung implisit di kedua arah dalam bahasa C, dan pembacaan kembali harus menggunakan tipe asli alamatnya. Dereference langsung terhadap void * ditolak kompilator karena ukuran objeknya tidak diketahui, dan menurut standar C aritmetika di atasnya juga dilarang. Konversi kembali dengan tipe yang keliru, misalnya membaca alamat int melalui double *, tetap lolos kompilasi tetapi menjadi undefined behavior sebab ukuran dan interpretasi bitnya berbeda.
// tidak-dijalankan
int angka = 7;
void *g = &angka;
/* *g; error kompilasi: void * tidak dapat didereference */
/* g + 1; aritmetika pada void * bukan bagian standar C */
/* *(double *)g; sah secara sintaks, tetapi tipe aslinya int: undefined behavior */Kesalahan umum#
- Mencast hasil
malloctanpa keperluan. Cast tidak dilarang, tetapi menyalahi gaya C modern dan menyembunyikan error tipe saat header<stdlib.h>terlupa. Tampung hasilnya langsung ke pointer bertipe tujuan. - Menderereference
void *dengan harapan kompilator mengetahui ukurannya. Informasi ukuran hilang pada tipe generik, sehingga cast ke tipe asli wajib dilakukan terlebih dahulu. - Membaca kembali dengan tipe yang berbeda dari aslinya. Program dapat tampak berjalan pada satu mesin dan gagal pada mesin lain. Catat tipe asli setiap
void *, misalnya melalui nama parameter yang deskriptif. - Mengisi parameter ukuran elemen
qsortdengan ukuran total. Argumen ketiga harus ukuran satu elemen, bukan ukuran seluruh array, sehinggaqsort(data, n, sizeof data, ...)menghasilkan urutan yang kacau atau akses di luar batas. Tulissizeof data[0]agar maksudnya terbaca jelas.
Ringkasan#
void *menyimpan alamat objek apa pun tanpa cast, tetapi tidak dapat didereference dan tidak dapat dikenai aritmetika menurut standar C.mallocmengembalikanvoid *, sehingga hasil alokasi ditampung tanpa cast dan ukurannya ditulis dengan idiomsizeof *data.qsortmengurutkan array bertipe apa pun karena parameternya generik, dan pembandingnya bertugas mengonversiconst void *kembali ke tipe aslinya.
Referensi#
- qsort — cppreference.com — Tanda tangan generik qsort beserta kontrak fungsi pembandingnya.
- Pointer declaration — cppreference.com — Sifat pointer ke void dan aturan konversinya.