C中的函数指针如何工作?
我最近在C语言中使用函数指针
因此,继续回答你自己的问题的传统,我决定对那些需要快速进入主题的人做一个基本的小结。
C中的函数指针
我们先来看一个我们将要指出的基本function:
int addInt(int n, int m) { return n+m; }
首先,让我们定义一个指向一个函数的指针,它接收2个int
并返回一个int
:
int (*functionPtr)(int,int);
现在我们可以安全地指向我们的function:
functionPtr = &addInt;
现在我们有了一个指向函数的指针,让我们来使用它:
int sum = (*functionPtr)(2, 3); // sum == 5
将指针传递给另一个函数基本上是相同的:
int add2to3(int (*functionPtr)(int, int)) { return (*functionPtr)(2, 3); }
我们也可以在返回值中使用函数指针(尝试跟上,它变得凌乱):
// 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; }
但是使用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; }
C中的函数指针可以用来在C中执行面向对象的编程。
例如,下面的行用C写成:
String s1 = newString(); s1->set(s1, "hello");
是的, ->
和缺less一个new
操作符是一个死的放弃,但它似乎暗示我们正在设置一些String
类的文本是"hello"
。
通过使用函数指针, 可以在C中模拟方法 。
这是如何完成的?
String
类实际上是一个带有一堆函数指针的struct
,这些函数指针是模拟方法的一种方式。 以下是String
类的部分声明:
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();
可以看出, String
类的方法实际上是指向声明函数的函数指针。 在准备String
的实例时,调用newString
函数来设置指向它们各自函数的函数指针:
String newString() { String self = (String)malloc(sizeof(struct String_Struct)); self->get = &getString; self->set = &setString; self->length = &lengthString; self->set(self, ""); return self; }
例如,调用get
方法调用的getString
函数定义如下:
char* getString(const void* self_obj) { return ((String)self_obj)->internal->value; }
有一件事可以注意到,没有对象实例的概念,并且实际上有方法是对象的一部分,所以每个调用都必须传入一个“自我对象”。 (而internal
只是一个隐藏的struct
,它在代码清单中被忽略了 – 它是一种执行信息隐藏的方法,但与函数指针无关。)
所以,而不是能够做s1->set("hello");
,必须通过对象来执行对s1->set(s1, "hello")
。
有了这个小的解释,我们就不得不通过一个引用来引用自己,我们将转到下一个inheritanceC的部分 。
假设我们想要创build一个String
的子类,比如一个ImmutableString
。 为了使string不可变, set
方法将不可访问,同时保持对get
和length
访问,并强制“构造函数”接受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);
基本上,对于所有的子类,可用的方法都是函数指针。 这一次, set
方法的声明不存在,因此它不能在ImmutableString
调用。
至于ImmutableString
的实现,唯一相关的代码是“构造函数”函数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; }
在实例化ImmutableString
,指向get
和length
方法的函数指针通过遍历base
variables(这是一个内部存储的String
对象)实际上引用String.get
和String.length
方法。
函数指针的使用可以实现父类中方法的inheritance。
我们可以继续在C中进行多态 。
例如,如果我们想要改变length
方法的行为,在ImmutableString
类中一直返回0
,所有必须完成的事情是:
- 添加一个将被用作重载
length
方法的函数。 - 转到“构造函数”并将函数指针设置为覆盖
length
方法。
在ImmutableString
添加重写length
方法可以通过添加一个lengthOverrideMethod
来执行:
int lengthOverrideMethod(const void* self) { return 0; }
然后,构造函数中的length
方法的函数指针被连接到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; }
现在,现在length
方法将引用lengthOverrideMethod
函数中定义的行为,而不是像ImmutableString
类中的length
方法那样具有相同的行为。
我必须添加一个免责声明,我仍然在学习如何用C编写一个面向对象的编程风格,所以可能有点我没有解释得很好,或者可能只是在如何最好地实现面向对象在C中,但我的目的是试图说明函数指针的许多用途之一。
有关如何在C中执行面向对象编程的更多信息,请参阅以下问题:
- 面向对象的C?
- 你能用C编写面向对象的代码吗?
被激发的指南:如何通过手动编译你的代码来滥用x86机器上GCC的函数指针:
-
返回EAX寄存器的当前值
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
-
写一个交换function
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);
-
写一个for循环计数器到1000,每次调用一些函数
((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
-
你甚至可以写一个recursion函数计数到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);
我最喜欢使用函数指针的一个便宜又简单的迭代器 –
#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); }
一旦你有了基本的声明符,函数指针就变得很容易声明:
- ID:
ID
: ID是一个 - 指针:
*D
: D指针 - function:
D(<parameters>)
: D函数以<
参数>
返回
而D是使用这些相同规则构build的另一个声明符。 最后,在某个地方,以ID
结尾(见下面的例子),这是所声明的实体的名称。 让我们尝试构build一个函数,它指向一个没有任何东西并返回int的函数,并返回一个指向一个函数的指针,并返回一个int值。 typesdefs是这样的
typedef int ReturnFunction(char); typedef int ParameterFunction(void); ReturnFunction *f(ParameterFunction *p);
正如你所看到的,使用typedef来构build它非常容易。 如果没有使用typedef,那么使用上面的声明规则并不难,一致的应用。 正如你所看到的,我错过了指针指向的部分以及函数返回的东西。 这就是声明最左边的内容,并不重要:如果已经build立了声明器,则在最后添加它。 我们来做吧 一贯地build立它,首先罗嗦 – 显示使用[
和]
的结构:
function taking [pointer to [function taking [void] returning [int]]] returning [pointer to [function taking [char] returning [int]]]
正如你所看到的,可以通过一个接一个地追加声明符来完全描述一个types。 build设可以通过两种方式完成。 一个是自下而上,从正确的事情(叶)开始,一直到标识符为止。 另一种方式是自上而下,从标识符开始,一路向下。 我将展示两种方式。
自下而上
构造从右边的东西开始:返回的东西,这是使用char的函数。 为了保持不同的声明,我要给他们编号:
D1(char);
直接插入char参数,因为它是微不足道的。 通过用*D2
replaceD1
来添加一个指向声明符的指针。 请注意,我们必须围绕*D2
包围括号。 通过查找*-operator
和函数调用操作符()
的优先级可以知道这一点。 没有我们的括号,编译器会把它读为*(D2(char p))
。 但是,这当然不会是D1的简单replace。 圆括号总是允许在宣言者身边。 所以,如果你真的添加了太多的东西,那么你不会犯任何错误。
(*D2)(char);
返回types完成! 现在,让我们用函数声明符函数replaceD2
,然后使用<parameters>
返回值 ,这就是我们现在使用的D3(<parameters>)
。
(*D3(<parameters>))(char)
请注意,不需要括号,因为我们希望 D3
这次是一个函数声明符而不是一个指针声明符。 很好,唯一剩下的就是它的参数。 这个参数和我们完成返回types完全一样,只是用char
replace成void
。 所以我会复制它:
(*D3( (*ID1)(void)))(char)
我已经用ID1
replace了D2
,因为我们完成了这个参数(它已经是一个指向函数的指针 – 不需要另一个声明符)。 ID1
将是参数的名称。 现在,我在上面告诉了所有那些声明者修改的types – 每个声明最左边的那个types。 对于函数来说,这将成为返回types。 对于指向types的指针等等…当写下types时它会很有趣,它会以相反的顺序出现在正确的位置:)无论如何,replace它会产生完整的声明。 当然两次都是。
int (*ID0(int (*ID1)(void)))(char)
在这个例子中,我已经调用了函数ID0
的标识符。
自上而下
这从types描述的最左边的标识符开始,当我们走过右边的时候,包装这个声明符。 从以<
parameters >
返回的函数开始
ID0(<parameters>)
描述中的下一个东西(在“返回”之后)是指针 。 让我们把它纳入:
*ID0(<parameters>)
然后接下来的事情是用<
参数>
返回的函数 。 这个参数是一个简单的字符,所以我们再把它放在一边,因为它非常简单。
(*ID0(<parameters>))(char)
注意我们添加的圆括号,因为我们再次希望*
先绑定, 然后再 (char)
。 否则,它会读取<
parameters >
返回函数…的函数 。 Noes,甚至不允许函数返回函数。
现在我们只需要把<
参数>
。 我将展示一个简短的派生版本,因为我认为你现在已经知道如何去做。
pointer to: *ID1 ... function taking void returning: (*ID1)(void)
就像我们自下而上的那样,把int
放在声明之前,我们就完成了
int (*ID0(int (*ID1)(void)))(char)
好的东西
是自下而上还是自上而下? 我习惯于自下而上,但有些人可能会更自在地从上到下。 这是一个我认为的品味问题。 顺便说一句,如果你申请所有的运算符在这个声明中,你最终会得到一个int:
int v = (*ID0(some_function_pointer))(some_char);
这是C中声明的一个很好的属性:声明声明如果这些运算符在使用标识符的expression式中使用,那么它将产生最左边的types。 对于数组也是如此。
希望你喜欢这个小教程! 现在我们可以链接到这个当人们想知道函数的奇怪的声明语法。 我试图尽可能less的C内部。 随意编辑/修复它的东西。
函数指针的另一个很好的用法:
在版本之间切换无痛
当你需要不同的function在不同的时间或不同的发展阶段时,它们非常方便使用。 例如,我正在主机上开发一个具有控制台的应用程序,但软件的最终版本将放在Avnet ZedBoard上(它有显示器和控制台的端口,但不需要/不需要最终版本)。 所以在开发过程中,我将使用printf
来查看状态和错误消息,但是当我完成时,我不想要任何打印。 以下是我所做的:
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
在version.c
我将定义version.c
的2个函数原型
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; }
注意函数指针是如何在version.h
中原型的
void (* zprintf)(const char *, ...);
当它在应用程序中被引用时,它将开始执行它指向的任何地方,这还没有被定义。
在version.c
,请注意在board_init()
函数中,根据version.h
定义的版本, zprintf
被分配了一个唯一的函数(其函数签名匹配)
zprintf = &printf;
zprintf调用printf进行debugging
要么
zprintf = &noprint;
zprintf只是返回,不会运行不必要的代码
运行代码如下所示:
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; }
如果处于debugging模式,上面的代码将使用printf
,或者在释放模式下不执行任何操作。 这比通过整个项目并注释掉或删除代码要容易得多。 所有我需要做的是在version.h
更改版本,代码将完成剩下的工作!
函数指针通常由typedef定义,用作参数&返回值,
以上的答案已经解释了很多,我只是举一个完整的例子:
#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 ∑ } // test - use function pointer as variable, void test_pointer_as_variable() { // create a pointer to function, two_num_operation sum_p = ∑ // 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; }
C中函数指针的一个重要用途是调用在运行时select的函数。 例如,C运行库有两个例程qsort和bsearch,它们指向一个被调用的函数来比较两个正在sorting的项目; 这允许您根据您希望使用的任何标准分别对任何内容进行sorting或search。
一个非常基本的例子,如果有一个叫做print(int x,int y)的函数又可能需要调用类似types的add()函数或者sub(),那么我们将会添加一个函数指向print()函数的指针参数如下所示:
int add() { return (100+10); } int sub() { return (100-10); } void print(int x, int y, int (*func)()) { printf("value is : %d", (x+y+(*func)())); } int main() { int x=100, y=200; print(x,y,add); print(x,y,sub); return 0; }
从头开始函数有一些内存地址从他们开始执行的地方。 在汇编语言中它们被称为(称为“函数的内存地址”)。现在回到C如果函数有一个内存地址,那么它们可以被C中的指针所操纵。通过C
1.首先你需要声明一个指向函数的指针2.通过Desired函数的地址
****注意 – >function应该是相同的types****
这个简单的程序会说明一切。
#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"); }
在32位体系结构中让我们看看机器如何理解上述程序的机器指令。
红色标记区域显示地址如何交换和存储在eax中。然后它们是eax上的一个调用指令。 eax包含函数的所需地址
由于函数指针通常是键入的callback函数,因此您可能需要查看types安全的callback函数。 入口点等同于不是callback的函数。
C是相当易变和宽容的同时:)
函数指针在很多情况下都很有用,例如:
- COM对象成员是指向函数ag的指针:
This->lpVtbl->AddRef(This);
AddRef是一个指向函数的指针。 - 函数callback,例如用户定义的函数来比较两个variables作为callback传递给一个特殊的sorting函数。
- 非常有用的插件实施和应用程序SDK。