Bagaimana fungsi pointer dalam C berfungsi?

1234

Saya memiliki beberapa pengalaman belakangan ini dengan fungsi pointer di C.

Jadi melanjutkan dengan tradisi menjawab pertanyaan Anda sendiri, saya memutuskan untuk membuat ringkasan kecil dari dasar-dasar yang sangat, bagi mereka yang membutuhkan menyelam cepat ke subjek.

Yuval Adam
sumber
35
Juga: Untuk sedikit analisis mendalam tentang pointer C, lihat blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Selain itu, Pemrograman dari Bawah ke Atas menunjukkan cara kerjanya pada level mesin. Memahami "model memori" C sangat berguna untuk memahami cara kerja pointer C.
Abbafei
8
Info bagus Dengan judulnya, saya akan berharap untuk benar-benar melihat penjelasan tentang bagaimana "fungsi pointer bekerja", bukan bagaimana mereka dikodekan :)
Bogdan Alexandru

Jawaban:

1478

Pointer fungsi dalam C

Mari kita mulai dengan fungsi dasar yang akan kita tunjukkan :

int addInt(int n, int m) {
    return n+m;
}

Hal pertama, mari kita tentukan pointer ke fungsi yang menerima 2 intdetik dan mengembalikan int:

int (*functionPtr)(int,int);

Sekarang kita dapat dengan aman menunjuk ke fungsi kita:

functionPtr = &addInt;

Sekarang kita memiliki pointer ke fungsi, mari kita gunakan:

int sum = (*functionPtr)(2, 3); // sum == 5

Melewati pointer ke fungsi lain pada dasarnya sama:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

Kita juga bisa menggunakan fungsi pointer dalam mengembalikan nilai (cobalah untuk menjaga, itu menjadi berantakan):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

Tetapi jauh lebih baik menggunakan typedef:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}
Yuval Adam
sumber
19
Terima kasih atas info hebatnya. Bisakah Anda menambahkan beberapa wawasan tentang di mana pointer fungsi digunakan atau kebetulan sangat berguna?
Rich.Carpenter
326
"functionPtr = & addInt;" dapat juga ditulis (dan seringkali) sebagai "functionPtr = addInt;" yang juga berlaku karena standar mengatakan bahwa nama fungsi dalam konteks ini dikonversi ke alamat fungsi.
hlovdal
22
hlovdal, dalam konteks ini menarik untuk menjelaskan bahwa inilah yang memungkinkan seseorang untuk menulis functionPtr = ****************** addInt;
Johannes Schaub - litb
105
@ Rich.Carpenter Saya tahu ini terlambat 4 tahun, tetapi saya pikir orang lain akan mendapat manfaat dari ini: Function pointer berguna untuk melewatkan fungsi sebagai parameter ke fungsi lainnya . Butuh banyak pencarian saya untuk menemukan jawaban itu untuk beberapa alasan aneh. Jadi pada dasarnya, ini memberikan fungsionalitas kelas semu C pseudo.
giant91
22
@ Rich.Carpenter: pointer fungsi bagus untuk deteksi CPU runtime. Miliki banyak versi dari beberapa fungsi untuk memanfaatkan SSE, popcnt, AVX, dll. Saat memulai, setel pointer fungsi Anda ke versi terbaik dari setiap fungsi untuk CPU saat ini. Dalam kode Anda yang lain, panggil saja melalui pointer fungsi alih-alih memiliki cabang bersyarat pada fitur CPU di mana-mana. Kemudian Anda dapat melakukan logika yang rumit tentang memutuskan dengan baik, meskipun CPU ini mendukung pshufb, itu lambat, jadi implementasi sebelumnya masih lebih cepat. x264 / x265 menggunakan ini secara luas, dan bersifat open source.
Peter Cordes
304

Pointer fungsi dalam C dapat digunakan untuk melakukan pemrograman berorientasi objek dalam C.

Misalnya, baris berikut ditulis dalam C:

String s1 = newString();
s1->set(s1, "hello");

Ya, ->dan kurangnya newoperator adalah hadiah mati, tetapi tampaknya menyiratkan bahwa kita sedang mengatur teks beberapa Stringkelas menjadi "hello".

Dengan menggunakan fungsi pointer, adalah mungkin untuk meniru metode dalam C .

Bagaimana ini dicapai?

The Stringkelas sebenarnya adalah structdengan sekelompok pointer fungsi yang bertindak sebagai cara untuk metode simulasi. Berikut ini adalah deklarasi sebagian dari Stringkelas:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

Seperti dapat dilihat, metode Stringkelas sebenarnya adalah pointer fungsi ke fungsi yang dideklarasikan. Dalam menyiapkan instance dari String, newStringfungsi dipanggil untuk mengatur pointer fungsi ke fungsi masing-masing:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Misalnya, getStringfungsi yang dipanggil dengan memanggil getmetode didefinisikan sebagai berikut:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Satu hal yang dapat diperhatikan adalah bahwa tidak ada konsep instance dari objek dan memiliki metode yang sebenarnya merupakan bagian dari objek, sehingga "objek diri" harus diteruskan pada setiap doa. (Dan internalitu hanya tersembunyi structyang dihilangkan dari daftar kode sebelumnya - ini adalah cara melakukan penyembunyian informasi, tetapi itu tidak relevan dengan pointer fungsi.)

Jadi, daripada bisa melakukan s1->set("hello");, seseorang harus meneruskan objek untuk melakukan tindakan s1->set(s1, "hello").

Dengan penjelasan minor harus lulus dalam referensi untuk diri sendiri keluar dari jalan, kami akan pindah ke bagian berikutnya, yaitu warisan di C .

Katakanlah kita ingin membuat subkelas dari String, katakanlah ImmutableString. Agar string tidak dapat diubah, setmetode ini tidak akan dapat diakses, dengan tetap mempertahankan akses ke getdan length, dan memaksa "konstruktor" untuk menerima char*:

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

Pada dasarnya, untuk semua subclass, metode yang tersedia adalah pointer fungsi sekali lagi. Kali ini, pernyataan untuk setmetode ini tidak ada, oleh karena itu, tidak dapat disebut dalam a ImmutableString.

Adapun implementasi ImmutableString, satu-satunya kode yang relevan adalah fungsi "konstruktor", yaitu newImmutableString:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

Dalam instantiating ImmutableString, fungsi pointer ke getdan lengthmetode sebenarnya merujuk ke String.getdan String.lengthmetode, dengan pergi melalui basevariabel yang merupakan Stringobjek yang disimpan secara internal .

Penggunaan pointer fungsi dapat mencapai pewarisan metode dari superclass.

Kita bisa lanjut terus polimorfisme di C .

Jika misalnya kita ingin mengubah perilaku lengthmetode untuk mengembalikan 0semua waktu di ImmutableStringkelas karena alasan tertentu, semua yang harus dilakukan adalah:

  1. Tambahkan fungsi yang akan berfungsi sebagai lengthmetode utama .
  2. Pergi ke "constructor" dan atur fungsi pointer ke lengthmetode utama .

Menambahkan lengthmetode override di ImmutableStringdapat dilakukan dengan menambahkan lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Kemudian, penunjuk fungsi untuk lengthmetode dalam konstruktor dihubungkan ke lengthOverrideMethod:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

Sekarang, daripada memiliki perilaku yang identik untuk lengthmetode di ImmutableStringkelas sebagai Stringkelas, sekarang lengthmetode akan merujuk ke perilaku yang didefinisikan dalam lengthOverrideMethodfungsi.

Saya harus menambahkan penafian bahwa saya masih belajar bagaimana menulis dengan gaya pemrograman berorientasi objek dalam C, jadi mungkin ada poin yang saya tidak jelaskan dengan baik, atau mungkin hanya meleset dalam hal cara terbaik untuk mengimplementasikan OOP di C. Tapi tujuan saya adalah mencoba mengilustrasikan salah satu dari banyak kegunaan pointer fungsi.

Untuk informasi lebih lanjut tentang cara melakukan pemrograman berorientasi objek di C, silakan merujuk ke pertanyaan berikut:

coobird
sumber
22
Jawaban ini mengerikan! Tidak hanya itu menyiratkan bahwa OO entah bagaimana tergantung pada notasi titik, itu juga mendorong memasukkan sampah ke objek Anda!
Alexei Averchenko
27
Ini OO baik-baik saja, tetapi tidak di dekat O-style C. Apa yang telah Anda implementasikan secara salah adalah OO berbasis prototipe gaya-Javascript. Untuk mendapatkan O ++ C-style / Pascal, Anda harus: 1. Memiliki const struct untuk tabel virtual setiap kelas dengan anggota virtual. 2. Memiliki pointer ke struct di objek polimorfik. 3. Panggil metode virtual melalui tabel virtual, dan semua metode lain secara langsung - biasanya dengan tetap berpegang pada beberapa ClassName_methodNamekonvensi penamaan fungsi. Hanya kemudian Anda mendapatkan runtime dan biaya penyimpanan yang sama seperti yang Anda lakukan di C ++ dan Pascal.
Pasang kembali Monica
19
Bekerja dengan OO dengan bahasa yang tidak dimaksudkan sebagai OO selalu merupakan ide yang buruk. Jika Anda ingin OO dan masih memiliki C hanya bekerja dengan C ++.
rbaleksandar
20
@rbaleksandar Katakan itu kepada pengembang kernel Linux. "Selalu ide yang buruk" sepenuhnya merupakan pendapat Anda, yang dengan tegas saya tidak setuju.
Jonathon Reinhart
6
Saya suka jawaban ini tetapi jangan melemparkan malloc
cat
227

Panduan untuk dipecat: Cara menyalahgunakan pointer fungsi di GCC pada mesin x86 dengan mengkompilasi kode Anda dengan tangan:

Literal string ini adalah byte dari kode mesin x86 32-bit. 0xC3adalah instruksi x86ret .

Anda biasanya tidak akan menulis ini dengan tangan, Anda akan menulis dalam bahasa assembly dan kemudian menggunakan assembler suka nasmmerakitnya menjadi biner datar yang Anda hexdump menjadi string C literal.

  1. Mengembalikan nilai saat ini pada register EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Tulis fungsi swap

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
    
  3. Tulis penghitung for-loop ke 1000, memanggil beberapa fungsi setiap kali

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. Anda bahkan dapat menulis fungsi rekursif yang dihitung hingga 100

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);
    

Perhatikan bahwa kompiler menempatkan string literal di .rodatabagian (atau .rdatapada Windows), yang ditautkan sebagai bagian dari segmen teks (bersama dengan kode untuk fungsi).

Segmen teks memiliki izin Baca + Exec, jadi casting string literal ke fungsi pointer berfungsi tanpa perlu mprotect()atau VirtualProtect()panggilan sistem seperti yang Anda perlukan untuk memori yang dialokasikan secara dinamis. (Atau gcc -z execstackmenautkan program dengan tumpukan + segmen data + tumpukan dapat dieksekusi, sebagai peretasan cepat.)


Untuk membongkar ini, Anda dapat mengkompilasi ini untuk memberi label pada byte, dan menggunakan disassembler.

// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";

Kompilasi gcc -c -m32 foo.cdan pembongkaran dengan objdump -D -rwC -Mintel, kita bisa mendapatkan perakitan, dan mengetahui bahwa kode ini melanggar ABI dengan mengalahkan EBX (register panggilan-diawetkan) dan umumnya tidak efisien.

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

Kode mesin ini akan (mungkin) bekerja dalam kode 32-bit pada Windows, Linux, OS X, dan sebagainya: konvensi pemanggilan default pada semua OS tersebut meneruskan argumen pada stack alih-alih lebih efisien dalam register. Tetapi EBX terpelihara dengan baik dalam semua konvensi panggilan biasa, jadi menggunakannya sebagai register awal tanpa menyimpan / mengembalikannya dapat dengan mudah membuat pemanggil macet.

Lee
sumber
8
Catatan: ini tidak berfungsi jika Pencegahan Eksekusi Data diaktifkan (mis. Pada Windows XP SP2 +), karena string C biasanya tidak ditandai sebagai executable.
SecurityMatt
5
Hai Matt! Bergantung pada level pengoptimalan, GCC akan sering memasukkan konstanta string ke dalam segmen TEXT, jadi ini akan berfungsi bahkan pada versi windows yang lebih baru asalkan Anda tidak melarang jenis optimasi ini. (IIRC, versi MINGW pada saat posting saya lebih dari dua tahun yang lalu inline literal string pada tingkat optimasi default)
Lee
10
bisakah seseorang tolong jelaskan apa yang terjadi di sini? Apa literal string yang tampak aneh itu?
ajay
56
@ Jay Sepertinya dia menulis nilai hexidecimal mentah (misalnya '\ x00' sama dengan '/ 0', keduanya sama dengan 0) ke dalam sebuah string, kemudian melemparkan string ke dalam pointer fungsi C, kemudian mengeksekusi pointer fungsi C karena dia iblis.
ejk314
3
hai FUZxxl, saya pikir ini mungkin bervariasi berdasarkan pada kompiler dan versi sistem operasi. Kode di atas tampaknya berjalan dengan baik di codepad.org; codepad.org/FMSDQ3ME
Lee
115

Salah satu kegunaan favorit saya untuk pointer fungsi adalah iterator yang murah dan mudah -

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}
Nick
sumber
7
Anda juga harus meneruskan pointer ke data yang ditentukan pengguna jika Anda ingin mengekstrak output apa pun dari iterasi (pikirkan penutupan).
Alexei Averchenko
1
Sepakat. Semua iterator saya terlihat seperti ini: int (*cb)(void *arg, ...). Nilai pengembalian iterator juga membuat saya berhenti lebih awal (jika bukan nol).
Jonathon Reinhart
24

Pointer fungsi menjadi mudah untuk dideklarasikan setelah Anda memiliki deklarator dasar:

  • id: ID: ID adalah
  • Pointer: *D: pointer D untuk
  • Fungsi: D(<parameters>): D fungsi taking <parameter >kembali

Sedangkan D adalah deklarator lain yang dibuat menggunakan aturan yang sama. Pada akhirnya, di suatu tempat, berakhir dengan ID(lihat di bawah untuk contoh), yang merupakan nama entitas yang dinyatakan Mari kita coba membangun fungsi dengan mengambil pointer ke fungsi yang tidak mengambil apa pun dan mengembalikan int, dan mengembalikan sebuah pointer ke fungsi mengambil char dan mengembalikan int. Dengan tipe-def seperti ini

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Seperti yang Anda lihat, sangat mudah untuk membuatnya menggunakan typedefs. Tanpa typedef, tidak sulit juga dengan aturan deklarator di atas, diterapkan secara konsisten. Seperti yang Anda lihat saya melewatkan bagian penunjuk menunjuk ke, dan hal fungsi kembali. Itulah yang muncul di bagian paling kiri dari deklarasi, dan tidak menarik: itu ditambahkan pada akhirnya jika seseorang sudah membangun deklarator. Ayo lakukan itu. Membangunnya secara konsisten, bertele-tele pertama - menunjukkan struktur menggunakan [dan ]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

Seperti yang Anda lihat, seseorang dapat mendeskripsikan suatu tipe sepenuhnya dengan menambahkan deklarator satu demi satu. Konstruksi dapat dilakukan dengan dua cara. Salah satunya adalah bottom-up, dimulai dengan hal yang sangat benar (daun) dan bekerja hingga pengidentifikasi. Cara lainnya adalah top-down, mulai dari pengenal, bekerja hingga daun. Saya akan menunjukkan dua cara.

Bawah ke Atas

Konstruksi dimulai dengan benda di kanan: Benda dikembalikan, yang merupakan fungsi pengambilan char. Untuk menjaga agar deklarator tetap berbeda, saya akan beri nomor:

D1(char);

Memasukkan parameter char secara langsung, karena sepele. Menambahkan pointer ke deklarator dengan mengganti D1dengan *D2. Perhatikan bahwa kita harus membungkus tanda kurung *D2. Itu bisa diketahui dengan melihat prioritas *-operatoroperator dan fungsi-panggilan (). Tanpa tanda kurung, kompiler akan membacanya sebagai *(D2(char p)). Tapi itu tidak akan menjadi pengganti D1 *D2lagi, tentu saja. Tanda kurung selalu diizinkan di sekitar deklarator. Jadi Anda tidak membuat kesalahan apa pun jika Anda menambahkannya terlalu banyak, sebenarnya.

(*D2)(char);

Jenis pengembalian selesai! Sekarang, mari kita ganti D2dengan fungsi deklarator fungsi mengambil <parameters>kembali , D3(<parameters>)yang kita sekarang.

(*D3(<parameters>))(char)

Perhatikan bahwa tidak ada tanda kurung yang diperlukan, karena kami ingin D3 menjadi deklarator fungsi dan bukan deklarator pointer kali ini. Hebat, satu-satunya yang tersisa adalah parameter untuk itu. Parameter dilakukan persis sama seperti yang kita lakukan pada tipe pengembalian, hanya dengan chardiganti oleh void. Jadi saya akan menyalinnya:

(*D3(   (*ID1)(void)))(char)

Saya sudah diganti D2oleh ID1, karena kita sudah selesai dengan parameter itu (itu sudah pointer ke fungsi - tidak perlu deklarator lain). ID1akan menjadi nama parameter. Sekarang, saya katakan di atas pada bagian akhir menambahkan tipe yang diubah oleh semua deklarator - yang muncul di paling kiri dari setiap deklarasi. Untuk fungsi, itu menjadi tipe pengembalian. Untuk pointer yang menunjuk untuk mengetik dll ... Sangat menarik ketika menuliskan tipe, itu akan muncul dalam urutan yang berlawanan, di bagian paling kanan :) Pokoknya, menggantikannya menghasilkan deklarasi lengkap. Kedua kali inttentu saja.

int (*ID0(int (*ID1)(void)))(char)

Saya telah memanggil pengenal fungsi ID0pada contoh itu.

Perintahkan ke bawah

Ini dimulai dari pengidentifikasi di paling kiri dalam deskripsi jenis, membungkus deklarator itu saat kami berjalan melalui kanan. Mulailah dengan fungsi yang mengembalikan <parameter>

ID0(<parameters>)

Hal berikutnya dalam deskripsi (setelah "kembali") adalah penunjuk ke . Mari menggabungkannya:

*ID0(<parameters>)

Kemudian hal berikutnya adalah fungsi mengambil <parameter >kembali . Parameternya adalah char sederhana, jadi kami langsung memasukkannya lagi, karena ini benar-benar sepele.

(*ID0(<parameters>))(char)

Perhatikan tanda kurung kami menambahkan, karena kita lagi mau bahwa *mengikat pertama, dan kemudian para (char). Jika tidak maka akan membaca fungsi mengambil <parameter >kembali fungsi ... . Noes, fungsi yang mengembalikan fungsi bahkan tidak diperbolehkan.

Sekarang kita hanya perlu meletakkan <parameter >. Saya akan menunjukkan versi singkat dari deriveration, karena saya pikir Anda sudah memiliki ide untuk melakukannya.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Letakkan di intdepan deklarator seperti yang kami lakukan dengan bottom-up, dan kami selesai

int (*ID0(int (*ID1)(void)))(char)

Yang menyenangkan

Apakah bottom-up atau top-down lebih baik? Saya sudah terbiasa dengan bottom-up, tetapi beberapa orang mungkin lebih nyaman dengan top-down. Ini masalah selera saya pikir. Secara kebetulan, jika Anda menerapkan semua operator dalam deklarasi itu, Anda akan mendapatkan int:

int v = (*ID0(some_function_pointer))(some_char);

Itu adalah properti bagus dari deklarasi di C: Deklarasi ini menegaskan bahwa jika operator tersebut digunakan dalam ekspresi menggunakan pengenal, maka itu menghasilkan tipe di paling kiri. Seperti itu juga untuk array.

Semoga Anda menyukai tutorial kecil ini! Sekarang kita dapat menautkan ini ketika orang bertanya-tanya tentang sintaksis deklarasi fungsi yang aneh. Saya mencoba untuk menempatkan internal C sesedikit mungkin. Jangan ragu untuk mengedit / memperbaiki hal-hal di dalamnya.

Johannes Schaub - litb
sumber
24

Penggunaan lain yang bagus untuk pointer fungsi:
Beralih antar versi tanpa rasa sakit

Mereka sangat berguna untuk digunakan ketika Anda ingin fungsi yang berbeda di waktu yang berbeda, atau fase pengembangan yang berbeda. Misalnya, saya sedang mengembangkan aplikasi pada komputer host yang memiliki konsol, tetapi rilis akhir dari perangkat lunak akan diletakkan pada Avnet ZedBoard (yang memiliki port untuk tampilan dan konsol, tetapi tidak diperlukan / diinginkan untuk rilis final). Jadi selama pengembangan, saya akan gunakan printfuntuk melihat status dan pesan kesalahan, tetapi ketika saya selesai, saya tidak ingin apapun dicetak. Inilah yang telah saya lakukan:

version.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

Dalam version.cI akan mendefinisikan 2 fungsi prototipe hadirversion.h

version.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Perhatikan bagaimana pointer fungsi di prototipe version.hsebagai

void (* zprintf)(const char *, ...);

Ketika direferensikan dalam aplikasi, itu akan mulai mengeksekusi di mana pun itu menunjuk, yang belum didefinisikan.

Dalam version.c, perhatikan di board_init()fungsi di mana zprintfditugaskan fungsi unik (yang tanda tangan fungsinya cocok) tergantung pada versi yang didefinisikan dalamversion.h

zprintf = &printf; zprintf memanggil printf untuk tujuan debugging

atau

zprintf = &noprint; zprintf hanya kembali dan tidak akan menjalankan kode yang tidak perlu

Menjalankan kode akan terlihat seperti ini:

mainProg.c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

Kode di atas akan digunakan printfjika dalam mode debug, atau tidak melakukan apa pun jika dalam mode rilis. Ini jauh lebih mudah daripada melalui seluruh proyek dan mengomentari atau menghapus kode. Semua yang perlu saya lakukan adalah mengubah versi version.hdan kode akan melakukan sisanya!

Zack Sheffield
sumber
4
Anda berdiri untuk kehilangan banyak waktu kinerja. Sebagai gantinya Anda bisa menggunakan makro yang mengaktifkan dan menonaktifkan bagian kode berdasarkan Debug / Rilis.
AlphaGoku
19

Pointer fungsi biasanya didefinisikan oleh typedef, dan digunakan sebagai nilai param & return.

Di atas jawaban sudah banyak dijelaskan, saya hanya memberikan contoh lengkap:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}
Eric Wang
sumber
14

Salah satu kegunaan besar untuk pointer fungsi di C adalah untuk memanggil fungsi yang dipilih saat run-time. Misalnya, pustaka run-time C memiliki dua rutinitas, qsortdan bsearch, yang membawa pointer ke fungsi yang dipanggil untuk membandingkan dua item yang sedang diurutkan; ini memungkinkan Anda untuk mengurutkan atau mencari, masing-masing, apa pun, berdasarkan kriteria apa pun yang ingin Anda gunakan.

Contoh yang sangat mendasar, jika ada satu fungsi yang dipanggil print(int x, int y)yang pada gilirannya mungkin perlu memanggil fungsi (baik add()atau sub(), yang dari jenis yang sama) maka apa yang akan kita lakukan, kita akan menambahkan satu argumen penunjuk fungsi ke print()fungsi seperti yang ditunjukkan di bawah ini :

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

Outputnya adalah:

nilainya adalah: 410
nilainya adalah: 390

Vamsi
sumber
10

Mulai dari fungsi awal memiliki Beberapa Alamat Memori Darimana Mereka mulai menjalankan. Dalam Bahasa Perakitan Mereka Disebut sebagai (panggilan "fungsi alamat memori"). Sekarang kembali ke C Jika fungsi memiliki alamat memori maka mereka dapat dimanipulasi oleh Pointer dalam C. Jadi oleh aturan C

1.Pertama Anda perlu mendeklarasikan pointer ke fungsi 2. Lewati Alamat fungsi yang Diinginkan

**** Catatan-> fungsinya harus dari jenis yang sama ****

Program Sederhana ini akan Menggambarkan Setiap Hal.

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

masukkan deskripsi gambar di siniSetelah Itu memungkinkan Lihat Bagaimana mesin Memahami Mereka. Lihatlah instruksi mesin dari program di atas dalam arsitektur 32 bit.

Area tanda merah menunjukkan bagaimana alamat dipertukarkan dan disimpan di eax. Maka mereka adalah instruksi panggilan pada eax. eax berisi alamat fungsi yang diinginkan.

Mohit Dabas
sumber
8

Pointer fungsi adalah variabel yang berisi alamat suatu fungsi. Karena itu adalah variabel pointer meskipun dengan beberapa properti terbatas, Anda dapat menggunakannya cukup banyak seperti Anda akan variabel pointer lainnya dalam struktur data.

Satu-satunya pengecualian yang dapat saya pikirkan adalah memperlakukan pointer fungsi sebagai menunjuk ke sesuatu selain nilai tunggal. Melakukan aritmatika pointer dengan menambah atau mengurangi pointer fungsi atau menambah / mengurangi offset ke pointer fungsi tidak benar-benar bermanfaat karena pointer fungsi hanya menunjuk ke satu hal, titik masuk dari suatu fungsi.

Ukuran variabel pointer fungsi, jumlah byte yang ditempati oleh variabel, dapat bervariasi tergantung pada arsitektur yang mendasarinya, misalnya x32 atau x64 atau apa pun.

Deklarasi untuk variabel penunjuk fungsi perlu menentukan jenis informasi yang sama dengan deklarasi fungsi agar kompiler C melakukan pengecekan seperti biasanya. Jika Anda tidak menentukan daftar parameter dalam deklarasi / definisi pointer fungsi, kompiler C tidak akan dapat memeriksa penggunaan parameter. Ada kasus-kasus ketika kurangnya pemeriksaan ini dapat bermanfaat namun ingatlah bahwa jaring pengaman telah dilepas.

Beberapa contoh:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

Dua deklarasi pertama agak mirip yaitu:

  • funcadalah fungsi yang mengambil intdan char *dan mengembalikanint
  • pFuncadalah fungsi pointer yang ditugaskan alamat sebuah fungsi yang mengambil intdan char *dan kembali sebuahint

Jadi dari atas kita dapat memiliki baris sumber di mana alamat fungsi func()ditugaskan ke variabel pointer fungsi pFuncseperti pada pFunc = func;.

Perhatikan sintaks yang digunakan dengan deklarasi / definisi pointer fungsi di mana tanda kurung digunakan untuk mengatasi aturan prioritas operator alami.

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

Beberapa Contoh Penggunaan Yang Berbeda

Beberapa contoh penggunaan pointer fungsi:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

Anda dapat menggunakan daftar parameter panjang variabel dalam definisi pointer fungsi.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

Atau Anda tidak dapat menentukan daftar parameter sama sekali. Ini bisa bermanfaat tetapi menghilangkan peluang bagi kompiler C untuk melakukan pemeriksaan pada daftar argumen yang disediakan.

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

Pemain gaya C

Anda dapat menggunakan gips gaya C dengan pointer fungsi. Namun perlu diketahui bahwa kompiler C mungkin longgar tentang pemeriksaan atau memberikan peringatan daripada kesalahan.

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

Bandingkan Function Pointer dengan Equality

Anda dapat memeriksa bahwa pointer fungsi sama dengan alamat fungsi tertentu menggunakan ifpernyataan meskipun saya tidak yakin seberapa berguna itu. Operator pembanding lainnya tampaknya memiliki utilitas yang lebih sedikit.

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

Array Pointer Fungsi

Dan jika Anda ingin memiliki array fungsi pointer masing-masing elemen di mana daftar argumen memiliki perbedaan maka Anda dapat mendefinisikan pointer fungsi dengan daftar argumen tidak ditentukan (bukan voidberarti tidak ada argumen tetapi hanya tidak ditentukan) sesuatu seperti berikut ini meskipun Anda mungkin melihat peringatan dari kompiler C. Ini juga berfungsi untuk parameter penunjuk fungsi ke fungsi:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

Gaya C namespaceMenggunakan Global structdengan Function Pointer

Anda dapat menggunakan statickata kunci untuk menentukan fungsi yang namanya lingkup file dan kemudian menetapkan ini ke variabel global sebagai cara menyediakan sesuatu yang mirip dengan namespacefungsi C ++.

Dalam file header, tentukan struct yang akan menjadi namespace kita bersama dengan variabel global yang menggunakannya.

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

Kemudian di file sumber C:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

Ini kemudian akan digunakan dengan menentukan nama lengkap variabel struct global dan nama anggota untuk mengakses fungsi. The constpengubah digunakan pada begitu global yang tidak dapat diubah oleh kecelakaan.

int abcd = FuncThingsGlobal.func1 (a, b);

Area Aplikasi Pointer Fungsi

Komponen pustaka DLL dapat melakukan sesuatu yang mirip dengan namespacependekatan gaya C di mana antarmuka pustaka tertentu diminta dari metode pabrik di antarmuka pustaka yang mendukung pembuatan structpointer fungsi yang mengandung .. Antarmuka pustaka ini memuat versi DLL yang diminta, membuat struct dengan pointer fungsi yang diperlukan, dan kemudian mengembalikan struct ke pemanggil yang meminta untuk digunakan.

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

dan ini dapat digunakan seperti pada:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

Pendekatan yang sama dapat digunakan untuk mendefinisikan lapisan perangkat keras abstrak untuk kode yang menggunakan model tertentu dari perangkat keras yang mendasarinya. Pointer fungsi diisi dengan fungsi khusus perangkat keras oleh pabrik untuk menyediakan fungsionalitas khusus perangkat keras yang mengimplementasikan fungsi yang ditentukan dalam model perangkat keras abstrak. Ini dapat digunakan untuk menyediakan lapisan perangkat keras abstrak yang digunakan oleh perangkat lunak yang memanggil fungsi pabrik untuk mendapatkan antarmuka fungsi perangkat keras tertentu kemudian menggunakan pointer fungsi yang disediakan untuk melakukan tindakan untuk perangkat keras yang mendasarinya tanpa perlu mengetahui detail implementasi tentang target spesifik .

Pointer Fungsi untuk membuat Delegasi, Penangan, dan Callback

Anda dapat menggunakan pointer fungsi sebagai cara untuk mendelegasikan beberapa tugas atau fungsi. Contoh klasik dalam C adalah pointer fungsi delegasi perbandingan yang digunakan dengan fungsi pustaka C Standar qsort()dan bsearch()untuk memberikan urutan pemeriksaan untuk menyortir daftar item atau melakukan pencarian biner pada daftar item yang diurutkan. Delegasi fungsi perbandingan menentukan algoritma pemeriksaan yang digunakan dalam pengurutan atau pencarian biner.

Penggunaan lain mirip dengan menerapkan algoritma ke wadah Perpustakaan Template C ++ Standar.

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

Contoh lain adalah dengan kode sumber GUI di mana handler untuk acara tertentu terdaftar dengan menyediakan pointer fungsi yang sebenarnya dipanggil ketika peristiwa itu terjadi. Kerangka kerja Microsoft MFC dengan peta pesannya menggunakan sesuatu yang mirip untuk menangani pesan Windows yang dikirim ke jendela atau utas.

Fungsi asinkron yang memerlukan panggilan balik mirip dengan pengendali event. Pengguna fungsi asinkron memanggil fungsi asinkron untuk memulai beberapa tindakan dan menyediakan penunjuk fungsi yang akan dipanggil fungsi asinkron setelah aksi selesai. Dalam hal ini acara adalah fungsi asinkron yang menyelesaikan tugasnya.

Richard Chambers
sumber
0

Karena fungsi pointer sering diketik callback, Anda mungkin ingin melihat jenis panggilan balik aman . Hal yang sama berlaku untuk titik masuk, dll fungsi yang bukan panggilan balik.

C cukup plin-plan dan pemaaf sekaligus :)

Pos Tim
sumber