Pointer Fungsi dan Callback

Sintaks pointer fungsi, pola callback, dan struktur kode berbasis tabel fungsi.

Daftar isi

Kode program juga menempati memori, sehingga alamat sebuah fungsi dapat ditampung ke dalam variabel bertipe pointer fungsi. Variabel semacam ini memungkinkan fungsi dipilih saat program berjalan, diteruskan sebagai argumen berupa callback, dan disusun menjadi tabel pemilihan. Topik ini membahas sintaks deklarasinya, pola callback, dan gaya kode berbasis tabel yang menggantikan percabangan panjang.

Sintaks deklarasi pointer fungsi#

Deklarasi pointer fungsi menyebut tipe kembali, nama variabel, dan daftar tipe parameternya, semuanya dibungkus tanda kurung, misalnya int (*op)(int, int). Tanda kurung menentukan maknanya, karena tanpa kurung, int *op(int, int) terbaca sebagai deklarasi fungsi op yang mengembalikan int *. Nama fungsi tanpa tanda kurung sudah berupa alamatnya, sehingga penugasan op = tambah dan op = &tambah sama sahnya. Pemanggilan op(4, 5) serta (*op)(4, 5) juga sama hasilnya.

c ganti-operator.c
#include <stdio.h>

int tambah(int a, int b)
{
    return a + b;
}

int kali(int a, int b)
{
    return a * b;
}

int main(void)
{
    int (*op)(int, int) = tambah;

    printf("op(4, 5)    : %d\n", op(4, 5));
    op = kali;
    printf("op(4, 5)    : %d\n", op(4, 5));
    printf("(*op)(4, 5) : %d\n", (*op)(4, 5));
    return 0;
}
text Keluaran
op(4, 5)    : 9
op(4, 5)    : 20
(*op)(4, 5) : 20

Satu variabel op memanggil fungsi yang berbeda tergantung isi terakhirnya, dan pemanggilan (*op)(4, 5) pada baris terakhir memperlihatkan bentuk eksplisit dengan hasil yang identik. Sifat ini serupa dengan nama array yang meluruh menjadi pointer, sehingga tanda & dan tanda bintang pada fungsi bersifat opsional secara praktis. Yang tidak boleh berbeda adalah tanda tangan fungsinya, yaitu tipe kembali dan daftar parameternya harus cocok dengan deklarasi pointer tersebut.

Deklarasi yang panjang dapat dipersingkat dengan typedef, misalnya typedef int (*Operasi)(int, int); yang menjadikan Operasi nama tipe siap pakai. Setelah tipe tersebut ada, variabelnya cukup dideklarasikan Operasi op; dan parameternya cukup ditulis Operasi proses. Gaya ini biasa dipilih ketika pointer fungsi muncul di banyak tempat, karena tanda tangan fungsinya hanya ditulis satu kali dan pembaruan tipenya cukup dilakukan di satu tempat.

Pola callback#

Callback terjadi ketika sebuah fungsi menerima pointer fungsi sebagai parameter, lalu memanggil fungsi tersebut dari dalam tubuhnya. Pemilik fungsi penerima tidak perlu tahu pekerjaan yang akan dilakukan, karena pekerjaan itu diserahkan oleh pemanggil. Program berikut mengubah seluruh elemen array dengan fungsi apa pun yang cocok tanda tangannya.

c callback-ubah-semua.c
#include <stdio.h>

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

int tanda_absolut(int n)
{
    return n < 0 ? -n : n;
}

/* Menerapkan fungsi proses ke setiap elemen array. */
void ubah_semua(int *data, int n, int (*proses)(int))
{
    for (int i = 0; i < n; i++) {
        data[i] = proses(data[i]);
    }
}

void tampil(const char *label, const int *data, int n)
{
    printf("%s:", label);
    for (int i = 0; i < n; i++) {
        printf(" %d", data[i]);
    }
    printf("\n");
}

int main(void)
{
    int nilai[] = {3, -4, 5, -6};

    ubah_semua(nilai, 4, tanda_absolut);
    tampil("absolut", nilai, 4);

    ubah_semua(nilai, 4, kuadrat);
    tampil("kuadrat", nilai, 4);
    return 0;
}
text Keluaran
absolut: 3 4 5 6
kuadrat: 9 16 25 36

Fungsi ubah_semua tidak pernah diubah sekalipun perilakunya berganti dua kali, karena pergantian itu terjadi pada argumen ketiga. Menambahkan transformasi baru berarti menulis satu fungsi kecil lalu meneruskannya sebagai argumen, tanpa menyentuh kode perulangan. Pola inilah yang digunakan qsort, yaitu pustaka yang menyediakan mekanisme, sedangkan pengguna menyediakan kebijakan.

Pustaka standar menerapkan pola callback yang sama pada beberapa tempat lain, misalnya bsearch yang menggunakan pembanding identik dengan qsort, serta atexit yang mendaftarkan fungsi untuk dipanggil ketika program berakhir. Pola ini dapat dibaca sebagai pembalikan arah kendali, yaitu kode kerangka yang memanggil kode milik pengguna, bukan sebaliknya. Pembalikan arah semacam ini dikenal luas sebagai inversion of control dalam literatur pemrograman, dan callback adalah wujudnya yang paling sederhana dalam C.

Kode berbasis tabel fungsi#

Sekumpulan operasi yang sejenis dapat dikumpulkan dalam array berisi struct yang memadukan nama dan pointer fungsinya. Perulangan di atas tabel menggantikan rantai switch atau if, dan menambah operasi baru cukup dilakukan dengan menambah satu baris tabel. Program berikut menyusun kalkulator sederhana dengan cara ini.

c kalkulator-tabel.c
#include <stdio.h>

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(void)
{
    struct Operasi {
        const char *nama;
        int (*hitung)(int, int);
    };

    struct Operasi tabel[] = {
        {"tambah", tambah},
        {"kurang", kurang},
        {"kali",   kali},
    };
    int banyak = (int)(sizeof tabel / sizeof tabel[0]);

    for (int i = 0; i < banyak; i++) {
        printf("12 %s 6 = %d\n", tabel[i].nama, tabel[i].hitung(12, 6));
    }
    return 0;
}
text Keluaran
12 tambah 6 = 18
12 kurang 6 = 6
12 kali 6 = 72

Struktur tabel juga memisahkan data dari logika, yaitu daftar operasi dapat diubah tanpa menyentuh perulangan yang menjalankannya. Gaya berbasis tabel ini muncul pada banyak program nyata, misalnya menu perintah, penangan sinyal, dan pemetaan nama ke fungsi. Untuk pemilihan yang bergantung masukan pengguna, tabel tetap digunakan dengan pencarian nama terlebih dahulu, lalu pointer fungsinya dipanggil.

Perbandingan dengan switch penting untuk memahami kapan tabel lebih tepat. Percabangan switch memilih berdasarkan nilai yang ditulis saat kompilasi, sedangkan tabel dapat dibangun, diurutkan, atau diisi dari data, dan pointer fungsinya dapat diganti ketika program berjalan. Untuk menu interaktif, tabel juga menyederhanakan pencarian karena nama pilihan cukup dicocokkan dengan perulangan biasa, tanpa rantai if yang memanjang setiap kali pilihan baru ditambahkan. Pilihannya menyesuaikan kebutuhan, yaitu switch untuk pilihan tetap yang sedikit, dan tabel untuk kumpulan operasi yang tumbuh atau berubah.

c Contoh: tanda kurung yang menentukan makna
// tidak-dijalankan
int (*f)(int, int);   /* pointer ke fungsi yang mengembalikan int */
/* int *g(int, int);   makna berbeda: deklarasi fungsi g yang mengembalikan int * */

Kesalahan umum#

  • Menulis deklarasi tanpa tanda kurung pembungkus. Tanpa kurung, deklarasi berubah menjadi fungsi yang mengembalikan pointer, dan pesan error yang muncul sering membingungkan. Biasakan membaca deklarasi dari dalam ke luar, mulai dari nama variabelnya.
  • Memanggil pointer fungsi yang berisi NULL. Seperti pointer data, pointer fungsi yang belum diisi tidak boleh dipanggil, dan kegagalan ini menghentikan program. Inisialisasi pointer fungsi pada saat deklarasi.
  • Mencampur tanda tangan fungsi yang berbeda. Menugaskan fungsi bertipe double (int) ke pointer int (*)(int) menimbulkan peringatan incompatible pointer types dan perilaku yang tidak terdefinisi saat dipanggil. Samakan tipe kembali serta daftar parameternya.
  • Menganggap dua fungsi sejenis memiliki pointer yang saling menggantikan. Dua fungsi dengan tipe kembali yang sama tetap memiliki tipe pointer yang berbeda apabila daftar parameternya berbeda. Cocokkan deklarasi pointer dengan tanda tangan fungsi tujuannya secara persis.

Ringkasan#

  • Deklarasi int (*op)(int, int) menyatakan pointer fungsi, dan tanda kurung itulah yang membedakan deklarasi itu dari fungsi yang mengembalikan pointer.
  • Callback bekerja dengan meneruskan nama fungsi sebagai argumen, sehingga mekanisme dan kebijakan dapat dipisahkan sebagaimana pola qsort.
  • Tabel berisi struct nama dan pointer fungsi menggantikan percabangan panjang serta membuat operasi baru cukup ditambahkan sebagai baris tabel.

Referensi#