Menu

Pointer dan array di C: decay, arr[i] = *(arr+i), dan jebakan sizeof

Di C, nama array berubah menjadi pointer ke elemen pertamanya hampir di mana pun kamu memakainya. Halaman ini menjelaskan decay itu, kenapa arr[i] secara harfiah sama dengan *(arr+i), kenapa ukuran array harus dikirim terpisah ke fungsi, dan apa bedanya pointer ke array dengan array berisi pointer.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Array dan pointer adalah dua hal yang berbeda di C, tapi sebagian besar waktu perilakunya sama. "Sebagian besar waktu" itulah yang membuat topik ini licin: kode yang memperlakukan array sebagai pointer biasanya bekerja, sampai suatu hari kode itu masuk ke salah satu dari dua tempat di mana perbedaannya penting, dan jawabannya diam-diam salah.

Halaman ini menarik garis pembatasnya dengan tepat.

Array decay: satu aturan di balik semuanya

Di hampir setiap ekspresi, nama array otomatis dikonversi menjadi pointer ke elemen pertamanya. Ini disebut decay. arr menjadi &arr[0], dengan tipe int *.

Nama array dan alamat elemen pertamanya tercetak sama persis. Itulah decay yang sedang bekerja, dan karena itulah kamu bisa memberikan array ke pointer tanpa &.

Hanya ada tepat tiga tempat decay tidak terjadi: sebagai operand sizeof, sebagai operand &, dan saat menginisialisasi array karakter dari string literal. Di tempat lain, anggap saja decay terjadi.

arr[i] secara harfiah adalah *(arr + i)

Standar C mendefinisikan operator subscript lewat aritmetika pointer: a[b] berarti *(a + b). Pengindeksan bukan fitur terpisah: itu hanya singkatan.

Ada dua akibatnya. Pertama, pointer bisa diberi subscript: p[2] adalah *(p + 2), dan karena itulah fungsi yang menerima pointer tetap bisa memakai sintaks arr[i] yang sudah akrab. Kedua, 2[arr] bisa dikompilasi: *(2 + arr) adalah alamat yang sama dengan *(arr + 2). Tidak ada yang menulisnya selain di kuis, tapi itu menjawab pertanyaan apakah pengindeksan "sebenarnya" adalah aritmetika pointer.

Mengirim array ke fungsi

Karena decay, sebuah fungsi tidak pernah menerima array. Fungsi menerima pointer.

Ada tiga hal yang layak diambil dari contoh itu.

Ukuran di int arr[100] hanyalah dokumentasi, tidak lebih. Compiler menulis ulang parameternya menjadi int *arr dan tidak pernah memeriksa apakah kamu benar-benar mengirim 100 elemen.

Panjangnya harus dikirim terpisah. Fungsi hanya punya alamat dan tidak tahu seberapa jauh datanya membentang. Karena itulah hampir semua fungsi C yang menerima array juga menerima jumlah: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). String adalah pengecualian hanya karena membawa penanda akhirnya sendiri, yaitu '\0'.

Mengirim pointer itu murah dan datanya dipakai bersama. Tidak ada salinan array yang dibuat, jadi fungsi bisa mengubah elemen milik pemanggil, dan sering kali memang itulah yang kamu inginkan:

Tandai parameter yang hanya dibaca dengan const int * seperti yang dilakukan print_all. Itu mendokumentasikan maksudnya dan membuat compiler bisa menangkap penulisan yang tidak disengaja.

Jebakan sizeof

Inilah bug yang pernah menjebak setiap programmer C sekali. sizeof adalah salah satu dari dua tempat decay tidak terjadi, jadi operator itu memberi ukuran array yang sebenarnya di tempat array itu dideklarasikan, dan ukuran sebuah pointer di mana pun array itu sudah mengalami decay.

Di main, sizeof data bernilai 40 byte dan panjangnya terhitung 10. Di dalam inspect, sizeof arr adalah ukuran pointer, 8 di mesin 64-bit, dan "panjangnya" keluar sebagai 2. Kodenya kelihatan sama persis dan diam-diam salah.

Aturan yang mengikutinya: hitung panjang array hanya di scope tempat array itu dideklarasikan, lalu kirim nilai itu dari sana.

& pada array bukan tipe yang sama

Tempat lain decay tidak terjadi adalah operator &. &arr adalah pointer ke seluruh array, bukan ke elemen pertamanya. Keduanya menyimpan alamat yang sama; bedanya ada pada arti "tambah 1".

Alamat awalnya sama, langkahnya berbeda. arr bertipe int * dan melangkah satu elemen; &arr bertipe int (*)[5] dan melangkah sebesar seluruh array. Inilah mekanisme yang membuat pengiriman satu baris array 2D bisa bekerja, yang dibahas di array multidimensi.

Pointer ke array vs array berisi pointer

Dua deklarasi yang kelihatan mirip tapi artinya berlawanan:

int *p[5];     // array of 5 pointers to int
int (*q)[5];   // pointer to an array of 5 ints

[] punya prioritas lebih tinggi daripada *, jadi int *p[5] dibaca sebagai "p adalah array, berisi pointer". Tanda kurung di int (*q)[5] memaksa * terikat lebih dulu: "q adalah pointer, ke sebuah array".

int *p[5]                     int (*q)[5]

 p[0] -> [ int ]               q -> [ int | int | int | int | int ]
 p[1] -> [ int ]                     one contiguous block of 5
 p[2] -> [ int ]
 p[3] -> [ int ]
 p[4] -> [ int ]
 five separate addresses

Array berisi pointer adalah yang umum, dan kamu sudah pernah memakainya: char *argv[] adalah array berisi pointer ke string, satu untuk setiap argumen baris perintah. Begitu juga caramu menyimpan daftar string yang panjangnya berbeda-beda tanpa membuang tempat.

Perhatikan (*q)[0]: dereference pointernya untuk mendapat array, lalu indeks array itu. Tanda kurungnya lagi-lagi wajib, dengan alasan prioritas yang sama.

Di mana array dan pointer benar-benar berbeda

Ingat tabel singkat ini dan kebingungannya akan hilang:

Array int arr[10]Pointer int *p
Apa itu10 int yang tersusun di memorisatu variabel yang menyimpan alamat
sizeof40: seluruh blok8: hanya pointernya
Bisa diberi nilai barutidak: arr = x; adalah errorya: p = x; tidak masalah
& menghasilkanint (*)[10]int **
Asal memorinyadideklarasikan bersama arrayke mana pun kamu mengarahkannya

Nama array bukan variabel yang menyimpan alamat: nama array adalah tempat penyimpanannya, dan alamatnya dihitung saat dibutuhkan. Karena itulah kamu tidak bisa memberinya nilai baru.

Pertanyaan yang sering diajukan

Apakah array dan pointer itu sama di C?

Tidak, tapi keduanya mudah tertukar karena nama array berubah (decay) menjadi pointer ke elemen pertamanya di hampir setiap ekspresi. Array sendiri adalah blok elemen dengan ukuran yang diketahui; pointer adalah satu variabel yang menyimpan satu alamat. sizeof dan & adalah dua tempat perbedaannya terlihat.

Kenapa sizeof(arr) memberi jawaban yang salah di dalam fungsi?

Karena parameternya bukan array. void f(int arr[]) diam-diam ditulis ulang menjadi void f(int *arr), jadi sizeof(arr) mengukur sebuah pointer (8 byte di kebanyakan mesin), bukan array aslinya. Kirim panjangnya sebagai parameter terpisah.

Apa bedanya int *p[5] dan int (*p)[5]?

int *p[5] adalah array berisi 5 pointer ke int. int (*p)[5] adalah satu pointer ke array berisi 5 int. Tanda kurung mengikat * ke nama lebih dulu; tanpa tanda kurung, [] yang menang karena prioritasnya lebih tinggi.

Apakah arr[i] benar-benar sama dengan *(arr + i)?

Ya: standar C mendefinisikan a[b] sebagai *(a + b). Karena itu juga i[arr] yang kelihatan aneh bisa dikompilasi dan bekerja: penjumlahan bersifat komutatif, jadi *(i + arr) adalah elemen yang sama. Jangan pernah menulisnya di kode sungguhan, tapi itu membuktikan aturannya.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai