Pointer adalah salah satu fitur paling khas sekaligus paling sering disalahpahami dalam bahasa C. Banyak pembaca yang berasal dari bahasa tingkat tinggi seperti Python atau JavaScript kerap merasa asing saat pertama kali bertemu konsep “variabel yang isinya adalah alamat variabel lain”. Padahal, tanpa pointer kita tidak akan bisa melewati array secara efisien, mengubah variabel pemanggil dari dalam fungsi, mengelola memori dinamis dengan malloc/free, ataupun membangun struktur data seperti linked list dan tree. Pada chapter ini kita akan menelusuri ide dasar pointer dengan analogi sederhana, melihat bagaimana compiler menyusunnya di memori, dan mencoba langsung dua contoh kode terkecil yang memperlihatkan proses baca serta tulis nilai melalui pointer.
Analogi: Variabel sebagai Kotak, Pointer sebagai Slip Alamat #
Bayangkan setiap variabel di C sebagai sebuah kotak bernama di dalam gudang memori komputer. Kotak tersebut menampung sebuah nilai, misalnya 10. Untuk bisa menemukan kotak itu, gudang membutuhkan alamat—misalnya “Rak B-3, Slot 8”. Variabel biasa berisi nilai (10); pointer berisi alamat kotak tersebut (Rak B-3, Slot 8). Ketika kita membaca isi pointer, kita sedang membaca alamat; ketika kita dereference pointer (menggunakan operator *), kita menyusun ke alamat tersebut dan mengambil nilai yang ada di dalamnya. Dua langkah ini—membaca alamat dan mengambil nilai di alamat—adalah inti dari setiap operasi pointer di C.
Mekanisme kerja di balik layar juga sederhana. Saat compiler C melihat deklarasi int x = 10;, compiler mengalokasikan sizeof(int) byte (biasanya 4 byte) di stack frame fungsi main dan mengikatkan nama x ke alamat tersebut. Saat compiler melihat int *p = &x;, compiler mengalokasikan sizeof(int*) byte (8 byte pada sistem 64-bit) untuk variabel p dan menyimpan alamat x sebagai nilai p. Operator & menghasilkan alamat operand, sedangkan operator * melakukan inverse: mengakses nilai pada alamat yang disimpan oleh pointer.
Visualisasi Konsep #
Diagram berikut memetakan hubungan antara variabel x, pointer p, dan region memori yang sama yang ditunjuk keduanya:
Pada diagram di atas, x dan p berbagi stack frame main tetapi memiliki peran berbeda: x menyimpan nilai int (10), sedangkan p menyimpan alamat x. Tanda panah penuh menyatakan bahwa isi p adalah alamat x; tanda panah putus-putus menyatakan jalur yang ditempuh operator *p untuk mengikuti alamat tersebut dan membaca nilai int di lokasi itu. Dengan demikian *p dan x selalu menghasilkan nilai yang sama.
Contoh Pertama: Membaca Nilai dan Alamat #
Kita mulai dengan kode terkecil yang memperlihatkan keduanya sekaligus: variabel biasa x, pointer p yang menunjuk ke x, lalu mencetak nilai x, alamat x, nilai p (yang seharusnya sama dengan alamat x), dan nilai yang ditunjuk oleh *p.
#include <stdio.h>
int main(void) {
/* Variabel biasa: kotak bernama x berisi nilai 10 */
int x = 10;
/* Pointer: slip kertas yang berisi alamat rumah x */
int *p = &x;
printf("Nilai x : %d\n", x);
printf("Alamat x : %p\n", (void*)&x);
printf("Nilai p : %p\n", (void*)p);
printf("Nilai yang ditunjuk *p : %d\n", *p);
return 0;
}Output yang Dihasilkan:
Nilai x : 10
Alamat x : 0x7ffd2b54f8a4
Nilai p : 0x7ffd2b54f8a4
Nilai yang ditunjuk *p : 10Perhatikan bahwa baris “Alamat x” dan “Nilai p” menampilkan deretan heksadesimal yang identik—persis seperti yang diprediksi analogi tadi. Alamat spesifik (misalnya 0x7ffd2b54f8a4) tidak deterministik; angka ini berubah setiap kali program dijalankan karena Address Space Layout Randomization (ASLR) yang aktif pada sistem operasi modern. Yang penting bagi kita adalah kedua baris tersebut selalu sama dalam satu eksekusi karena p benar-benar menyimpan alamat x.
Mari kita telaah baris-baris kode di atas:
- Baris 4 (
int x = 10;): deklarasi variabelxbertipeintsekaligus inisialisasi nilai10. Compiler mengalokasikan 4 byte di stack untukx. - Baris 7 (
int *p = &x;): deklarasi pointerpbertipeint *(pointer keint) dan mengisinya dengan alamatxmenggunakan operator&. Tipeint *memberitahu compiler bahwa byte-byte pada alamat yang ditunjuk harus ditafsirkan sebagaiint. - Baris 9 (
printf("Nilai x ...", x);): mencetak nilai variabelxsecara langsung—tidak melibatkan pointer sama sekali. - Baris 10 (
printf("Alamat x ... %p", (void*)&x);): format specifier%pkhusus untuk mencetak alamat pointer. Standar C mengharuskan argumen dikonversi kevoid*terlebih dahulu—itulah sebabnya kita menulis(void*)&x. Tanpa cast ini, GCC dapat memunculkan peringatan-Wformat. - Baris 12 (
printf("... *p : %d", *p);): operator*melakukan dereference: membaca nilai bertipeintpada alamat yang tersimpan dip. Hasilnya sama dengan nilaixyaitu10.
Contoh Kedua: Mengubah Nilai Melalui Pointer #
Pointer bukan sekadar cara membaca dari jarak jauh. Karena pointer menyimpan alamat, kita juga bisa menulis ke alamat tersebut dengan operator * di sisi kiri penugasan (*ptr = ...). Perubahan ini langsung terlihat di variabel aslinya karena keduanya menunjuk memori yang sama.
#include <stdio.h>
int main(void) {
int skor = 75;
int *ptr = &skor;
printf("Sebelum diubah -- skor: %d, *ptr: %d\n", skor, *ptr);
/* Ubah nilai skor melalui pointer */
*ptr = 90;
printf("Setelah *ptr = 90 -- skor: %d, *ptr: %d\n", skor, *ptr);
return 0;
}Output yang Dihasilkan:
Sebelum diubah -- skor: 75, *ptr: 75
Setelah *ptr = 90 -- skor: 90, *ptr: 90Penugasan *ptr = 90; tidak mengubah isi pointer ptr itu sendiri (alamat masih tetap), melainkan menulis nilai 90 ke alamat yang ditunjuk. Karena skor menempati alamat tersebut, nilai skor juga berubah menjadi 90. Inilah mengapa pointer sangat penting: ia memungkinkan satu variabel diubah dari banyak tempat di kode, termasuk dari dalam fungsi lain yang menerima pointer sebagai parameter—topik yang akan kita dalami di chapter Pointer sebagai Parameter.