Pointer ke pointer dan function pointer

Pointer bertingkat, callback, dan tabel fungsi.

Daftar isi

Pointer dapat menunjuk ke pointer lain, dan pointer juga dapat menunjuk ke fungsi. Bentuk pertama muncul pada data bertingkat dan pada parameter yang perlu mengganti pointer milik pemanggil, sedangkan bentuk kedua, yang disebut function pointer, menyimpan alamat fungsi sehingga fungsi dapat dikirim sebagai nilai dan dipanggil kemudian. Topik ini membahas deklarasi, dereference, dan pola pemakaian keduanya beserta contoh yang dapat dijalankan.

Pointer ke pointer#

Deklarasi int** pp membuat pointer yang menyimpan alamat pointer lain, sehingga dereference perlu dilakukan dua kali untuk mencapai nilainya. Ekspresi *pp menghasilkan pointer yang ditunjuk, sedangkan **pp menghasilkan nilai akhirnya. Tingkat bertingkat seperti ini bukan konsep baru, melainkan aturan dereference yang diulang, dan setiap tambahan * berarti satu lapisan alamat lagi.

Program berikut menyusun rantai nilai, p, lalu pp, kemudian menulis nilai akhirnya dari ujung rantai.

cpp pointer-bertingkat.cpp
#include <iostream>

int main() {
    int nilai = 12;
    int* p = &nilai;
    int** pp = &p;

    std::cout << "nilai = " << nilai << '\n';
    std::cout << "*p    = " << *p << '\n';
    std::cout << "**pp  = " << **pp << '\n';

    **pp = 20;
    std::cout << "nilai setelah **pp = 20? " << std::boolalpha << (nilai == 20) << '\n';
    return 0;
}
text Keluaran
nilai = 12
*p    = 12
**pp  = 12
nilai setelah **pp = 20? true

Penulisan **pp = 20 menulis nilai akhirnya tanpa menyentuh p maupun nilai secara langsung, dan perbandingan di akhir membuktikan bahwa nilai benar-benar berubah. Rantai yang lebih dalam seperti int*** sah menurut bahasa, tetapi pemakaiannya menandai struktur data yang rumit dan semestinya dihindari bila ada alternatif. Batas yang rasional adalah satu tingkat ekstra, karena struktur yang membutuhkan tiga tanda bintang biasanya perlu dirancang ulang bentuk datanya.

Pemakaian yang wajar untuk pointer bertingkat adalah parameter fungsi yang perlu mengganti pointer milik pemanggil. Kasus yang paling terkenal adalah parameter argv pada fungsi main, yang bertipe char** karena menyimpan alamat elemen pertama dari deretan pointer ke teks argumen. Bentuk bertingkat lain sebaiknya ditinjau ulang desainnya, karena reference atau container pustaka standar biasanya menyederhanakan struktur yang sama dengan lebih aman.

Pemakaian umum lainnya adalah matriks dinamis dua dimensi. Ekspresi int** matriks dapat menampung deretan pointer yang setiap elemennya menunjuk satu baris yang dialokasikan tersendiri. Struktur ini memang sah, tetapi biayanya tinggi, yaitu beberapa alokasi terpisah, pelepasan per baris, dan risiko kebocoran bila satu baris terlewat. Alternatif yang lebih aman adalah satu array datar dengan perhitungan indeks, atau container pustaka standar yang mengelola ruangnya sendiri.

Function pointer#

Function pointer menyimpan alamat sebuah fungsi dan memungkinkan fungsi dipanggil melalui pointer itu. Nama fungsi tanpa tanda kurung meluruh menjadi alamatnya, sehingga inisialisasi cukup ditulis tanpa tanda &. Tipe function pointer harus mencantumkan tipe kembalian dan seluruh tipe parameternya, sehingga deklarasinya terlihat panjang, misalnya int (*f)(int, int).

Program berikut menaruh tiga fungsi dengan tanda tangan yang sama ke dalam satu array, lalu memanggilnya lewat indeks.

cpp tabel-fungsi.cpp
#include <iostream>

int tambah(int a, int b) { return a + b; }
int kurang(int a, int b) { return a - b; }
int kali(int a, int b)   { return a * b; }

int main() {
    int (*operasi[3])(int, int) = {tambah, kurang, kali};
    const char* nama[3] = {"tambah", "kurang", "kali"};

    for (int i = 0; i < 3; ++i) {
        std::cout << nama[i] << "(6, 4) = " << operasi[i](6, 4) << '\n';
    }
    return 0;
}
text Keluaran
tambah(6, 4) = 10
kurang(6, 4) = 2
kali(6, 4) = 24

Array function pointer seperti operasi menyediakan pemilihan perilaku pada saat program berjalan tanpa percabangan yang panjang. Indeks yang dipakai boleh berasal dari masukan pengguna, sehingga pilihan operasi mengikuti masukan pada saat program berjalan. Tanda tangan ketiga fungsi tersebut harus identik, karena satu array hanya dapat menampung function pointer dengan tipe yang sama.

Deklarasi yang panjang dapat dipendekkan dengan alias tipe. Penulisan using Operasi = int (*)(int, int); membuat Operasi berarti satu tipe function pointer, sehingga deklarasi array menjadi Operasi operasi[3];. Alias tidak mengubah mekanismenya sama sekali, melainkan hanya merapikan penulisannya, dan bentuk using ini tersedia sejak C++11.

Function pointer juga dapat dibandingkan dengan nullptr dan satu sama lain. Dua function pointer sama bila keduanya menunjuk fungsi yang sama, dan perbandingan ini berguna untuk menghindari pendaftaran callback yang ganda. Di balik layar, isi pointer itu adalah alamat kode fungsi, tetapi nilai angkanya tidak pernah perlu diperiksa secara manual.

Callback#

Callback adalah fungsi yang dikirim sebagai argumen agar dipanggil oleh fungsi lain pada saat yang tepat. Dalam C++, function pointer menjadi salah satu cara mewujudkannya, dan pustaka standar menawarkan alternatif yang lebih modern berupa lambda dan std::function yang dibahas pada modul pendalaman STL. Meskipun begitu, function pointer tetap digunakan luas pada antarmuka pustaka yang berakar dari bahasa C.

Program berikut mengirim sebuah fungsi sebagai argumen, sehingga fungsi penerima dapat menerapkannya pada setiap elemen array.

cpp callback.cpp
#include <iostream>

void terapkan(int* data, int n, int (*ubah)(int)) {
    for (int i = 0; i < n; ++i) {
        data[i] = ubah(data[i]);
    }
}

int kuadrat(int x) { return x * x; }

int main() {
    int nilai[] = {1, 2, 3, 4};

    terapkan(nilai, 4, kuadrat);

    for (int i = 0; i < 4; ++i) {
        if (i > 0) {
            std::cout << ' ';
        }
        std::cout << nilai[i];
    }
    std::cout << '\n';
    return 0;
}
text Keluaran
1 4 9 16

Fungsi terapkan tidak mengetahui keberadaan kuadrat, tetapi tetap dapat mengubah seluruh elemen array melalui parameter int (*ubah)(int). Pola ini memisahkan algoritma penyusuran dari operasi yang diterapkan pada tiap elemen, sehingga operasi lain, misalnya penggandaan, cukup dikirim sebagai argumen baru tanpa menulis ulang fungsi penyusurannya.

Parameter callback yang boleh kosong dinyatakan dengan nilai bawaan nullptr, misalnya int (*ubah)(int) = nullptr. Fungsi penerima kemudian memeriksa parameternya sebelum memanggil, dan melewati pemanggilan bila tidak ada operasi yang dikirim. Pola ini membuat callback menjadi pelengkap yang opsional, bukan syarat yang wajib dipenuhi pemanggil.

Kesalahan umum#

Deklarasi int* f(int); bukan function pointer, melainkan deklarasi fungsi yang mengembalikan int*. Tanda kurung pada (*f) wajib ditulis karena tanda * tanpa kurung terikat pada tipe kembalian. Aturan prioritas ini menjadikan deklarasi function pointer terlihat janggal, tetapi posisi kurungnya tidak boleh digeser, sebab satu perpindahan kurung mengubah arti deklarasi seluruhnya.

Function pointer yang belum diinisialisasi atau bernilai nullptr tidak boleh dipanggil, dan pemanggilan tersebut termasuk undefined behavior. Pemeriksaan if (f != nullptr) sebelum pemanggilan menjadi kebiasaan yang aman, terutama pada callback yang boleh tidak diisi. Kebiasaan inilah yang sama dengan pemeriksaan null pada topik nullptr, hanya saja yang diperiksa adalah alamat fungsi, bukan alamat data.

Ringkasan#

  • int** pp menyimpan alamat pointer lain, dan dua kali dereference diperlukan untuk mencapai nilainya.
  • Nama fungsi meluruh menjadi alamat, sehingga function pointer dideklarasikan dengan tipe kembalian dan parameter yang cocok persis.
  • Array function pointer dan parameter callback memungkinkan pemilihan perilaku pada saat program berjalan.
  • Tanda kurung pada (*f) membedakan pointer ke fungsi dengan fungsi yang mengembalikan pointer.

Referensi#