Sintaks deklarasi pointer di C sering membingungkan pemula karena simbol * dipakai untuk dua hal sekaligus: sebagai penanda tipe pointer dalam deklarasi, dan sebagai operator dereference dalam ekspresi. Padanya itu, letak * relatif terhadap tipe dan nama variabel memberi kesan visual yang bisa menyesatkan—terutama saat mendeklarasikan beberapa variabel dalam satu pernyataan. Pada chapter ini kita akan menelusuri tata cara deklarasi pointer, cara membaca tipe pointer dengan benar, dan beberapa jebakan yang sering membuat pemula salah tafsir.

Sintaks Dasar Deklarasi Pointer #

Deklarasi pointer mengikuti pola T *nama; di mana T adalah tipe data yang ditunjuk dan nama adalah identifier pointer. Compiler C mengabaikan whitespace, sehingga int *p, int* p, dan int * p adalah tiga cara menulis deklarasi yang sama. Yang penting bagi compiler adalah keberadaan simbol * antara tipe dan identifier. Namun konvensi penulisan berbeda-beda: gaya K&R (Kernighan & Ritchie) menempatkan * menempel ke nama (int *p); gaya C++ menempatkannya menempel ke tipe (int* p).

Mekanisme kerjanya: saat compiler membaca int *p;, compiler memesan sizeof(int*) byte (8 byte pada sistem 64-bit) untuk variabel p dan mencatat bahwa p bertipe int *. Operator * pada konteks deklarasi bukan operator dereference—melainkan penanda bahwa variabel yang dideklarasikan adalah pointer. Tipe int * berarti “pointer ke int”; tipe char * berarti “pointer ke char”; tipe int ** berarti “pointer ke pointer ke int”.

c
#include <stdio.h>

int main(void) {
    /* Tiga gaya penempatan * yang ekuivalen */
    int  *a;        /* * menempel ke nama (gaya K&R) */
    int* b;         /* * menempel ke tipe (gaya C++) */
    int *c;         /* * di tengah (umum di buku C) */

    int x = 10, y = 20, z = 30;
    a = &x;
    b = &y;
    c = &z;

    printf("a -> %d, b -> %d, c -> %d\n", *a, *b, *c);

    /* Jebakan: * hanya berlaku ke identifier terdekat */
    int  *p, q;     /* p adalah int*, q adalah int biasa */
    p = &x;
    q = 999;
    printf("p -> %d, q = %d\n", *p, q);

    return 0;
}

Output yang Dihasilkan:

text
a -> 10, b -> 20, c -> 30
p -> 10, q = 999

Tiga deklarasi awal (int *a;, int* b;, int *c;) menghasilkan tiga variabel pointer int * yang identik secara semantik. Perbedaan hanya gaya penulisan—compiler memperlakukan ketiganya sama. Namun perhatikan jebakan pada baris int *p, q;: hanya p yang bertipe int *, sedangkan q bertipe int biasa. Simbol * hanya berlaku untuk satu identifier terdekatnya. Untuk membuat kedua variabel menjadi pointer, kita harus menulis int *p, *q; dengan * terpisah untuk masing-masing.

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-6 (int *a; int* b; int *c;): tiga deklarasi pointer bertipe int * dengan tiga gaya penulisan berbeda. Ketiganya ekuivalen secara semantik.
  2. Baris 8-11 (a = &x; b = &y; c = &z;): pointer-pointer diinisialisasi dengan alamat variabel x, y, z. Tanpa inisialisasi, pointer berisi nilai sampah yang berbahaya jika di-dereference.
  3. Baris 13 (printf("a -> %d, ...", *a, *b, *c);): dereference ketiga pointer untuk membaca nilai int pada alamat yang ditunjuk. Hasilnya: 10, 20, 30.
  4. Baris 16 (int *p, q;): deklarasi multi-variabel. Hanya p yang bertipe int *; q bertipe int biasa karena * tidak “menular” ke identifier berikutnya.
  5. Baris 17-18 (p = &x; q = 999;): p menerima alamat; q menerima nilai int biasa. Penugasan ini sah karena tipe keduanya berbeda.

Pointer ke Berbagai Tipe #

Kita bisa mendeklarasikan pointer ke tipe data apa pun. Tipe pointer menentukan berapa byte yang dibaca saat operator * dipakai. Pada contoh berikut kita mendeklarasikan tiga pointer ke tipe berbeda, semuanya menunjuk ke variabel int yang sama (dua di antaranya melalui cast eksplisit). Contoh ini juga memperlihatkan deklarasi pointer ke pointer (int **ppi).

c
#include <stdio.h>

int main(void) {
    int v = 7;

    /* Pointer ke berbagai tipe */
    int    *pi = &v;
    char   *pc = (char*)&v;   /* cast int* ke char* */
    double *pd = (double*)&v; /* cast int* ke double* (tidak aman, hanya demo) */

    printf("sizeof(int*)    = %zu, *pi = %d\n", sizeof(pi), *pi);
    printf("sizeof(char*)   = %zu, *pc = %d (byte pertama dari int)\n",
           sizeof(pc), (int)(*pc));
    printf("sizeof(double*) = %zu\n", sizeof(pd));

    /* Pointer ke pointer */
    int **ppi = &pi;
    printf("\nppi = %p menunjuk ke pi, *ppi = %p, **ppi = %d\n",
           (void*)ppi, (void*)*ppi, **ppi);

    return 0;
}

Output yang Dihasilkan:

text
sizeof(int*)    = 8, *pi = 7
sizeof(char*)   = 8, *pc = 7 (byte pertama dari int)
sizeof(double*) = 8

ppi = 0x7fff38a11eb8 menunjuk ke pi, *ppi = 0x7fff38a11ec4, **ppi = 7

Ukuran ketiga pointer sama (8 byte), tetapi perilaku dereference-nya berbeda. *pi membaca 4 byte int; *pc hanya membaca 1 byte (byte pertama dari v); *pd akan mencoba membaca 8 byte double dari region yang sebenarnya hanya 4 byte int—ini undefined behavior yang sengaja tidak kita dereference. Deklarasi int **ppi = &pi; membuat pointer dua level: ppi menyimpan alamat pi (yang juga pointer). Ekspresi **ppi berarti “dereference ppi untuk dapatkan pi, lalu dereference pi untuk dapatkan nilai int” yang akhirnya menghasilkan 7.