Pointer bisa menunjuk ke objek bertipe apa pun—termasuk pointer lain. Tipe int ** berarti “pointer ke pointer ke int”. Konstruksi ini terdengar rumit, tetapi sebenarnya hanya merangkai dua level alamat: variabel pertama berisi alamat variabel kedua, yang berisi alamat int akhir. Pointer ke pointer sering dipakai untuk mengalokasikan objek dinamis dari dalam fungsi (idiom factory function), atau untuk merepresentasikan array 2D yang dialokasikan dinamis. Pada chapter ini kita akan menelusuri rantai dereference **pp dan melihat kasus pemakaian nyata untuk int **.
Rantai Dereference pada Pointer ke Pointer #
Deklarasi int **p2 membuat variabel p2 bertipe int **—pointer yang menunjuk ke objek bertipe int *. Untuk mencapai int akhir, kita perlu dereference dua kali: *p2 menghasilkan int * (pointer antara), lalu **p2 (yang setara dengan *(*p2)) menghasilkan int akhir. Sintaks ini bisa diperpanjang: int ***p3 adalah pointer ke pointer ke pointer ke int, dengan tiga level dereference ***p3.
Mekanisme kerjanya: saat compiler melihat **p2, compiler mengevaluasi dari dalam ke luar. Pertama *p2 membaca 8 byte (ukuran int *) di alamat p2, menghasilkan pointer int *. Kemudian dereference luar membaca 4 byte (ukuran int) di alamat pointer antara tadi, menghasilkan nilai int akhir. Setiap level pointer menambah satu langkah dereference dan satu byte ukuran pointer (8 byte pada sistem 64-bit).
Visualisasi Konsep #
Diagram berikut memperlihatkan rantai dereference **p2 yang menghubungkan p2, p1, dan nilai:
Pada diagram di atas, dereference **p2 dijalankan dalam dua langkah berurutan: langkah pertama *p2 mengikuti alamat di p2 untuk mencapai p1, langkah kedua *(*p2) mengikuti alamat di p1 untuk mencapai nilai. Setiap level pointer menyimpan alamat level berikutnya, dan penugasan **p2 = 88 menulis melalui rantai penuh hingga ke variabel nilai di main. Inilah mengapa setiap tambahan * pada deklarasi pointer (int *, int **, int ***) menambah satu langkah dereference untuk mencapai nilai akhir.
#include <stdio.h>
int main(void) {
int nilai = 77;
int *p1 = &nilai; /* pointer ke int */
int **p2 = &p1; /* pointer ke pointer ke int */
printf("nilai = %d\n", nilai);
printf("&nilai = %p\n", (void*)&nilai);
printf("\np1 (int*) = %p\n", (void*)p1);
printf("&p1 = %p\n", (void*)&p1);
printf("*p1 = %d\n", *p1);
printf("\np2 (int**) = %p\n", (void*)p2);
printf("*p2 = %p (alamat yang juga disimpan p1)\n",
(void*)*p2);
printf("**p2 = %d (nilai int di akhir rantai)\n", **p2);
/* Modifikasi nilai melalui p2 */
**p2 = 88;
printf("\nSetelah **p2 = 88 -> nilai = %d\n", nilai);
return 0;
}Output yang Dihasilkan:
nilai = 77
&nilai = 0x7ffe61631054
p1 (int*) = 0x7ffe61631054
&p1 = 0x7ffe61631048
*p1 = 77
p2 (int**) = 0x7ffe61631048
*p2 = 0x7ffe61631054 (alamat yang juga disimpan p1)
**p2 = 77 (nilai int di akhir rantai)
Setelah **p2 = 88 -> nilai = 88Perhatikan rantai alamat: &nilai == 0x...054, p1 berisi 0x...054 (alamat nilai), &p1 == 0x...048, dan p2 berisi 0x...048 (alamat p1). Dereference *p2 membaca 8 byte di 0x...048, menghasilkan 0x...054 (yaitu nilai p1). Dereference kedua **p2 membaca 4 byte di 0x...054, menghasilkan 77 (yaitu nilai nilai). Penugasan **p2 = 88 menulis melalui rantai penuh hingga ke variabel nilai di main.
Mari kita telaah baris-baris kode di atas:
- Baris 4 (
int nilai = 77;): variabelintbiasa yang menjadi target akhir rantai pointer. - Baris 5 (
int *p1 = &nilai;): pointer level 1, menyimpan alamatnilai.*p1mengaksesnilai. - Baris 6 (
int **p2 = &p1;): pointer level 2, menyimpan alamatp1.*p2mengaksesp1;**p2mengaksesnilai. - Baris 18-20 (
printf("... p2 = %p", (void*)p2); ... *p2 = ...; **p2 = ...;): tiga tingkat akses padap2—pointer itu sendiri, hasil dereference pertama, hasil dereference kedua. - Baris 23 (
**p2 = 88;): penugasan melalui dua level dereference. Compiler menulis 4 byteintdi alamat hasil*(*p2), yang akhirnya adalah alamatnilai.
Kasus Pemakaian: Fungsi yang Mengalokasikan untuk Pemanggil #
Salah satu kegunaan utama int ** adalah fungsi yang perlu mengisi pointer pemanggil dengan alamat objek baru—misalnya fungsi factory yang mengalokasikan memori dan mengembalikan alamatnya melalui out-parameter. Karena C hanya bisa mengembalikan satu nilai via return, kita pakai pointer ke pointer untuk “mengembalikan” alamat yang baru saja dialokasikan.
#include <stdio.h>
#include <stdlib.h>
/* Mengalokasikan blok dan mengisi pointer pemanggil */
int alokasi_int(int **out_ptr) {
int *p = malloc(sizeof(int));
if (p == NULL) {
return -1; /* gagal alokasi */
}
*p = 123;
*out_ptr = p; /* isi pointer pemanggil dengan alamat blok */
return 0;
}
/* Mengubah pointer pemanggil menjadi NULL */
void set_null(int **pp) {
*pp = NULL;
}
int main(void) {
int *q = NULL;
if (alokasi_int(&q) == 0) {
printf("Alokasi berhasil: *q = %d\n", *q);
printf("Sekarang set q ke NULL via set_null(&q)\n");
set_null(&q);
printf("q == NULL? %s\n", (q == NULL) ? "ya" : "tidak");
/* CATATAN: di sini q sudah NULL, memori yang dialokasikan
tadi bocor karena tidak di-free sebelum di-set NULL.
Demo ini menunjukkan pointer-ke-pointer, bukan manajemen
memori yang benar. */
} else {
printf("Alokasi gagal\n");
}
return 0;
}Output yang Dihasilkan:
Alokasi berhasil: *q = 123
Sekarang set q ke NULL via set_null(&q)
q == NULL? yaFungsi alokasi_int menerima int **out_ptr—alamat dari pointer pemanggil. Di dalam fungsi, *out_ptr adalah pointer pemanggil sendiri (bukan salinan). Penugasan *out_ptr = p mengubah pointer pemanggil agar menunjuk ke blok yang baru di-malloc. Begitu pula set_null(int **pp): parameter pp adalah alamat dari pointer pemanggil, *pp adalah pointer pemanggil sendiri, dan penugasan *pp = NULL mengubahnya menjadi NULL. Inilah cara C “mengembalikan” pointer baru—tidak bisa dilakukan tanpa pointer ke pointer.