如何像计算机科学家一样思考,分清函数指针和指针函数

Mr.C/文

C语言指针导学(4)——分清函数指针和指针函数

物格而后知至,知至而后意诚,意诚而后心正,心正而后身修,身修而后家齐,家齐而后国治,国治而后天下平。自天子以至於庶人,一是皆以修身为本。
—— 《大学》

四.分清函数指针和指针函数

图片 1

关于指针和数组斩不断理还乱的恩怨还真是说了不少,不过现在应该已经理清了。有了上一讲的基础,本讲的内容相对来说就比较容易理解了。

早上在Instagram看到这张图片,觉得蛮有意思便转发到朋友圈,然后…有说“写这种代码会被打”,有一脸蒙圈的,以及还有相当一部分是想把我拉黑的。

1.指向函数的指针(函数指针)

它在Ins上的评论是这样的(如果翻译得不对,请老师指正!):

来分析这样一个声明,void (*f) (
);虽然()的优先级高于*,但由于有括号存在,首先执行的是解引用,所以f是一个指针;接下来执行(
),表明f指向一个函数,这个函数不返回任何值。现在得出结论:f是一个指向不接受参数且不返回任何值的函数的指针,简称函数指针(pointer
to function)。

图片 2

对比一下int(*p)
[100],p是一个指向含有100个整型元素的数组的指针,它们有一个共同的特点:指针声明符(*)和标识符(f或p)都被限制在一个括号中,由于括号的优先级是最高的,所以我们从标识符开始由内向外分析,即可得到以上结果。

<1>.初始化

其实,作为一名较老司机,在推荐语言时,都会优先推荐Python,而不会推荐C。因为Python语法简单、现成的可用工具很多,关于这个问题的具体讨论可以参考
“为什么知乎上大多数人不推荐C语言入门?” 。

注意指向函数的指针(函数指针)指向的是函数而非普通的变量,它所指向的函数也是有特定类型的,函数的类型由它的返回值类型以及形参列表确定,和函数名无关。对函数指针初始化时可以采用相同类型函数的函数名或函数指针(当然还有零指针常量)。假如有函数void
test ( ),int wrong_match (int)和函数指针void (*ptf) ( )。

C语言的地位如何,我想TIOBE的排名可以说明,一直稳居三甲之内。(P.S.
刚查了一下,C语言八月份排名TIOBE有史以来最低 T_T,
相关的同学要保持一颗警惕的心了。)
:

下面的初始化是错误的,因为函数指针的类型与函数的类型不匹配:

图片 3

f = wrong_match;

f = & wrong_match;

图中的代码部分: void (*

ptf = wrong_match;

实际上这里定义了一个数组f[],数组中保存了未确定个函数指针,该类函数指针是指向一个返回值为void
参数为空的函数的函数指针。**(但需注意的是,在C语言的数组定义中,是需要确定数组大小的,否则编译不过)
**

图片 4那到底是什么鬼?!!

ptf = & wrong_match;

咳咳,正确的打开理解方式是这样的:以“上帝版密室逃脱”为例

以下初始化及赋值是合法的:

  • 我们在密室逃脱中,上帝给了我们一串钥匙: key = f[]
  • 这串钥匙中的任意一把可以打开其相对应的一个房间门 ** room = **
  • 打开这个房间门后,我们需要过五关斩六将,寻找到下一关的钥匙**
    anotherKey**
  • 这个钥匙可以开启下一个房间门(因为我们第一次所开的门不同,此时我们所通往下一关的钥匙可能开启的是同一道门,也可能不是,这取决于上帝怎么设计。)
  • 下一个房间就是*anotherRoom = void *
    ,在该房间内上帝可能放了一个惊喜也可能什么都没有。*

f = test;

所以,这个定义只是相当于现实生活中的一个“双层密室逃脱”而已,这样说是不是容易理解多了?容易理解多了?容易理解多了?

图片 5………………好吧……图片 6

f = &test;

ptf = test;

Linus Torvalds said:“Talk is cheap, show me the
code
”。So,我们用代码来解析一下试试看,顺便安利一下他的TED视频:
Linux 操作系统之父.

ptf = &test;

首先,定义一个房间的钥匙的类型:

f = pf;

 typedef void (* room_key) ();//最终房间的钥匙

要做出解释的是test和&test都可以用来初始化函数指针。C语言规定函数名会被转换为指向这个函数的指针,除非这个函数名作为&操作符或sizeof操作符的操作数(注意:函数名用于sizeof的操作数是非法的)。也就是说f
= test;中test被自动转换为&test,而f=
&test;中已经显示使用了&test,所以test就不会再发生转换了。因此直接引用函数名等效于在函数名上应用&运算符,两种方法都会得到指向该函数的指针。

然后,我们创建两个最终的房间(假设一间房里面藏有1W奖金和一间房什么都没有):

<2>.通过函数指针调用函数

void nothing_in_the_room{ printf("Sorry! You Get Nothing.\\n"); } void something_in_the_room{ printf("Congratulation! You Get Ten Thounsands.\\n"); }//这里可以注意到,这是两个返回值为void,参数为空的函数。

通过函数指针调用函数可以有两种方法,直接使用函数指针或在函数指针前使用解引用运算符,如下所示:

接下来,我们需要把上面房间的钥匙藏在另一个房间里(由于我们上面定义了两个房间,那么我们这里就需要在一个或N个房间内藏有可以开上面两个房间的钥匙,为了展示我们拥有的是一串钥匙(
f[] ),这里我们定义三个房间):

f = test;

room_key get_room_key1{ room_key = nothing_in_the_room;//这里相当于把钥匙藏在该房间内,该钥匙对应的房间什么都没有 printf( "You had got the key\\n "); return room_key;}room_key get_room_key2{ room_key = something_in_the_room();//这里相当于把钥匙藏在该房间内,该钥匙对应的房间藏有1W printf( "You had got the key\\n "); return room_key;}room_key get_room_key3{ room_key = nothing_in_the_room();//这里相当于把钥匙藏在该房间内,该钥匙对应的房间同样什么都没有 printf( "You had got the key\\n "); return room_key;}//通过以上定义,我们就把上面两个房间的钥匙藏在了下面三个房间中,//其中两个房间所藏的钥匙打开的是那间什么没有的屋子,因此我们可以认为中奖概率为1/3

ptf = test;

**等灯邓蹬~~~
**此时我们讨论的重点该出场了,也就是我们所拥有的钥匙,它可以这样被使用:

f ( );

void ( *   = {get_room_key1,get_room_key2,get_room_key3};//这里就是我们所拥有的三把钥匙了,这三把钥匙分别藏在f[0]、f[1]以及f[2]中。

(*f) (
);//指针两侧的括号非常重要,表示先对f解引用,然后再调用相应的函数

把上面的函数放到一个完整的游戏过程里面就是这样的:

ptf ( );

void main { void ( *   = {get_room_key1,get_room_key2,get_room_key3}; //这里就是我们所拥有的三把钥匙了,这三把钥匙分别藏在f[0]、f[1]以及f[2]中。 printf("Welcome To My Game! --- Mr.C\\n"); int key_number = -1;//定义钥匙的编号,不同编号的钥匙对应不同的房间 int play_times = 10;//玩家可以选择钥匙的次数 int i = 0; for(i = 0; i < play_times;i++) { printf("\\n=============Game Start=============\\n"); printf("Please input your key number\\n"); scanf(“%d",&key_number);//获取玩家输入的钥匙编码 if ( key_number >= 0 && key_number < 3) { f[key_number];//通过上帝的钥匙,打开两个房间门 /*为了便于阅读,我们可以分两步调用: step1: room_key key = f[key_number];//通过第一把钥匙打开第一个房间,并获取下一把钥匙 step2: key();//通过第二把钥匙打开第二个房间门 */ }else { printf("You has not that key,I know everything.Don't cheat me\\n"); } //end if }// end for}//end main

(*ptf) ( );//括号同样不能少

运行结果如下:

以上语句都能达到调用test函数的作用。ANSI C标准将f ( )认为是(*f)(
)的简写形式,并且推荐使用f (
)形式,因为它更符合函数调用的逻辑。要注意的是:如果指向函数的指针没有初始化,或者具有0值(零指针常量),那么该指针不能在函数调用中使用。只有当指针已经初始化,或被赋值后指向某个函数才能安全地用来调用函数。

图片 7

<3>.探究函数名

现在有如下程序:

C语言博大精深,现在许多操作系统(大至Linux、unix,小至各Soc芯片的任务系统)或者大型项目的核心都是用C语言完成的,包括Python的核心也是C语言实现的。但C语言的实际应用会像本文所举的例子有那么复杂么?其实并没有,犹如上面评论所说,经常用这种充满技巧的方式写代码其实很容易被打shi的。当然,可以理解到这种复杂程度的指针个人觉得是必须而且是有一定用处的(起码比那些比较运算符优先级的题目实用多了),据说在Linux内核的网络驱动中有用到过类似本文中同一复杂程度的指针,找不到源码了,所以只能是据说了T-T.

#include

In the end, 标题似乎定得有点大,But…

void test( )

图片 8

{

我们讲究的是,格局!格局!

printf(“test called!n”);

图片 9

}

int main()

{

void (*f) ( );

f = test;

f ( );

(*f)( );

//test++;//error,标准禁止对指向函数的指针进行自增运算

//test = test +
2;//error,不能对函数名赋值,函数名也不能用于进行算术运算

printf(“%pn”, test);

printf(“%pn”, &test);

printf(“%pn”, *test);

return 0;

}

在我机器上的运行结果为:

test called!

test called!

004013EE

004013EE

004013EE

这个程序中较难理解的是3个输出语句都可以得到函数的入口地址。首先来看函数名test,它与数组名类似(注意:只是类似),是一个符号用来标识一个函数的入口地址,在使用中函数名会被转换为指向这个函数的指针,指针的值就是函数的入口地址,&test在前面已经说了:显示获取函数的地址。对于*test,可以认为由于test已经被转换成了函数指针,指向这个函数,所以*test就是取这个指针所指向的函数名,而又根据函数名会被转换指向该函数的指针的规则,这个函数也转变成了一个指针,所以*test最终也是一个指向函数test的指针。对它们采用%p格式项输出,都会得到以16进制数表示的函数test的入口地址。注意函数的地址在编译期是未知的,而是在链接时确定的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注