Sebelum mendalami pointer lebih jauh, kita perlu memahami apa sebenarnya “alamat memori” itu. Setiap byte di RAM komputer memiliki nomor identifikasinya sendiri—mirip dengan nomor rumah di sebuah jalan. Saat program C berjalan, sistem operasi memberikan program tersebut sebagian virtual address space yang dipetakan ke memori fisik. Variabel yang kita deklarasikan ditempatkan di address space ini, dan alamat variabel adalah nomor byte pertama dari blok memori yang dialokasikan untuk variabel tersebut. Pada chapter ini kita akan mencetak alamat beberapa variabel, mengamati ukuran pointer, dan melihat bagaimana elemen array menempati alamat yang berurutan.

Memori Virtual dan Alamat Variabel #

Sistem operasi modern tidak memberikan program akses langsung ke RAM fisik. Sebagai gantinya, sistem operasi menyediakan virtual address space—sebuah ruang alamat privat yang tampak kontigu dari sudut pandang program. Pada arsitektur 64-bit (x86-64, ARM64), ukuran virtual address space teoretis sangat besar, tetapi kernel biasanya hanya mengizinkan program memetakan sebagian kecil di antaranya. Alamat yang kita cetak dengan %p adalah alamat virtual, bukan alamat fisik di RAM.

Setiap kali sebuah variabel dideklarasikan, compiler memesan sejumlah byte pada stack frame fungsi. Jumlah byte tersebut tergantung pada tipe variabel: char menempati 1 byte, int umumnya 4 byte, double 8 byte. Alamat variabel adalah alamat byte pertama dari blok tersebut. Karena variabel bisa diatur ulang letaknya oleh compiler (tergantung urutan deklarasi dan keputusan optimisasi), alamat yang tercetak tidak deterministik dan berbeda setiap kali program dijalankan—efek ini semakin diperkuat oleh Address Space Layout Randomization (ASLR) pada kernel Linux modern.

Visualisasi Konsep #

Diagram berikut memperlihatkan tiga variabel dengan tipe berbeda yang ditempatkan compiler di stack frame beserta ukuran masing-masing:

Pada diagram di atas, setiap variabel menempati blok memori seukuran tipenya: a 4 byte, b 8 byte, dan c 1 byte. Alamat yang tercetak adalah alamat byte pertama dari setiap blok. Compiler bebas menyusun ulang urutan variabel di stack untuk efisiensi alignment, sehingga alamat a, b, dan c tidak selalu berurutan secara rapi—berbeda dengan elemen array yang selalu menempati alamat berurutan dan rapat.

Contoh berikut mencetak alamat dan ukuran tiga variabel dengan tipe berbeda, lalu membandingkan ukuran tiga pointer bertipe berbeda.

c
#include <stdio.h>

int main(void) {
    int    a = 42;
    double b = 3.14;
    char   c = 'Z';

    printf("Variabel a (int)    : alamat %p, ukuran %zu byte\n", (void*)&a, sizeof(a));
    printf("Variabel b (double) : alamat %p, ukuran %zu byte\n", (void*)&b, sizeof(b));
    printf("Variabel c (char)   : alamat %p, ukuran %zu byte\n", (void*)&c, sizeof(c));

    /* Ukuran pointer itu sendiri selalu sama, tidak peduli tipenya */
    int    *pi = &a;
    double *pd = &b;
    char   *pc = &c;
    printf("\nUkuran pointer:\n");
    printf("  sizeof(int*)    = %zu\n", sizeof(pi));
    printf("  sizeof(double*) = %zu\n", sizeof(pd));
    printf("  sizeof(char*)   = %zu\n", sizeof(pc));

    return 0;
}

Output yang Dihasilkan:

text
Variabel a (int)    : alamat 0x7ffce22c1464, ukuran 4 byte
Variabel b (double) : alamat 0x7ffce22c1458, ukuran 8 byte
Variabel c (char)   : alamat 0x7ffce22c1457, ukuran 1 byte

Ukuran pointer:
  sizeof(int*)    = 8
  sizeof(double*) = 8
  sizeof(char*)   = 8

Perhatikan dua hal penting. Pertama, semua pointer berukuran 8 byte pada sistem 64-bit—tidak peduli apakah pointer itu menunjuk ke int, double, atau char. Ukuran pointer ditentukan oleh lebar virtual address space, bukan oleh tipe data yang ditunjuk. Kedua, alamat variabel a, b, dan c tidak berurutan secara rapi: c di ...457, b di ...458, dan a di ...464. Compiler bebas menyusun ulang letak variabel di stack untuk efisiensi alignment, sehingga urutan alamat tidak selalu mencerminkan urutan deklarasi.

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-6 (int a = 42; double b = 3.14; char c = 'Z';): tiga variabel dengan tipe berbeda dideklarasikan sekaligus. Compiler memesan 4, 8, dan 1 byte untuk mereka.
  2. Baris 8-10 (printf(... sizeof(a));): operator sizeof menghasilkan jumlah byte yang ditempati variabel. Format specifier %zu khusus untuk size_t (tipe kembalian sizeof), bukan %d atau %u.
  3. Baris 13-15 (int *pi = &a; ...): tiga pointer dengan tipe berbeda, semuanya menunjuk ke variabel yang sesuai. Tipe pointer berbeda, tetapi ukurannya sama.
  4. Baris 17-19 (printf(" sizeof(int*) = %zu", sizeof(pi));): sizeof(pi) menghasilkan ukuran variabel pointer itu sendiri (8 byte), bukan ukuran variabel yang ditunjuk.

Array Menempati Alamat Berurutan #

Berbeda dengan variabel-variabel lepas yang letaknya bisa diacak compiler, elemen-elemen sebuah array selalu menempati alamat memori yang berurutan dan rapat. Jika data[0] berada di alamat X, maka data[1] berada di X + sizeof(int), data[2] di X + 2*sizeof(int), dan seterusnya. Inilah mengapa akses elemen array berjalan dalam waktu konstan—compiler cukup mengalikan indeks dengan ukuran elemen dan menambahkannya ke alamat awal.

Diagram berikut memperlihatkan empat elemen array int data[4] yang menempati alamat berurutan dengan jarak sizeof(int) = 4 byte:

Pada diagram di atas, setiap elemen array data menempati 4 byte berurutan di stack. Selisih alamat antar elemen selalu tepat sizeof(int) = 4 byte, sehingga alamat elemen ke-i dapat dihitung dengan rumus &data[0] + i * sizeof(int). Pola inilah yang menjadi dasar pointer arithmetic dan akses elemen array dalam waktu konstan.

c
#include <stdio.h>

int main(void) {
    int data[4] = {10, 20, 30, 40};

    printf("Alamat setiap elemen array data (int 4 byte):\n");
    for (int i = 0; i < 4; i++) {
        printf("  &data[%d] = %p  (selisih dari data[0] = %ld byte)\n",
               i, (void*)&data[i],
               (long)((char*)&data[i] - (char*)&data[0]));
    }
    return 0;
}

Output yang Dihasilkan:

text
Alamat setiap elemen array data (int 4 byte):
  &data[0] = 0x7fffa0358fd0  (selisih dari data[0] = 0 byte)
  &data[1] = 0x7fffa0358fd4  (selisih dari data[0] = 4 byte)
  &data[2] = 0x7fffa0358fd8  (selisih dari data[0] = 8 byte)
  &data[3] = 0x7fffa0358fdc  (selisih dari data[0] = 12 byte)

Selisih antar elemen selalu 4 byte—tepat sizeof(int). Pola inilah yang menjadi dasar pointer arithmetic: jika sebuah int *p menunjuk ke data[0], maka p+1 otomatis menunjuk ke data[1] tanpa kita perlu menyebut 4 byte eksplisit. Compiler mengalikan increment dengan sizeof(int) untuk kita. Hal ini akan kita perdalam di chapter Pointer Arithmetic.