我最近在C语言中有一些函数指针的经验。
因此,按照回答自己问题的传统,我决定对最基本的问题做一个简短的总结,以供那些需要快速切入主题的人使用。
我最近在C语言中有一些函数指针的经验。
因此,按照回答自己问题的传统,我决定对最基本的问题做一个简短的总结,以供那些需要快速切入主题的人使用。
当前回答
函数指针通常由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;
}
其他回答
被解雇的指南:如何在x86机器上通过手工编译代码滥用GCC中的函数指针:
这些字符串文字是32位x86机器代码的字节。0xC3是x86 ret指令。
你通常不会用手工编写这些,你会用汇编语言编写,然后使用像nasm这样的汇编程序将其汇编成一个平面二进制,然后将其十六进制转储成一个C字符串文字。
返回EAX寄存器上的当前值int eax=((int(*)())(“\xc3<-返回eax寄存器的值”)();编写交换函数整数a=10,b=20;((void(*)(int*,int*))“\x8b\x44\x04\x8b\x5c\x24\x08\x80\x80\x8b\ux1b\x31\x31\xc3\xcb\x4c\x24\x04\x09\x01\xcb\x4c\x04\x08\x49\xc3<-这将交换a和b的值”)(-a,-b);将for循环计数器写入1000,每次调用一些函数((int(*)())“\x66\x31\xc0\x8b\x5c\x24\x64\x66\x40\x50\xxf\xd3\x58\x66\x3d\xC8\x03\x75\xf4\xc3”)(函数);//调用函数1->1000您甚至可以编写一个计数为100的递归函数const char*lol=“\x8b\x5c\x24\x4\x3\x8\x3\x0\x0\x7e\x2\x31\x80\x83\x48\x64\x7d\x6\x40\x53\xdf\xd3\x5b\xc3\xc3<-递归调用地址lol处的函数。”;i=((int(*)())(lol))(lol);
请注意,编译器将字符串文本放在.rodata部分(或Windows上的.rata),该部分作为文本段的一部分链接(以及函数代码)。
文本段具有Read+Exec权限,因此将字符串文本转换为函数指针是有效的,而无需像动态分配内存那样进行mprotect()或VirtualProtect()系统调用。(或者gcc-z execstack将程序与堆栈+数据段+堆可执行文件链接起来,作为快速破解。)
要反汇编这些,您可以编译它以在字节上添加标签,并使用反汇编程序。
// 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";
用gcc-c-m32-foo.c编译,用objdump-D-rwC-Mintel反汇编,我们可以得到程序集,并发现该代码通过破坏EBX(一个调用保留寄存器)而违反了ABI,通常效率很低。
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
在Windows、Linux、OS X等系统上,这段机器代码(可能)可以在32位代码中工作:所有这些OS上的默认调用约定都在堆栈中传递参数,而不是在寄存器中更有效地传递参数。但EBX在所有正常的调用约定中都保留了调用,因此将其用作临时寄存器而不保存/恢复它很容易使调用者崩溃。
由于函数指针通常是类型化回调,因此您可能需要查看类型安全回调。这同样适用于非回调函数的入口点等。
C相当善变,同时也很宽容:)
一旦有了基本的声明器,函数指针就很容易声明:
id:id:id是指针:*D:D指针指向函数:D(<parameters>):D函数获取<parameter>返回
而D是使用相同规则构建的另一个声明器。最后,在某个地方,它以ID结尾(参见下面的示例),这是声明实体的名称。让我们尝试构建一个函数,该函数获取指向不获取任何内容并返回int的函数的指针,并返回指向获取char并返回int函数的指针
typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);
如您所见,使用typedef构建它非常容易。如果没有typedef,上面的声明器规则也不难,而且应用一致。如您所见,我漏掉了指针指向的部分,以及函数返回的内容。这是出现在声明最左边的内容,并不重要:如果已经建立了声明人,则在末尾添加。让我们这样做吧。始终如一地构建它,首先使用[和]显示结构:
function taking
[pointer to [function taking [void] returning [int]]]
returning
[pointer to [function taking [char] returning [int]]]
如您所见,可以通过一个接一个地附加声明符来完全描述类型。施工有两种方式。一种是自下而上的,从非常正确的事情(树叶)开始,一直到标识符。另一种方式是自上而下的,从标识符开始,一直到叶子。我会双向展示。
自下而上
构造从右边的东西开始:返回的东西,它是取char的函数。为了保持声明人的不同,我将对他们进行编号:
D1(char);
直接插入了char参数,因为它很简单。通过用*D2替换D1来添加指向声明器的指针。注意,我们必须用括号括住*D2。通过查找*-运算符和函数调用运算符()的优先级可以知道这一点。如果没有括号,编译器会将其读为*(D2(char p))。当然,这不会再简单地用*D2取代D1。声明符周围始终允许使用括号。实际上,如果你添加了太多,你不会出错。
(*D2)(char);
返回类型已完成!现在,让我们用返回<parameters>的函数声明器函数来替换D2,这就是我们现在的D3(<parameter>)。
(*D3(<parameters>))(char)
注意,不需要括号,因为我们希望D3这次是函数声明器,而不是指针声明器。很好,唯一剩下的就是它的参数。该参数的处理方式与我们处理返回类型的处理方式完全相同,只是将char替换为void。所以我会复制它:
(*D3( (*ID1)(void)))(char)
我已经用ID1替换了D2,因为我们已经完成了该参数(它已经是一个指向函数的指针,不需要另一个声明器)。ID1将是参数的名称。现在,我在上面的结尾告诉我们,一个添加了所有声明者修改的类型,即出现在每个声明的最左边的类型。对于函数,这将成为返回类型。对于指向类型等的指针……写下类型时很有趣,它将以相反的顺序出现在最右边:)无论如何,替换它会产生完整的声明。当然,这两次都是int。
int (*ID0(int (*ID1)(void)))(char)
在该示例中,我调用了函数ID0的标识符。
自上而下
这从类型描述中最左边的标识符开始,当我们从右边走过时,将该声明符包装起来。从函数获取<parameters>开始返回
ID0(<parameters>)
描述中的下一件事(在“返回”之后)是指针指向。让我们合并它:
*ID0(<parameters>)
然后,下一件事是functon获取<parameters>返回。该参数是一个简单的字符,因此我们再次将其放入,因为它非常简单。
(*ID0(<parameters>))(char)
注意我们添加的括号,因为我们再次希望先绑定*,然后绑定(char)。否则它将读取获取<parameters>返回函数的函数。。。。不,甚至不允许返回函数的函数。
现在我们只需要放置<parameters>。我将展示一个简短的嘲笑版本,因为我认为你现在已经知道该怎么做了。
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中声明的一个很好的属性:声明断言,如果在使用标识符的表达式中使用这些运算符,那么它将在最左边生成类型。阵列也是如此。
希望你喜欢这个小教程!现在,当人们对函数奇怪的声明语法感到疑惑时,我们可以联系到这一点。我尽量少放C内部构件。随意编辑/修复其中的内容。
指向函数的指针很有用,因为正如《C编程语言》一书所说,C中的函数不是变量。这意味着,
// Say you have add function
int add(int x, int y){
return x + y;
}
// Say you have another add function
int another_add(int x, int y){
return y + x;
}
int main(){
// Although the types of another_add and add are same
// You can't do
another_add = add
// You have a compute function that takes a function of int's signature
int (*compute)(int, int);
// You won't even be able to pass functions to other functions
// (Although when you do, C is just passing the pointer to that function)
// So, compute(add) is really compute(&add)
// But you can create a pointer to functions that are variables
// you can assign to and/or pass to other functions
int (*operation)(int, int);
// Now you can do
operation = &add;
// You could also do, the following to do the same thing
// When a function is passed in right hand side of assignment,
// C knows that you mean pointer, and you don't need explicit &
operation = add;
}
同样,数组也不是C中的变量。您可以编写一个与上面类似的示例并进行测试。
函数指针通常由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;
}