前置知识:
函数
指针
事先声明:
本章中出现的各种程序的编译以及反汇编操作全部使用MinGW-w64工具链的gcc12以及IDA7.5,展示结果仅供参考.
函数指针
C语言中,函数也是一个可寻址对象,我们也可以获取其地址——换句话说,函数代码被存储于内存中的某一个地方,并且可以根据其指针来访问.
调用该函数时,程序就会跳转到该函数的地址,运行此处的代码,也即调用了该函数.
显然,我们可以使用一种特殊的指针来存储一个函数的地址(指针),并且完全可以使用该指针来进行访问.
这就是函数指针
,或者说"指向函数的指针".
声明一个函数指针
我们可以如此声明一个函数指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> int func (int i) { return i*2 ; } int main () { int (*p)(int ) = func; printf ("p: 0x%p\n" , p); printf ("&func: 0x%p\n" , &func); printf ("(*p)(3): %d\n" ,(*p)(3 )); printf ("func(4): %d\n" ,func(4 )); return 0 ; }
考虑声明 int (*p)(int)
:
首先从标识符p
开始,有一对()约束*p
,指出p是一个指针;
然后在(*p)
外的int (int)
代表一个函数类型,此函数接受一个int参数,并返回一个int值;
因此推导出,p
是一个指针,可以指向int (int)
类型的函数,亦即p是一个函数指针.
上面代码的运行结果如下:
可以看出,指针p的值(p指向的内存)就是func的地址.
函数指针的类型
前一个例子中,函数指针p指向的函数的类型是int (int)
,它代表了"一类"函数,而不是具体的一个函数,考虑下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> int func1 (int i) { return i * 2 ; } int func2 (int i) { return i * 3 ; } int main () { int (*p)(int ) = NULL ; p = func1; printf ("/*当前p指向func1函数*/\n" ); printf ("p: %p\n" , p); printf ("&func1: %p\n" , &func1); printf ("(*p)(2): %d\n" , (*p)(2 )); printf ("func1(3): %d\n" , func1(3 )); p=func2; printf ("\n/*当前p指向func2函数*/\n" ); printf ("p: %p\n" , p); printf ("&func2: %p\n" , &func2); printf ("(*p)(2): %d\n" , (*p)(2 )); printf ("func2(3): %d\n" , func2(3 )); return 0 ; }
运行结果如下:
显然,func1
和func2
的类型完全相同,均为int (int)
,但是他们却是完全不同的2个函数,p
指针可以分别指向他们,并进行调用.
另一方面,复习指针的内容:指针变量只能被赋值(指向)为指针变量所能指向的类型的地址,或者是能够被隐式转换为这种类型的地址.
和其他所有指针一样,函数指针只能指向自身指向的类型(即特定的一种函数类型)的地址.
但是需要注意,不同类型的函数指针是不能够互相转换的,也就是说,int (*p)(int)
只能指向int (int)
类型的函数,而不能指向例如int (double)
类型的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> int func1 (int i) { return i * 2 ; } int func2 (double i) { return (int )i; } int main () { int (*p)(int ) = func1; printf ("p: %p\n" , p); return 0 ; }
C语言中,不仅仅是参数类型,包括返回值类型,参数个数不同,都意味着他们不是相同类型的函数!
注: C语言没有函数重载,而且即使是C++的函数重载也不允许有这种指针类型转换
函数名的本质
C语言在处理函数指针和函数调用这方面有一些很有意思的特性,先看下面的代码,有可能让你懵逼:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> int func (int i) { return i*2 ; } int main () { int (*p)(int ) = func; printf (" p: %p\n" , p); printf (" *p: %p\n" , *p); printf (" func: %p\n" , func); printf ("*func: %p\n" , *func); printf ("&func: %p\n" , &func); return 0 ; }
令人迷惑的是,每一行的输出值都是一样的:
我们可以将其反汇编查看汇编代码,结果发现每一步的参数都是完全一样的:
也就是说,无论是对func做*
运算,还是&
运算,还是直接对函数名func求值,得到的结果甚至汇编代码都是完全一致的,函数指针p也是如此.
事实上,函数名被使用时总是被编译器转换为函数指针
,因此,诸如&func
这样手动加上&运算符
只不过是显式地说明了编译器本将隐式执行的任务.
因此,我们通常会这样简化对函数指针的赋值:
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int func (int i) { return i * 2 ; } int main () { int (*p)(int ) = func; printf ("p: %p\n" , p); return 0 ; }
而同时,使用函数指针进行函数调用的代码也可以如此简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int func (int i) { return i * 2 ; } int main () { int (*p)(int ) = func; printf ("p(2): %d\n" , p(2 )); printf ("(*p)(2): %d\n" , (*p)(2 )); return 0 ; }
另外,尽管从上面的角度分析来看,这种编译器负责的"转换"是必然发生的,但是事实并非如此:
就拿上面这段代码(“简化使用指针进行函数调用”)来看,编译器甚至可能直接把这个指针变量p优化掉:
指针变量p呢?我不知道……你知道吗(手动滑稽)
所以从实际优化的角度来看,简单的函数指针使用甚至会直接被优化掉,更别提什么"函数名的转换"了,统统直接用lea func
指令获取地址就完事了……
总结:实际使用过程中一旦用一个函数指针p来指向某个函数f,那么干脆直接把p当成函数f的一个别名
也无伤大雅.唯一的区别是,p是一个指针,可以改变它的指向.
函数指针数组
这个很简单,前面讲过指针数组,函数指针数组就是一种指针数组,只不过这个数组中的每个元素都是一个函数指针而已.
简单举个例子,注意声明的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> int add (int a, int b) { printf ("add: %d + %d" , a, b); return a + b; } int sub (int a, int b) { printf ("sub: %d - %d" , a, b); return a - b; } int mul (int a, int b) { printf ("mul: %d * %d" , a, b); return a * b; } int div (int a, int b) { printf ("div: %d / %d" , a, b); return a / b; } int main () { int (*p[4 ])(int , int ) = { add, sub, mul, div }; int x, y; scanf ("%d %d" , &x, &y); for (int i = 0 ; i < 4 ; i++) { printf (" = %d\n" , p[i](x, y)); } return 0 ; }
以8 2
作为x,y的值运行,结果如下:
函数指针数组可以存储一系列类似功能(接口)的函数的指针,适当地使用可以大大简化程序,例如实现一个功能菜单的选择.
——WAHAHA 2024.1.23
上一篇:C语言教程-13_3-初探指针和数组的关系
下一篇:C语言教程-14_1-初识结构体