Aritmetika pointer adalah operasi penjumlahan, pengurangan, dan perbandingan atas nilai alamat yang disimpan pointer. C++ menetapkan bahwa satu langkah pointer selalu bergerak seukuran satu elemen, bukan satu byte, sehingga p + 1 berbeda artinya untuk setiap tipe yang ditunjuk. Topik ini membahas perilaku tersebut, hubungan eratnya dengan array, serta proses yang disebut decay, yakni peluruhan nama array menjadi pointer pada banyak konteks.
Satu langkah bergerak seukuran tipe#
Penjumlahan p + 1 pada pointer p menghasilkan alamat elemen berikutnya, bukan alamat satu byte setelahnya. Jarak byte yang ditempuh ditentukan oleh tipe yang ditunjuk, misalnya 4 byte untuk int dan 8 byte untuk double pada mesin 64 bit. Aturan ini menjadikan aritmetika pointer cocok untuk menyusuri deret elemen, karena setiap langkah selalu mendarat tepat pada elemen berikutnya, seberapa pun besar elemennya.
Program berikut menyusuri array lima elemen tanpa notasi indeks sama sekali, lalu mengukur jarak dan ukuran langkahnya.
#include <iostream>
int main() {
int data[] = {10, 20, 30, 40, 50};
int* p = data;
for (int i = 0; i < 5; ++i) {
std::cout << "elemen ke-" << i << " = " << *(p + i) << '\n';
}
std::cout << "Selisih p+3 dengan p: " << (p + 3) - p << " langkah\n";
std::cout << "Satu langkah menempuh " << sizeof(int) << " byte\n";
return 0;
}elemen ke-0 = 10
elemen ke-1 = 20
elemen ke-2 = 30
elemen ke-3 = 40
elemen ke-4 = 50
Selisih p+3 dengan p: 3 langkah
Satu langkah menempuh 4 byteEkspresi *(p + i) mengakses elemen ke-i langsung dari pointer, sehingga perulangan di atas bekerja tanpa menyebut data lagi. Ekspresi (p + 3) - p menghasilkan 3 karena pengurangan dua pointer dihitung dalam satuan elemen, bukan byte, sehingga selisihnya tidak terpengaruh ukuran tipe. Jarak byte per langkah baru terlihat melalui sizeof(int), yang pada program ini bernilai 4.
Pointer juga dapat dibandingkan. Operator < dan > atas dua pointer menentukan urutan posisinya di dalam array yang sama, sedangkan operator == dan != menguji kesamaan alamatnya. Perbandingan inilah yang dipakai perulangan berbasis pointer, misalnya menyusuri array dengan while selama pointer belum mencapai alamat satu posisi setelah elemen terakhir. Perbandingan lintas array tidak terdefinisi, sama seperti pengurangan dua pointer lintas array.
Perulangan menyusuri array pun dapat ditulis tanpa indeks. Pointer mulai dari elemen pertama, bergerak dengan ++p, dan berhenti pada alamat satu posisi setelah elemen terakhir. Bentuk ini umum pada kode C klasik, sedangkan C++ modern lebih memilih indeks atau iterator yang niatnya lebih terbaca.
Array meluruh menjadi pointer#
Nama array pada sebagian besar ekspresi meluruh menjadi pointer ke elemen pertamanya, dan inilah alasan int* p = data; cukup ditulis tanpa tanda &. Peluruhan ini tidak menghapus array-nya, tetapi informasi panjang array tidak lagi tersedia pada pointer hasilnya. Konsekuensinya paling terasa ketika array dikirim ke fungsi, sebagaimana diukur program berikut.
#include <iostream>
void ukuranDari(int* p) {
std::cout << "Di dalam fungsi (parameter int*): " << sizeof(p) << " byte\n";
}
int main() {
int data[] = {4, 8, 15, 16, 23};
std::cout << "Di main (array utuh): " << sizeof(data) << " byte\n";
std::cout << "Satu elemen int: " << sizeof(int) << " byte\n";
ukuranDari(data);
return 0;
}Di main (array utuh): 20 byte
Satu elemen int: 4 byte
Di dalam fungsi (parameter int*): 8 bytePerbedaan pada keluaran di atas muncul dari konteks. Di dalam main, data masih berupa array utuh, sehingga sizeof(data) menghitung seluruh isinya, yaitu 20 byte untuk lima int. Saat array dikirim ke fungsi, yang diterima parameter hanyalah pointer, sehingga sizeof(p) di dalam fungsi menghasilkan 8 byte, yaitu ukuran satu alamat. Karena itu fungsi yang menerima array hampir selalu membutuhkan parameter panjang tersendiri.
Konsekuensi lain dari decay adalah hilangnya kemampuan menyalin array melalui penugasan. Penugasan int* p = data; hanya menyalin alamat, bukan lima elemennya, sehingga dua pointer yang menunjuk array yang sama dapat saling memengaruhi lewat dereference. Penyalinan array yang benar dilakukan elemen demi elemen, atau lebih mudah, dengan container pustaka standar seperti std::array dan std::vector yang memang dirancang untuk itu.
Indeks dan aritmetika pointer yang setara#
Compiler menerjemahkan penulisan data[i] menjadi *(data + i), sehingga notasi indeks hanyalah bentuk lain dari aritmetika pointer. Kedua bentuk tersebut menghasilkan kode yang sama setelah kompilasi, dan pilihan di antara keduanya adalah perkara keterbacaan. Bentuk indeks lebih disarankan untuk kode sehari-hari karena niatnya paling jelas, sedangkan bentuk pointer berguna ketika algoritmanya memang bekerja dengan posisi.
Kesetaraan ini juga menjelaskan mengapa indeks array dimulai dari nol. Ekspresi data[0] berarti *(data + 0), yaitu elemen pada alamat yang sama dengan array itu sendiri. Penomoran dari nol membuat selisih indeks dan selisih langkah pointer bernilai persis sama, sehingga keduanya mudah dipertukarkan saat membaca kode. Bila satu variabel menghitung indeks, variabel itu sekaligus menghitung banyaknya langkah pointer dari elemen pertama.
Satu pengecualian perlu dicatat, yaitu pointer void*. Pointer itu dapat menampung alamat tipe apa pun, tetapi tidak dapat dijumlahkan, dikurangkan, atau di-dereference secara langsung, karena ukuran elemennya tidak diketahui. Aritmetika pointer selalu membutuhkan tipe yang jelas, dan itulah alasan tipe yang ditunjuk menjadi bagian dari deklarasi pointer, bukan sekadar keterangan.
Kesalahan umum#
Aritmetika pointer tidak diperiksa batasnya oleh bahasa, sehingga *(p + 5) pada array lima elemen tetap dikompilasi meskipun elemennya tidak ada. Membaca atau menulis di luar batas array adalah undefined behavior, yakni perilaku yang tidak ditetapkan bahasa dan dapat berubah tanpa peringatan. Setiap langkah pointer perlu dijaga agar tetap berada pada elemen yang ada. Satu-satunya posisi di luar elemen yang masih sah adalah pointer ke satu posisi setelah elemen terakhir (one-past-the-end). Posisi tersebut boleh dibandingkan, tetapi tidak boleh di-dereference.
Kesalahan lain adalah mengurangi dua pointer yang tidak berasal dari array yang sama. Selisih dua pointer hanya bermakna bila keduanya menunjuk elemen pada array yang sama, sedangkan selisih lintas array tidak terdefinisi dan hasilnya tidak memberi informasi apa pun. Apabila perlu menghitung jarak dua posisi, pastikan kedua pointer berasal dari array yang sama, atau bandingkan lewat indeksnya.
Ringkasan#
p + 1bergerak seukuran satu elemen, bukan satu byte, sehingga jarak byte per langkah bergantung pada tipe yang ditunjuk.- Nama array meluruh menjadi pointer ke elemen pertama pada sebagian besar ekspresi, dan informasi panjangnya hilang setelah meluruh.
data[i]identik dengan*(data + i), sehingga indeks nol dan aritmetika pointer saling menguatkan.- Selisih dua pointer dihitung dalam satuan elemen dan hanya sah untuk pointer yang menunjuk array yang sama.
Referensi#
- Arithmetic operators — cppreference.com — Kaidah aritmetika pointer, selisih pointer, dan batas-batasnya.
- Pointer arithmetic and subscripting — LearnCpp — Penjumlahan pointer dan kesetaraannya dengan notasi indeks.
- Array declaration — cppreference.com — Deklarasi array dan konteks yang memicu peluruhan menjadi pointer.