函数指针认读 ¶
首先,让我介绍一下两个惹人喜爱的小家伙。
int function(int arg)
{
return 2 * arg;
}
一般通过函数,类型是 int (int)
。
int (*func_ptr)(int);
芝士什么?芝士函数指针,类型是 int (*)(int)
。
那么,这两个小家伙能干什么呢?
func_ptr = &function;
func_ptr = function;
前者是什么?指针。没错,平平无奇的取地址操作,洒洒水啦。
后者是什么?我们知道指针的值实际上是某块内存地址,不考虑奇技淫巧的话,将所指对象的值赋值给指针是没有意义的。但是,将函数指代器 function
赋值给函数指针 func_ptr
,却没有产生任何问题。
坏了,自动播放妈妈生的,大脑要打高端局了。不过你先别急,这个看起来不知所云的赋值语句,其实与前者完全等效。C 标准是仁慈的,它不忍心看到被函数绕晕的初学者面对错误的解引用不知所措,于是规定了函数到指针的隐式转换。
任何函数指代器表达式,在用于异于下列语境时
- 作为取址运算符的操作数
- 作为
sizeof
的操作数会经历到指向表达式所指代函数的指针的转换。
这里 function
经过一次隐式转换变成 &function
。这一步转换并不改变值,而类型从 int (int)
变成 int (*)(int)
了。也就是实际上在编译器看来,第二行就是第一行。
现在你已经对函数和函数指针有了一定了解,让我们看一看下面这个简单的例子,把我们已经学到的知识运用到实践中吧。
请解释以下代码的作用:
// 试试看!:)
(*(void(*)())0)();
好吧,这样的表达式或许对你来说为时尚早。我们换些简单点的例子。
function(20);
(function)(20);
(*&function)(20);
(&function)(20);
(*function)(20);
(***********function)(20); // 不用数了,11 个
先不管第六个是什么东西,我们看看前五个。
第一个不能再熟悉了,一个人畜无害的函数调用。
第二个看着和第一个差不多……?没错,相信自己,它们完全一样。
第三个呢?没错,取地址再解引用,除了出题没有用。连编译器都会毫不犹豫地优化掉。
第四个开始奇怪了起来。缺失的解引用并非粗心大意,它象征着我们玩转指针的自信与惬意,以及捍卫指针地位的决心。鉴于 C 标准允许通过函数指针调用函数,编译器对此也没什么意见。
第五个换了种折磨方式。聪明如你早已料到最后的结果。但是为什么?引言定真,鉴定为隐式转换惯的。回忆一下函数到指针的隐式转换,*function
等效于 *&function
,这下看懂了。
现在第六个你应该也能理解了。每次尝试对 function
解引用,都会迫使编译器将它转换为函数指针,以满足你对代码风格的奇怪癖好。
还是看看远处的 func_ptr
吧家人们。
(*func_ptr)(20);
func_ptr(20);
(****func_ptr)(20);
(&func_ptr)(20);
第一个,简单易懂的指针解引用,相当于上面的第二个。
第二个,人畜无害的函数指针调用函数,相当于上面的第四个。
第三个,函数到指针的隐式转换发生了整整三回啊三回,类似上面的第六个。
第四个,编译器跟你爆了。怎么会是呢?我们不妨拆开看看:
int (*func_ptr)(int) = &function; // 这是之前的
int (**second_ptr)(int) = &func_ptr; // 这是我们正在做的
second_ptr(20); // 我们想这样
在这里,second_ptr
已经是个二级指针了。函数到函数指针(它是个一级指针)的转换过程中,地址值是不会改变的,就是这个函数的可执行代码所在的位置。问题在于,二级指针的值不是 function
的地址,而是 func_ptr
这个变量的地址。这还得了,骗自己可以,骗兄弟也就算了,编译器可不吃你这套。
接下来,我们再看点不太一样的:
int func_two(int a, int f(int arg))
{
return 3 * f(a);
}
这是什么?函数,调用一下:
// function 如上文定义
int function(int arg);
int a = 1;
int b = func_two(a, function);
相信聪明的你已经猜到了,b
的值就是 6。桥豆麻袋,好像有哪里不太对劲。func_two
的两个参数类型分别是 int
和 int (int)
,但是调用时却传入了一个函数指针(还记得隐式转换吗
又是隐式转换?Bingo!不过需要注意发生隐式转换的位置。func_two
的第二个形参实际上是 int (*)(int)
,而非字面上的 int (int)
。这就和 void f(int a[])
实际上是 void f(int *a)
一样,同样是出于节约资源的考虑。也就是说,编译器眼中的 func_two
是 int func_two(int a, int (*f)(int)
。搞清楚这一点,其余部分也就顺理成章了。
经过练习,你应该已经可以看出,从函数到指针的隐式转换规定出发,理解上述情景并非难事。对函数指针的畏惧,往往是因为不熟悉转换规则,或受复杂的声明语法干扰。因此,提高识别类型的熟练度,足以让你自信运用函数指针。