前置知识:

  1. 函数
  2. 指针

事先声明:

本章中出现的各种程序的编译以及反汇编操作全部使用MinGW-w64工具链的gcc12以及IDA7.5,展示结果仅供参考.

函数指针

C语言中,函数也是一个可寻址对象,我们也可以获取其地址——换句话说,函数代码被存储于内存中的某一个地方,并且可以根据其指针来访问.

调用该函数时,程序就会跳转到该函数的地址,运行此处的代码,也即调用了该函数.

显然,我们可以使用一种特殊的指针来存储一个函数的地址(指针),并且完全可以使用该指针来进行访问.

这就是函数指针,或者说"指向函数的指针".

声明一个函数指针

我们可以如此声明一个函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
/* func函数用于返回int参数2倍 */
int func(int i){
return i*2;
}
int main() {
int (*p)(int) = func; // p是一个函数指针,初始化为指向func函数
printf("p: 0x%p\n", p); // 输出p的值
printf("&func: 0x%p\n", &func); // 输出func函数的地址
printf("(*p)(3): %d\n",(*p)(3)); // 使用p指针调用func函数
printf("func(4): %d\n",func(4)); // 调用func函数
return 0;
}

考虑声明 int (*p)(int):

首先从标识符p开始,有一对()约束*p,指出p是一个指针;

然后在(*p)外的int (int)代表一个函数类型,此函数接受一个int参数,并返回一个int值;

因此推导出,p是一个指针,可以指向int (int)类型的函数,亦即p是一个函数指针.

上面代码的运行结果如下:

image-20240123145057712

可以看出,指针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>
/* func1函数用于返回int参数2倍 */
int func1(int i) {
return i * 2;
}
/* func2函数用于返回int参数3倍 */
int func2(int i) {
return i * 3;
}
int main() {
int (*p)(int) = NULL; // p是一个函数指针,初始化为NULL
p = func1; // 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; // 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;
}

运行结果如下:

image-20240123145635901

显然,func1func2的类型完全相同,均为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>
/* 2个不同类型的函数 */
int func1(int i) {
return i * 2;
}
int func2(double i) {
return (int)i;
}
int main() {
int (*p)(int) = func1;
// 下面这行代码会报错:
// invalid conversion from 'int (*)(double)' to 'int (*)(int)'
// p = func2;
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;
}

令人迷惑的是,每一行的输出值都是一样的:

image-20240123153519714

我们可以将其反汇编查看汇编代码,结果发现每一步的参数都是完全一样的:

image-20240123154307027

也就是说,无论是对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)); // 不需要(*p)(2),因为*p是函数,编译器仍然会将其重新转换为函数指针
// 下面的代码没有问题,但是一步解引用看起来多此一举
printf("(*p)(2): %d\n", (*p)(2));
return 0;
}

另外,尽管从上面的角度分析来看,这种编译器负责的"转换"是必然发生的,但是事实并非如此:

就拿上面这段代码(“简化使用指针进行函数调用”)来看,编译器甚至可能直接把这个指针变量p优化掉:

image-20240123155637813

指针变量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() {
/* p是一个数组,每个元素是一个指针,指针指向的类型是int (int,int)函数 */
int (*p[4])(int, int) = { add, sub, mul, div }; // 使用4个函数指针来初始化数组
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的值运行,结果如下:

image-20240123160756038

函数指针数组可以存储一系列类似功能(接口)的函数的指针,适当地使用可以大大简化程序,例如实现一个功能菜单的选择.


——WAHAHA 2024.1.23



上一篇:C语言教程-13_3-初探指针和数组的关系

下一篇:C语言教程-14_1-初识结构体