Pointer menyimpan alamat, dan alamat adalah angka, jadi wajar kalau kita berharap aritmetika bisa dipakai padanya. Memang bisa, tapi tidak persis seperti aritmetika integer biasa. C menskalakan setiap operasi pointer dengan ukuran hal yang ditunjuk, sehingga "tambah satu" berubah menjadi sesuatu yang jauh lebih berguna: "pindah ke elemen berikutnya".
Satu keputusan desain itulah yang membuat menelusuri array dengan pointer terasa sealami menelusurinya dengan indeks di C.
Menambah 1 berarti bergeser satu objek, bukan satu byte
Ambil sebuah pointer, tambahkan 1, lalu cetak kedua alamatnya:
int * melompat 4 byte, double * melompat 8, char * melompat 1: masing-masing mendarat tepat di tempat objek berikutnya dengan tipe yang sama akan dimulai. Aturannya sederhana:
ptr + n == (address in ptr) + n * sizeof(*ptr)
Persis karena inilah pointer punya tipe. Alamat polos tanpa tipe tidak akan tahu seberapa jauh "berikutnya" itu.
Menelusuri array dengan pointer
Sekarang hasilnya. Elemen-elemen array terletak di memori yang berurutan, jadi melangkahkan pointer akan mengunjunginya secara berurutan:
Kedua loop mencetak lima angka yang sama. Di loop kedua, scores berubah (decay) menjadi pointer ke elemen pertamanya, p++ maju sebesar satu int, dan *p membaca elemen yang ada di sana. Kalau digambarkan di memori:
scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
| 88 | 92 | 75 | 60 | 100 |
+---------+---------+---------+---------+---------+
^ ^ ^
p p+1 scores+5
(start) (4 bytes later) (one past the end)
Hubungan antara kedua bentuk itu persis dan layak dihafal: scores[i] didefinisikan sebagai *(scores + i). Penjelasan lebih lanjut ada di halaman pointer dan array.
Increment, decrement, dan jebakan *p++
++ dan -- bekerja pada pointer dengan penskalaan yang sama.
Ekspresi yang membingungkan semua orang adalah *p++. ++ postfix terikat lebih kuat daripada *, jadi artinya "ambil p saat ini, majukan p, lalu dereference nilai yang lama":
Tiga ekspresi, tiga arti yang berbeda:
| Ekspresi | Pointer sesudahnya | Nilai yang dihasilkan |
|---|---|---|
*p++ | maju | elemen sebelum bergeser |
*++p | maju | elemen setelah bergeser |
(*p)++ | tidak berubah | nilai lama, dan elemennya dinaikkan |
*p++ adalah gaya C yang lazim, dan begitulah salinan string dan buffer ditulis, tapi tulislah tanda kurung kalau yang kamu maksud lain.
Mengurangkan dua pointer
Mengurangkan satu pointer dari pointer lain menghasilkan jumlah elemen di antara keduanya, bukan jumlah byte:
Tipe hasilnya adalah ptrdiff_t, tipe integer bertanda dari <stddef.h>, yang dicetak dengan %td. Mengurangkan pointer ke dua array yang berbeda menghasilkan nilai yang tidak terdefinisi: standarnya hanya mendefinisikan operasi ini di dalam satu objek.
Kamu juga bisa mengurangkan integer dari pointer (p - 3 mundur tiga elemen), tapi kamu tidak bisa menjumlahkan dua pointer. "Jumlah dua alamat" tidak bermakna apa-apa.
Membandingkan pointer
Operator relasional bekerja pada pointer ke dalam array yang sama, dan artinya persis seperti yang ditunjukkan tata letak memorinya: p < q bernilai benar saat p menunjuk elemen yang lebih awal.
front < back adalah seluruh cerita tentang kapan loop itu berhenti, dan itu benar karena kedua pointer tetap berada di dalam satu array. == dan != juga cara alami untuk menguji loop terhadap penanda akhirnya, dan itu membawa kita ke aturan yang membuat semua ini sah.
Aturan satu-melewati-akhir
C secara eksplisit mengizinkanmu membentuk pointer ke posisi satu setelah elemen terakhir sebuah array. Kamu boleh menghitungnya, menyimpannya, dan membandingkan dengannya. Kamu tidak boleh melakukan dereference padanya.
int arr[4];
arr[0] arr[1] arr[2] arr[3] (no element)
+------+ +------+ +------+ +------+ +- - - -+
| | | | | | | | | |
+------+ +------+ +------+ +------+ +- - - -+
^ ^
arr arr + 4
legal to use legal to FORM and COMPARE
never legal to dereference
Aturan itulah yang membuat bentuk loop standar ini valid:
for (int *p = arr; p != arr + n; p++) {
/* ... */
}
Di iterasi terakhir p menjadi arr + n, perbandingannya gagal, dan loop selesai, tanpa pernah membaca posisi itu.
Meski begitu, ada dua hal yang tetap undefined. Membentuk pointer dua posisi setelah akhir, atau satu posisi sebelum awal, adalah undefined behavior meskipun kamu tidak pernah melakukan dereference:
int *bad1 = arr + n + 1; // undefined - too far past
int *bad2 = arr - 1; // undefined - before the beginning
Yang kedua penting dalam praktik: loop mundur yang ditulis for (int *p = arr + n - 1; p >= arr; p--) menghitung arr - 1 pada decrement terakhirnya. Kode itu bekerja di semua compiler yang umum, tapi secara formal tetap undefined. Loop mundur yang bersih menghindarinya:
Pointer void dan aritmetika yang tidak bisa dilakukan
void * menyimpan alamat tanpa tipe, dan dengan cara itulah malloc dan qsort tetap generik. Karena tidak ada ukuran elemen, aritmetika pointer pada void * tidak diizinkan oleh standar: cast dulu ke tipe yang konkret (atau ke char * untuk pekerjaan tingkat byte).
void *v = buffer;
// v + 1; // not standard C - no size to scale by
char *b = v;
b + 1; // fine - one byte forward
GCC dan Clang menerima aritmetika void * sebagai ekstensi yang memperlakukannya seperti char *, jadi kode yang bergantung padanya tetap bisa dikompilasi sampai suatu hari di-build di tempat yang lebih ketat. Kompilasi dengan -std=c17 -pedantic kalau kamu ingin diberi tahu.
Apa yang kamu dapatkan dari sini
Aritmetika pointer bukan trik optimasi yang sesekali kamu pakai: ini mekanisme di balik pengindeksan array, penanganan string, dan setiap penelusuran buffer di library standar. strlen adalah pointer yang dimajukan sampai '\0' penutup lalu dikurangkan dari awalnya. memcpy adalah dua pointer yang melangkah bersamaan.
Selanjutnya, lihat bagaimana kesetaraan array dan pointer sebenarnya bekerja, termasuk jebakan sizeof yang menangkap semua orang saat pertama kali mengirim array ke fungsi: pointer dan array.
Pertanyaan yang sering diajukan
Apa yang dilakukan ptr + 1 di C?
Pointer bergeser maju sejauh satu objek, bukan satu byte. Untuk int * di mesin pada umumnya, itu 4 byte; untuk double * itu 8 byte. Compiler mengalikannya dengan sizeof(*ptr) untukmu, dan karena itulah tipe pointer itu penting.
Bisakah dua pointer dikurangkan di C?
Bisa, kalau keduanya menunjuk ke dalam array yang sama. end - start menghasilkan jumlah elemen di antara keduanya, dengan tipe ptrdiff_t (cetak dengan %td). Mengurangkan pointer ke objek yang tidak berhubungan adalah undefined behavior.
Apakah boleh menunjuk satu posisi setelah akhir array?
Boleh: membentuk pointer ke satu elemen setelah elemen terakhir memang diizinkan secara eksplisit, supaya loop seperti for (int *p = arr; p != arr + n; p++) bisa bekerja. Kamu boleh menghitung dan membandingkan pointer itu, tapi melakukan dereference padanya adalah undefined behavior.
Kenapa *p++ tidak sama dengan (*p)++?
*p++ menaikkan pointer lalu melakukan dereference pada nilai lamanya: membaca elemen saat ini, lalu maju. (*p)++ tidak mengubah pointer dan menaikkan nilai yang ditunjuknya. ++ postfix terikat lebih kuat daripada *, jadi tanda kurunglah yang mengubah artinya.