Setiap variabel C++ disimpan pada sebuah alamat memori yang pasti, dan pointer adalah variabel yang tugasnya menyimpan alamat tersebut. Topik ini membahas cara memandang memori sebagai deretan kotak bernomor, cara mengambil alamat dengan operator &, cara mendeklarasikan pointer, serta cara membaca dan menulis nilai melalui dereference, yakni pengaksesan nilai lewat alamatnya. Pemahaman ini menjadi fondasi seluruh pembahasan memori pada modul ini, mulai dari aritmetika pointer sampai alokasi dinamis.
Memori sebagai kotak bernomor#
Memori komputer dapat dibayangkan sebagai deretan panjang kotak bernomor. Setiap kotak berukuran satu byte dan memiliki nomor urut yang disebut alamat (address). Sebuah variabel int menempati beberapa kotak berurutan, misalnya empat kotak pada umumnya, dan alamat variabel tersebut adalah nomor kotak pertama dari deret itu. Compiler yang menentukan kotak mana yang dipakai setiap variabel, sehingga program tidak perlu mengetahui angka alamatnya secara persis.
Gambaran kotak bernomor ini menjelaskan perbedaan antara nilai dan alamat. Nilai adalah isi yang disimpan variabel, sedangkan alamat adalah letak isinya di memori. Dua variabel yang nilainya sama tetap berada pada alamat yang berbeda, dan dua alamat yang berdekatan belum tentu menyimpan data yang saling berkaitan. Kebiasaan memisahkan kedua konsep ini membuat pesan error seputar memori jauh lebih mudah dibaca.
Alamat itu sendiri hanyalah bilangan besar yang biasanya ditulis dalam notasi heksadesimal, misalnya saat ditampilkan oleh debugger. Bilangan tersebut tidak membawa makna lain bagi program, sehingga menghafal atau membandingkan angkanya secara manual tidak pernah berguna. Yang bermakna adalah hubungannya, yaitu alamat mana yang menyimpan variabel mana, dan hubungan itulah yang disimpan pointer.
Operator & mengambil alamat#
Operator & yang diletakkan di depan variabel menghasilkan alamat variabel itu, sehingga &nilai dibaca sebagai alamat dari nilai. Operator ini tidak mengubah apa pun, hanya melaporkan letak penyimpanan. Angka alamat berbeda pada setiap eksekusi program, sehingga cara yang tepat untuk memeriksanya adalah perbandingan, misalnya menguji apakah dua ekspresi menghasilkan alamat yang sama.
Program berikut mengambil alamat sebuah int, menyimpannya pada pointer, lalu menulis nilai baru melalui pointer tersebut.
#include <iostream>
int main() {
int nilai = 42;
int* p = &nilai;
std::cout << "Nilai variabel: " << nilai << '\n';
std::cout << "Nilai lewat pointer: " << *p << '\n';
*p = 100;
std::cout << "Setelah *p = 100: " << nilai << '\n';
std::cout << std::boolalpha;
std::cout << "p menunjuk nilai? " << (p == &nilai) << '\n';
return 0;
}Nilai variabel: 42
Nilai lewat pointer: 42
Setelah *p = 100: 100
p menunjuk nilai? trueDeklarasi int* p = &nilai; membuat pointer p yang menyimpan alamat nilai, sedangkan ekspresi *p membaca isi dari alamat yang disimpan p. Penulisan *p = 100 menulis pada alamat itu, sehingga variabel nilai berubah menjadi 100 tanpa disentuh langsung. Perbandingan p == &nilai menghasilkan true karena kedua ekspresi menyatakan alamat yang sama, dan perbandingan semacam ini menjadi cara aman untuk membuktikan hubungan dua pointer tanpa menampilkan angka alamatnya.
Selama variabelnya hidup, alamat sebuah variabel tidak berpindah. Variabel nilai pada program di atas menempati alamat yang sama dari awal sampai akhir fungsi, sehingga pointer yang menyimpan alamat itu tetap sah sepanjang masa hidup variabelnya. Satu pointer juga boleh mengganti tujuannya, yaitu menyimpan alamat variabel lain yang tipenya sama, dan tata cara mengunci perpindahan itu dibahas pada topik const dan pointer.
Deklarasi pointer dan dereference#
Tanda * pada deklarasi dan tanda * pada ekspresi berperan berbeda. Pada deklarasi int* p, tanda itu menyatakan bahwa p bertipe pointer ke int, yaitu variabel yang menyimpan alamat sebuah int. Pada ekspresi *p, tanda itu adalah operator dereference, yakni operator yang membaca atau menulis nilai pada alamat yang ditunjuk. Dua peran ini sering tertukar bagi pembaca baru, sehingga pembedaannya perlu dipegang sejak awal.
Hal lain yang mengejutkan adalah ukurannya yang seragam. Program berikut mengukur tiga pointer dengan tipe yang ditunjuk berbeda.
#include <iostream>
int main() {
int* pInt = nullptr;
double* pDouble = nullptr;
char* pChar = nullptr;
std::cout << "sizeof(int*) = " << sizeof(pInt) << " byte\n";
std::cout << "sizeof(double*) = " << sizeof(pDouble) << " byte\n";
std::cout << "sizeof(char*) = " << sizeof(pChar) << " byte\n";
return 0;
}sizeof(int*) = 8 byte
sizeof(double*) = 8 byte
sizeof(char*) = 8 byteSemua pointer menyimpan hal yang sama, yaitu sebuah alamat, sehingga ukurannya seragam. Pada mesin 64 bit seperti yang dipakai program di atas, satu alamat menempati 8 byte, tidak peduli tipe yang ditunjuknya. Yang membedakan tipe pointer adalah cara byte-byte pada alamat itu ditafsirkan dan seberapa jauh langkahnya saat diaritmetikakan, sebuah perilaku yang dibahas pada topik aritmetika pointer. Penulisan nullptr pada deklarasi di atas menyatakan pointer yang belum menunjuk ke mana pun, dan pembahasannya ada pada topik nullptr dan jebakan pointer.
Pointer ke objek bertipe kelas atau struct menggunakan operator panah -> untuk mengakses anggotanya, sehingga p->isi setara dengan (*p).isi. Bentuk panah ini muncul penuh pada topik pratinjau smart pointer, sedangkan contoh di atas cukup memakai tipe fundamental. Apa pun tipe yang ditunjuk, mekanisme alamat dan dereference-nya tidak berubah.
Kesalahan umum#
Kesalahan pertama menyangkut tata letak tanda * pada deklarasi majemuk. Tanda itu melekat pada nama variabel, bukan pada tipe, sehingga baris int* a, b; membuat a sebagai pointer ke int dan b sebagai int biasa. Baris tersebut sering salah dibaca karena kedengarannya seolah membuat dua pointer. Cara amannya adalah mendeklarasikan satu pointer per baris, misalnya int* a; diikuti int* b;, sehingga niatnya tidak mungkin salah baca.
Kesalahan kedua adalah melakukan dereference pada pointer yang belum diinisialisasi. Pointer seperti itu menyimpan alamat sisa yang tidak dapat diprediksi, dan membaca atau menulis pada alamat tersebut adalah undefined behavior, yakni perilaku yang tidak ditetapkan bahasa dan dapat berubah tanpa peringatan. Setiap pointer sebaiknya diberi nilai sejak deklarasinya, baik alamat variabel yang sah maupun nullptr, agar keadaan kosongnya dinyatakan secara eksplisit dan dapat diperiksa.
Ringkasan#
- Memori dipandang sebagai deretan kotak bernomor, dan alamat adalah nomor kotak tempat sebuah variabel disimpan.
- Operator
&mengambil alamat variabel, sedangkan operator*melakukan dereference, yakni membaca atau menulis nilai pada alamat yang ditunjuk. - Semua pointer menyimpan alamat dengan ukuran yang sama, sedangkan tipe yang ditunjuk menentukan cara isinya ditafsirkan dan jarak langkah aritmetikanya.
- Deklarasi
int* a, b;hanya membuat satu pointer, sehingga satu pointer per baris menjadi kebiasaan yang lebih aman.
Referensi#
- Pointer declaration — cppreference.com — Kaidah deklarasi pointer dan operasi yang sah atas pointer.
- Introduction to pointers — LearnCpp — Pengenalan pointer, operator alamat, dan dereference.