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.

c
#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:

text
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 = 88

Perhatikan 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:

  1. Baris 4 (int nilai = 77;): variabel int biasa yang menjadi target akhir rantai pointer.
  2. Baris 5 (int *p1 = &nilai;): pointer level 1, menyimpan alamat nilai. *p1 mengakses nilai.
  3. Baris 6 (int **p2 = &p1;): pointer level 2, menyimpan alamat p1. *p2 mengakses p1; **p2 mengakses nilai.
  4. Baris 18-20 (printf("... p2 = %p", (void*)p2); ... *p2 = ...; **p2 = ...;): tiga tingkat akses pada p2—pointer itu sendiri, hasil dereference pertama, hasil dereference kedua.
  5. Baris 23 (**p2 = 88;): penugasan melalui dua level dereference. Compiler menulis 4 byte int di alamat hasil *(*p2), yang akhirnya adalah alamat nilai.

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.

c
#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:

text
Alokasi berhasil: *q = 123
Sekarang set q ke NULL via set_null(&q)
q == NULL? ya

Fungsi 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.