C 语言函数指针的一些理解

函数指针属于 C 语言的进阶内容之一,前两天研究 tinyhttpd 时,碰到了函数指针,产生了一些疑问...

普通指针

在研究函数指针之前,先回顾一下普通指针,先看下面的代码:

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
30
31
32
33
#include <stdio.h>
struct point {
int x, y;
};

int main(int argc, char const *argv[]) {
int a = 10, *pa = &a;
printf("*pa = %d, a = %d\n", *pa, a);

float f = 1.0, *pf = &f;
printf("*pf = %f, f = %f\n", *pf, f);

struct point A, *pA = &A;
A.x = 3, A.y = 3;
printf("A.x = %d, A.y = %d\n", A.x, A.y);
printf("pA->x = %d, pA->y = %d\n", pA->x, pA->y);
printf("*pA.x = %d, *pA.y = %d\n", (*pA).x, (*pA).y);

return 0;
}

/*
output:
*pa = 10, a = 10
*pf = 1.000000, f = 1.000000
A.x = 3, A.y = 3
pA->x = 3, pA->y = 3
*pA.x = 3, *pA.y = 3
sizeof(pa) = 4
sizeof(pf) = 4
sizeof(pA) = 4

*/

从输出结果可以看出,不管是普通变量的指针,还是结构体的指针,都是普通的指针变量,而指针的本质就是保存地址的变量,通过这个地址可以找到对应的变量。利用&符号,可以得到变量的地址从而赋值给指针变量,这样这个指针变量就指向了使用&运算符的变量。

函数指针

现在,回到对函数指针的思考上,首先应该要认识的是:函数指针的本质还是指针,它与普通指针没有什么差别,还是指针变量,只不过这个指针变量指向的是函数,就跟不同类型的指针变量指向对应类型的变量一样。
通过上面的代码,我们知道&符号可以取出一个变量的地址,那函数的地址是不是也可以这样取出呢?写个程序测试一下:

test1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void fun() { }

int main(int argc, char const *argv[]) {
printf("&fun = %p\n", &fun);
printf("sizeof(&fun) = %d\n", sizeof(&fun));
return 0;
}
/*
output:
&fun = 00401460
sizeof(&fun) = 4
*/

可以发现使用&符号的确可以输出函数的地址,同时使用sizeof关键字得到了“取出的地址”的大小。按照前面的思路,我们知道这是一个指针,但这是个什么类型的指针,不知道,为什么是 4,也不知道,但我们可以再多写点代码验证一下思路:

test2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void fun() { }

int main(int argc, char const *argv[]) {
printf("&fun = %p\n", &fun);
printf("sizeof(&fun) = %d\n", sizeof(&fun));
int i = 10, *pi = &i;
printf("sizeof(&i) = %d\n", sizeof(&i));
printf("*pi = %d, sizeof(pi) = %d\n", *pi, sizeof(pi));
return 0;
}
/*
&fun = 00401460
sizeof(&fun) = 4
sizeof(&i) = 4
*pi = 10, sizeof(pi) = 4
*/

现在,可以很清楚的确定,利用&取出的函数的地址与指针变量是一样,它们都是 4 个字节的大小,那么也就是说函数指针也是一种指针变量,这与前面的思路是一致的。

了解函数指针大概是什么样子的后,肯定会冒出这样的问题:怎么使用函数指针变量指向某个函数呢?换句话说,就是如何像使用int i, *a = &i;这样,声明某个指针变量并指向某个函数呢?

显然,这个问题 C 语言肯定是有规定的,函数指针的声明方式与函数声明是类似的,只是需要加上*()。同样,我们用代码说话:

test3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int fun(int a) {
printf("In fun, a = %d\n", a);
return a;
}

int main(int argc, char const *argv[]) {
int (*pf)(int) = &fun, a = 10;
pf(a);
return 0;
}

/*
output:
In fun, a = 10
*/

在上面的代码中,声明了一个函数指针变量pf指向函数fun,然后通过pf指针,调用了函数fun,输出a的值。相比函数的声明而言,有两个特点:

  1. 省略了参数名,但不能省略参数类型,且必须与被指向的函数参数类型、数量一致
  2. 需要用*()讲指针名括起来

按照上面的思路,可以知道函数指针的声明方式是[return-type] (*pointer-name)(parameter-list)

现在,我们在上面的代码中尝试将函数指针作为函数的参数使用:

test4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int fun(int a) {
printf("In fun, a = %d\n", a);
return a;
}

void func(int (*pf)(int)) {
int a = 10;
pf(a);
}

int main(int argc, char const *argv[]) {
func(&fun);
return 0;
}
/*
output:
In fun, a = 10
*/

test3 和 test4 两段代码的输出结果是完全一致的,不同的是在 test4 中,直接使用函数指针作为另一函数的参数,然后在另一函数中通过函数指针调用这个函数。可以发现,函数指针作为函数参数的写法与其声明没有太大差异。

数组指针

现在我们再回顾一下数组指针的用法,先看下面的代码:

test5
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
#include <stdio.h>

void fun(int *pa) {
printf("*pa = %d\n", *pa);
printf("sizeof(pa) = %d\n", sizeof(pa));
}

int main(int argc, char const *argv[]) {
int a[5] = {1, 2, 3, 4, 5};
fun(a);
printf("sizeof(a) = %d\n", sizeof(a));
int (*parr)[5] = &a;
printf("a = %p, &a = %p\n", a, &a);
printf("parr = %p\n", parr);
printf("*parr = %d\n", *parr);
printf("sizeof(&a) = %d\n", sizeof(&a));
printf("sizeof(parr) = %d\n", sizeof(parr));
return 0;
}
/*
*pa = 1
sizeof(pa) = 4
sizeof(a) = 20
a = 0061FF08, &a = 0061FF08
parr = 0061FF08
*parr = 6422280
sizeof(&a) = 4
sizeof(parr) = 4

我们知道数组名就表示数组的首地址,所以可以将数组名直接作为函数的参数,从而传入数组的地址,但数组名和指向数组的指针是两个完全不同的东西,这一点可以通过sizeof关键字看出,也就是说parr&a是同一类事物——二者都是数组指针,对应的它的类型声明才是数组指针的声明,而sizeof则认为a是一个数组,所以sizeof(a) = 20。从这里也可以看出,数组指针的声明与函数指针类似,有两个条件需要指定:

  1. 数组的类型
  2. 数组的维度

不过,实际上,C 语言存在隐式类型转换(还有兼容老版本写法之类的历史遗留原因等),所以有时候写法不严格,编译也可以通过,但会产生 warnings,总之尽量保持类型一致,不要出现 warnings。
但这里有意思的是,数组名作为地址可以直接作为指针传给函数使用,那函数名呢?当然也可以了~
那么,test4 的代码可以改成:

test4_1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int fun(int a) {
printf("In fun, a = %d\n", a);
return a;
}

// void func(int (*pf)(int)) {
void func(int pf(int)) {
int a = 10;
pf(a);
}

int main(int argc, char const *argv[]) {
// func(&fun);
func(fun);
return 0;
}
/*
output:
In fun, a = 10
*/

在上面的代码中,有两个改动的地方:

  1. 函数指针做参数的函数声明中,去掉了*
  2. 直接将函数名作为参数传入另一个函数中,去掉了&

虽然这两处改动都不影响使用,但是读起来容易引起混淆,语义不明确,这应该是某种“历史遗留写法”,最好不要这样写。
实际上,我们可以通过sizeof来观察一下函数名与函数指针的差异,再添几行代码:

test6
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

void func() { }

int main(int argc, char const *argv[]) {
printf("sizeof(func) = %d, sizeof(&func) = %d\n", sizeof(func), sizeof(&func));

return 0;
}
/*
output:
sizeof(func) = 1, sizeof(&func) = 4
*/

显然,与数组类似,函数名与函数指针也是两个不同的东西。

指针数组

接下来,在看看指针数组(反着叫又是另外一种东西了😂)的用法,直接看下面的代码:

test7
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char const *argv[]) {
int a = 10, b = 20;
int *parr[2] = {&a, &b};
printf("%d, %d\n", *parr[0], *parr[1]);
return 0;
}

其实就是在指针后面加上了[]符号,对于其他类型也是一样的用法。

函数指针数组

说完了函数指针也数组指针的区别,见到了指针数组的用法,接下来,再来看看如何使用函数指针数组。
假设我们要计算两个整数的四则运算结果,为了使用到函数指针数组,先定义四个函数,然后放到函数指针数组中,再利用函数指针数组逐个调用函数完成计算。
按照这样的思路,可以写出以下代码:

test8
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
30
31
32
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int minus(int a, int b) {
return a - b;
}

int mult(int a, int b) {
return a * b;
}

int div(int a, int b) {
return a / b;
}

int main(int argc, char const *argv[]) {
int a = 10, b = 10;
int (*pfun[4])(int, int) = {add, minus, mult, div};
for(int i = 0; i < 4; i++)
printf("pfun[%d](a, b) = %d\n", i, pfun[i](a, b));
return 0;
}
/*
output:
pfun[0](a, b) = 20
pfun[1](a, b) = 0
pfun[2](a, b) = 100
pfun[3](a, b) = 1
*/

实际上,函数指针数组也就是在函数指针中间加上[],这与指针数组是类似的。但是要注意的是,函数指针数组只能保存“同一类”函数指针,而“同一类”在这里是指返回类型和参数列表需要一致。

总结

虽然这篇文章题目叫做函数指针,但是目前我们一共学习了五种不同的与指针相关的概念和用法,分别是:

  1. 普通指针
  2. 数组指针
  3. 函数指针
  4. 指针数组
  5. 函数指针数组

同时,还认识到了数组名与数组指针、函数名与函数指针的区别。这些都只是指针用法的冰山一角,指针还有很多其他的用法,比如通过指针在函数中修改实参的值,或者是在 C++ 中用父类指针指向子类对象,调用子类方法,从而实现动态多态等等。
所以,学习之路还很漫长啊~😪


Buy me a coffee ? :)
0%