Mengoper Array ke Fungsi

Apa yang sebenarnya diterima fungsi saat array dioper, dan dampaknya pada desain parameter.

Daftar isi

Saat array dioper ke fungsi, yang diterima fungsi bukanlah salinan seluruh isinya. Topik ini membahas apa yang sebenarnya dikirim, yaitu pointer ke elemen pertama, beserta konsekuensinya terhadap desain parameter. Setelah membacanya, parameter array dapat didesain dengan ukuran yang tepat dan efek samping yang terkendali.

Array meluruh menjadi pointer#

Dalam hampir semua ekspresi, nama array otomatis menyusut menjadi pointer ke elemen pertamanya. Proses ini disebut array decay, yaitu peluruhan array menjadi pointer. Ketika cetakJumlah(nilai, 5) dipanggil, fungsi menerima alamat elemen pertama array nilai dan bukan salinan dua puluh byte isinya.

Alasan di balik peluruhan adalah keputusan desain bahasa demi efisiensi. Menyalin seluruh array pada setiap pemanggilan berarti memindahkan byte dalam jumlah besar, sedangkan sebagian besar fungsi cukup membaca atau mengubah isinya di tempat. Karena itu C memilih mengirim alamatnya saja, dan salinan dibuat secara eksplisit apabila memang diperlukan.

int nilai[5] di main7085609075ukuran array: 20 bytecetakJumlah(nilai, 5)yang dikirim: alamatelemen pertamaparameter int nilai[]menyimpan alamatelemen pertamaukuran parameter: 8 byte
Array yang dioper ke fungsi hanya mengirim alamat elemen pertamanya

Konsekuensinya terlihat jelas pada sizeof. Di dalam main, sizeof nilai menghitung ukuran array aslinya, sedangkan di dalam fungsi yang menerima array sebagai parameter, sizeof nilai menghitung ukuran pointer. Program berikut menampilkan keduanya dengan array yang sama.

c peluruhan.c
#include <stdio.h>

void cetakJumlah(int *nilai, int n) {
    /* parameter array dilihat sebagai pointer, sehingga sizeof menghasilkan 8 */
    printf("sizeof nilai di dalam fungsi: %zu byte\n", sizeof nilai);

    int total = 0;
    for (int i = 0; i < n; i++) {
        total += nilai[i];
    }
    printf("Total nilai: %d\n", total);
}

int main(void) {
    int nilai[] = {70, 85, 60, 90, 75};

    printf("sizeof nilai di dalam main  : %zu byte\n", sizeof nilai);
    cetakJumlah(nilai, 5);
    return 0;
}
text Keluaran
sizeof nilai di dalam main  : 20 byte
sizeof nilai di dalam fungsi: 8 byte
Total nilai: 380

Angka 8 byte adalah ukuran satu pointer pada lingkungan 64-bit, sedangkan 20 byte adalah ukuran lima elemen int. Perbedaan itu muncul karena parameter fungsi tidak pernah benar-benar bertipe array, sehingga program di atas sengaja menuliskan parameternya sebagai int *nilai. Apabila parameter tetap dituliskan int nilai[] sementara sizeof dijalankan terhadapnya, gcc 14 memberi peringatan khusus karena hasilnya ukuran pointer dan bukan ukuran array. Ketiga bentuk berikut setara sebagai parameter fungsi.

c Kesetaraan tiga bentuk parameter
// tidak-dijalankan
/* ketiga deklarasi ini setara: semuanya menerima int * */
void f(int arr[5]);
void g(int arr[]);
void h(int *arr);

Menyertakan panjang sebagai parameter#

Karena fungsi hanya menerima alamat, fungsi tidak tahu berapa banyak elemen yang sah untuk dibaca. Konvensi yang dipakai secara luas adalah menyertakan jumlah elemen sebagai parameter terpisah. Parameter panjang inilah yang menjaga perulangan di dalam fungsi tetap berada di dalam batas array pemanggil.

c rata-rata-fungsi.c
#include <stdio.h>

double rataRata(const int nilai[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += nilai[i];
    }
    return (double) total / n;
}

int main(void) {
    int nilai[] = {70, 85, 60, 90, 75};

    printf("Rata-rata = %.2f\n", rataRata(nilai, 5));
    return 0;
}
text Keluaran
Rata-rata = 76.00

Kata kunci const pada parameter const int nilai[] menyatakan janji bahwa fungsi hanya membaca dan tidak mengubah isi array. Apabila isi array diubah dengan sengaja, kompilator menolaknya dengan pesan error. Kebiasaan menambahkan const pada parameter yang hanya dibaca memperjelas maksud setiap fungsi tanpa biaya apa pun.

Nilai n juga wajib sesuai dengan ukuran array aslinya. Mengoper nilai n yang lebih besar dari jumlah elemen nyata membuat perulangan di dalam fungsi membaca di luar batas, sedangkan nilai yang lebih kecil hanya membuat sebagian elemen diabaikan. Konvensi lain yang lazim pada pustaka standar adalah memakai size_t untuk parameter panjang karena tipenya sama dengan hasil sizeof.

Perubahan elemen terlihat oleh pemanggil#

Sisi lain dari pengiriman alamat adalah fungsi dapat mengubah isi array pemanggil. Karena parameter menunjuk memori yang sama, setiap penulisan elemen langsung terlihat setelah fungsi selesai. Perilaku ini berbeda dari variabel biasa yang dioper sebagai salinan nilai.

c naikkan-nilai.c
#include <stdio.h>

void tambahBonus(int nilai[], int n, int bonus) {
    for (int i = 0; i < n; i++) {
        nilai[i] += bonus;
    }
}

int main(void) {
    int nilai[] = {60, 70, 80};

    tambahBonus(nilai, 3, 5);

    for (int i = 0; i < 3; i++) {
        printf("nilai[%d] = %d\n", i, nilai[i]);
    }
    return 0;
}
text Keluaran
nilai[0] = 65
nilai[1] = 75
nilai[2] = 85

Pada program di atas, tambahan lima poin terjadi pada array yang sama dengan milik main, dan bukan pada salinannya. Mekanisme alamat di balik perilaku ini dibahas tuntas pada modul Pointer. Konsekuensi praktisnya sudah dapat dipakai sekarang. Beri const bila fungsi tidak boleh mengubah, dan nyatakan perubahan secara eksplisit pada nama fungsi bila perubahan itu memang disengaja.

Pola ini juga menjelaskan salah satu gaya paling umum di pustaka standar C, yaitu fungsi mengisi array yang disiapkan pemanggil lalu mengembalikan status atau panjang hasilnya. Array tidak dapat menjadi nilai kembali fungsi secara utuh, sehingga pengisian lewat parameter justru menjadi cara standarnya.

Parameter array dua dimensi#

Parameter matriks mencantumkan jumlah kolomnya dan boleh mengosongkan jumlah barisnya. Bentuk int m[][3] menyatakan matriks dengan tiga kolom dan baris sebanyak apa pun yang dioper. Jumlah kolom wajib karena rumus offset byte membutuhkannya, sedangkan jumlah baris tidak dipakai rumus dan cukup dikirim sebagai parameter terpisah.

c matriks-ke-fungsi.c
#include <stdio.h>

void cetakMatriks(int m[][3], int baris) {
    for (int i = 0; i < baris; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%4d", m[i][j]);
        }
        printf("\n");
    }
}

int main(void) {
    int data[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };

    cetakMatriks(data, 2);
    return 0;
}
text Keluaran
   1   2   3
   4   5   6

Bentuk int m[][] tanpa jumlah kolom ditolak kompilator, begitu juga int m[2][]. Berkat aturan ini, ketidakcocokan jumlah kolom antara matriks pemanggil dan parameter fungsi terdeteksi pada saat kompilasi dan tidak menunggu kegagalan saat program berjalan. Matriks int data[2][4] yang dioper ke fungsi bertipe int m[][3] misalnya ditampik dengan peringatan ketidakcocokan tipe.

Menuliskan jumlah baris pada parameter tetap sah, misalnya int m[2][3], dan maknanya sama dengan int m[][3] karena jumlah baris tidak dipakai perhitungan alamat. Jumlah baris yang dioper tetap dikirim lewat parameter terpisah agar fungsi yang sama dapat dipakai untuk matriks dengan tinggi berapa pun, seperti cetakMatriks(data, 2) pada program di atas.

Kesalahan umum#

  • Menghitung ukuran array dengan sizeof di dalam fungsi. Hasilnya ukuran pointer, dan perbaikannya adalah mengoper jumlah elemen sebagai parameter terpisah.
  • Melupakan parameter panjang. Tanpa panjang, fungsi tidak punya cara sah untuk mengetahui batas pembacaan, sehingga perulangannya mudah keluar batas tanpa terdeteksi.
  • Mengira pointer yang dikirim membawa salinan array. Perubahan elemen langsung menyentuh array pemanggil, sehingga fungsi yang seharusnya hanya membaca wajib memakai const agar kesalahan semacam ini tertangkap kompilator.
  • Menuliskan parameter matriks tanpa jumlah kolom. Bentuk int m[][] tidak dapat dikompilasi karena rumus penomoran elemen membutuhkan jumlah kolom.
  • Mengoper nilai panjang yang tidak sesuai dengan array aslinya. Angka yang terlalu besar membuat fungsi membaca di luar batas, dan angka yang terlalu kecil membiarkan sebagian data tidak terolah.

Ringkasan#

  • Array yang dioper ke fungsi meluruh menjadi pointer ke elemen pertama, sehingga fungsi tidak menerima salinan array.
  • Jumlah elemen dikirim sebagai parameter terpisah karena fungsi tidak dapat menghitungnya dari pointernya.
  • Perubahan elemen di dalam fungsi terlihat oleh pemanggil karena keduanya menunjuk memori yang sama.
  • Parameter matriks wajib mencantumkan jumlah kolom, misalnya int m[][3], sedangkan jumlah barisnya boleh dikosongkan.

Referensi#