C语言函数指针基础
本文写的非常详细,因为我想为初学者建立一个意识模型,来帮助他们理解函数指针的语法和基础。如果你不讨厌事无巨细,请尽情阅读吧。
函数指针虽然在语法上让人有些迷惑,但不失为一种有趣而强大的工具。本文将从C语言函数指针的基础开始介绍,再结合一些简单的用法和关于函数名称和地址的趣闻。在最后,本文给出一种简单的方式来看待函数指针,让你对其用法有一个更清晰的理解。
函数指针和一个简单的函数
我们从一个非常简单的”Hello World“函数入手,来见识一下怎样创建一个函数指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <stdio.h> // 函数原型 void sayHello(); //函数实现 void sayHello(){ printf ( "hello world\n" ); } // main函数调用 int main() { sayHello(); } |
我们定义了一个名为sayHello的函数,它没有返回值也不接受任何参数。当我们在main函数中调用它的时候,它向屏幕输出出”hello world“。非常简单。接下来,我们改写一下main函数,之前直接调用的sayHello函数,现在改用函数指针来调用它。
1
2
3
4
|
int main() { void (*sayHelloPtr)() = sayHello; (*sayHelloPtr)(); } |
第二行void (*sayHelloPtr)()
的语法看起来有些奇怪,我们来一步一步分析。
- 这里,关键字
void
的作用是说我们创建了一个函数指针,并让它指向了一个返回void(也就是没有返回值)的函数。 - 就像其他任何指针都必须有一个名称一样,这里
sayHelloPtr
被当作这个函数指针的名称。 - 我们用
*
符号来表示这是一个指针,这跟声明一个指向整数或者字符的指针没有任何区别。 *sayHelloPtr
两端的括号是必须的,否则,上述声明变成void *sayHelloPtr()
,*
会优先跟void
结合,变成了一个返回指向void的指针的普通函数的声明。因此,函数指针声明的时候不要忘记加上括号,这非常关键。- 参数列表紧跟在指针名之后,这个例子中由于没有参数,所以是一对空括号
()
。 - 将上述要点结合起来,
void (*syaHelloPtr)()
的意义就非常清楚了,这是一个函数指针,它指向一个不接收参数且没有返回值的函数。
在上面的第二行代码,即void (*sayHelloPtr)() = sayHello;
,我们将sayHello这个函数名赋给了我们新建的函数指针。关于函数名的更多细节我们会在下文中讨论,现在暂时可以将其看作一个标签,它代表函数的地址,并且可以赋值给函数指针。这就跟语句int *x = &myint;
中我们把myint的地址赋给一个指向整数的指针一样。只是当我们考虑函数的时候,我们不需要加上一个取地址符&
。简而言之,函数名就是它的地址。接着看第三行,我们用代码’(*sayHelloPtr)();·‘解引用并调用了函数指针。
- 在第二行被声明之后,sayHelloPtr作为函数指针的名称,跟其他任何指针没有差别,能够储值和赋值。
- 我们对sayHelloPtr解引用的方式也与其他任何指针一样,即在指针之前使用解引用符
*
,也就是代码中的*sayHelloPtr
。 - 同样的,我们需要在其两端加上括号,即
(*sayHelloPtr)
,否则它就不被当做一个函数指针。因此,记得声明和解引用的时候都要在两端加上括号。 - 括号操作符用于C语言中的函数调用,如果有参数参与,就将其放入括号中。这对于函数指针也是相似的,即代码中的
(*sayHelloPtr)()
。 - 这个函数没有返回值,也就没有必要将它赋值给任何变量。单独来说,这个调用跟
sayHello()
没什么两样。
接下来,我们再对函数稍加修改。你会看到函数指针奇怪的语法,以及用调用普通函数的方法来调用赋值后函数指针的现象。
1
2
3
4
|
int main() { void (*sayHelloPtr)() = sayHello; sayHelloPtr(); } |
跟之前一样,我们将sayHello函数赋给函数指针。但是这一次,我们用调用普通函数的方法调用了它。稍后讨论函数名的时候我会解释这一现象,现在只需要知道(*syaHelloPtr)()
和syaHelloPtr()
是相同的即可。
带参数的函数指针
好了,这一次我们来创建一个新的函数指针吧。它指向的函数仍然不返回任何值,但有了参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <stdio.h> //函数原型 void subtractAndPrint( int x, int y); //函数实现 void subtractAndPrint( int x, int y) { int z = x - y; printf ( "Simon says, the answer is: %d\n" , z); } //main函数调用 int main() { void (*sapPtr)( int , int ) = subtractAndPrint; (*sapPtr)(10, 2); sapPtr(10, 2); } |
跟之前一样,代码包括函数原型,函数实现和在main函数中通过函数指针执行的语句。原型和实现中的特征标变了,之前的sayHello函数不接受任何参数,而这次的函数subtractAndPrint接受两个int作为参数。它将两个参数做一次减法,然后输出到屏幕上。
- 在第14行,我们通过’(*sapPtr)(int, int)’创建了sapPtr这个函数指针,与之前的区别仅仅是用
(int, int)
代替了原来的空括号。而这与新函数的特征标相符。 - 在第15行,解引用和执行函数的方式与之前完全相同,只是在括号中加入了两个参数,变成了
(10, 2)
。 - 在第16行,我们用调用普通函数的方法调用了函数指针。
带参数且有返回值的函数指针
这一次,我们把subtractAndPrint函数改成一个名为subtract的函数,让它把原本输出到屏幕上的结果作为返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include <stdio.h> // 函数原型 int subtract( int x, int y); // 函数实现 int subtract( int x, int y) { return x - y; } // main函数调用 int main() { int (*subtractPtr)( int , int ) = subtract; int y = (*subtractPtr)(10, 2); printf ( "Subtract gives: %d\n" , y); int z = subtractPtr(10, 2); printf ( "Subtract gives: %d\n" , z); } |
这与subtractAndPrint函数非常相似,只是subtract函数返回了一个整数而已,特征标也理所当然的不一样了。
- 在第13行,我们通过
int (*subtractPtr)(int, int)
创建了subtractPtr这个函数指针。与上一个例子的区别只是把void换成了int来表示返回值。而这与subtract函数的特征标相符。 - 在在第15行,解引用和执行这个函数指针,除了将返回值赋值给了y以外,与调用subtractAndPrint没有任何区别。
- 在第16行,我们向屏幕输出了返回值。
- 18到19行,我们用调用普通函数的方法调用了函数指针,并且输出了结果。
这跟之前没什么两样,我们只是加上了返回值而已。接下来我们看看另一个稍微复杂点儿的例子——把函数指针作为参数传递给另一个函数。
把函数指针作为参数来传递
我们已经了解过了函数指针声明和执行的各种情况,不论它是否带参数,或者是否有返回值。接下来我们利用一个函数指针来根据不同的输入执行不同的函数。
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> // 函数原型 int add( int x, int y); int subtract( int x, int y); int domath( int (*mathop)( int , int ), int x, int y); // 加法 x+ y int add( int x, init y) { return x + y; } // 减法 x - y int subtract( int x, int y) { return x - y; } // 根据输入执行函数指针 int domath( int (*mathop)( int , int ), int x, int y) { return (*mathop)(x, y); } // main函数调用 int main() { // 用加法调用domath int a = domath(add, 10, 2); printf ( "Add gives: %d\n" , a); // 用减法调用domath int b = domath(subtract, 10, 2); printf ( "Subtract gives: %d\n" , b); } |
我们来一步一步分析。
- 我们有两个特征标相同的函数,add和subtract,它们都返回一个整数并接受两个整数作为参数。
- 在第六行,我们定义了函数
int domath(int (*mathop)(int, int), int x, int y)
。它第一个参数int (*mathop)(int, int)
是一个函数指针,指向返回一个整数并接受两个整数作为参数的函数。这就是我们之前见过的语法,没有任何不同。它的后两个整数参数则作为简单的输入。因此,这是一个接受一个函数指针和两个整数作为参数的函数。 - 19到21行,domath函数将自己的后两个整数参数传递给函数指针并调用它。当然,也可以像这么调用。
mathop(x, y);
- 27到31行出现了我们没见过的代码。我们用函数名作为参数调用了domath函数。就像我之前说过的,函数名是函数的地址,而且能代替函数指针使用。
main函数调用了两次domath函数,一次用了add,一次用了subtract,并输出了这两次结果。
函数名和地址
既然有约在先,那我们就讨论一下函数名和地址作为结尾吧。一个函数名(或称标签),被转换成了一个指针本身。这表明在函数指针被要求当作输入的地方,就能够使用函数名。这也导致了一些看起来很糟糕的代码却能够正确的运行。瞧瞧下面这个例子。
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
34
|
#include <stdio.h> // 函数原型 void add( char *name, int x, int y); // 加法 x + y void add( char *name, int x, int y) { printf ( "%s gives: %d\n" , name, x + y); } // main函数调用 int main() { // 一些糟糕的函数指针赋值 void (*add1Ptr)( char *, int , int ) = add; void (*add2Ptr)( char *, int , int ) = *add; void (*add3Ptr)( char *, int , int ) = &add; void (*add4Ptr)( char *, int , int ) = **add; void (*add5Ptr)( char *, int , int ) = ***add; // 仍然能够正常运行 (*add1Ptr)( "add1Ptr" , 10, 2); (*add2Ptr)( "add2Ptr" , 10, 2); (*add3Ptr)( "add3Ptr" , 10, 2); (*add4Ptr)( "add4Ptr" , 10, 2); (*add5Ptr)( "add5Ptr" , 10, 2); // 当然,这也能运行 add1Ptr( "add1PtrFunc" , 10, 2); add2Ptr( "add2PtrFunc" , 10, 2); add3Ptr( "add3PtrFunc" , 10, 2); add4Ptr( "add4PtrFunc" , 10, 2); add5Ptr( "add5PtrFunc" , 10, 2); } |
这是一个简单的例子。运行这段代码,你会看到每个函数指针都会执行,只是会收到一些关于字符转换的警告。但是,这些函数指针都能正常工作。
- 在第15行,add作为函数名,返回这个函数的地址,它被隐式的转换为一个函数指针。我之前提到过,在函数指针被要求当作输入的地方,就能够使用函数名。
- 在第16行,解引用符作用于add之前,即
*add
,在返回在这个地址的函数。之后跟函数名一样,它被隐式的转换为一个函数指针。 - 在第17行,取地址符作用于add之前,即
&add
,返回这个函数的地址,之后又得到一个函数指针。 - 18到19行,add不断地解引用自身,不断返回函数名,并被转换为函数指针。到最后,它们的结果都和函数名没有区别。
显然,这段代码不是优秀的实例代码。我们从中收获到了如下知识:其一,函数名会被隐式的转换为函数指针,就像作为参数传递的时候,数组名被隐式的转换为指针一样。在函数指针被要求当作输入的任何地方,都能够使用函数名。其二,解引用符*
和取地址符&
用在函数名之前基本上都是多余的。
总结
我希望本文帮助你们认清了函数指针以及它的用途。只要你掌握了函数指针,它就是C语言中一个强大的工具。我也许会在以后的文章中讲述更多函数指针的细节用法,包括回调和C语言中基本的面向对象等等。
更新1
我删掉了关于描述(*sayHelloPrt)(void)
跟(*sayHelloPrt)()
相同的那一部分,那其实是错误的。在评论区中,Dave G给出了一个关于这个问题很好的解释。
C语言函数指针基础的更多相关文章
- C#委托与C语言函数指针及函数指针数组
C#委托与C语言函数指针及函数指针数组 在使用C#时总会为委托而感到疑惑,但现在总新温习了一遍C语言后,才真正理解的委托. 其实委托就类似于C/C++里的函数指针,在函数传参时传递的是函数指针,在调用 ...
- “对外部(局部)变量的访问”是C语言函数指针的最大弱点
1.“对外部(局部)变量的访问”是C语言函数指针的最大弱点 . #include <stdio.h> #include <stdlib.h> /* 结构体定义 */ struc ...
- C语言函数指针 和 OC-Block
C语言函数指针 和 OC-Block 一. C语言函数指针 关于函数指针的知识详细可参考:http://www.cnblogs.com/mjios/archive/2013/03/19/2967037 ...
- 深入理解C语言-函数指针
函数指针在C++中有着重要的应用,函数的函数名其本质就是代表一个地址,这个地址叫做函数入口,得到这个地址就可以对这个函数进行各种操作. 函数类型基础 函数三要素: 名称.参数.返回值 C语言中的函数有 ...
- C语言函数指针实验
上次看Atmel的示例工程,发现人家使用了函数指针的结构体(函数指针结构体).感叹人家的C语言功夫审核,自己费劲还是只能读懂的份.不过,函数指针确实好用.今天就试试这个超牛的东西.Now let's ...
- C语言函数指针的用法
函数指针是一种在C.C++.D语言.其他类 C 语言和Fortran 2003中的指针.函数指针可以像一般函数一样,用于调用函数.传递参数.在如 C 这样的语言中,通过提供一个简单的选取.执行函数的方 ...
- c语言函数指针的理解与使用
1.函数指针的定义 顾名思义,函数指针就是函数的指针.它是一个指针,指向一个函数.看例子: A) char * (*fun1)(char * p1,char * p2); B) char * *fun ...
- C语言函数指针(转载)
二.通常的函数调用 一个通常的函数调用的例子:/* 自行包含头文件 */void MyFun(int x); /* 此处的声明也可写成:void MyFun(int) */int main(int a ...
- C语言函数指针变量和指针函数以及指针数组
C语言中,一个函数总是占用一段连续的内存区,而函数名就是该函数所占内存区的首地址.我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使该指针变量指向该函数.然后通过指针变量就可以找到并调用这 ...
随机推荐
- PHP中数据库的连接
<?php //1.链接MySQL服务器 $conn = mysql_connect("localhost", "root" , 199452); //2 ...
- 批量关闭 WordPress 的 Pingback 和 Trackback 功能
方法很简单,WordPress 后台即可实现,在设置-讨论中把"接收来自外部博客的引用通告(Pingback 和 Trackback)"这一项勾选去掉,保存设置.这样,以后新增的文 ...
- build a git repo and clone
First machine: git init --bare gitrepo.git Second machine: git clone user@server:~/gitrepo.git cd gi ...
- HDU1006
Problem Description The three hands of the clock are rotating every second and meeting each other ma ...
- java中的接口回调
[接口回调]接口回调是多态的另一种体现.接口回调是指:可以把使用某一个接口的类创建的对象的引用赋给该接口声明的接口变量中,那么该接口变量就可以调用被类实现的接口中的方法.当接口变量调用被类实现的接口中 ...
- rman 命令
OS: Oracle Linux Server release 5.7 DB: Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - ...
- 一段高质量的SQL从问问题开始(笔记)
首先SQL书写的目的是为了解决问题,因此只有明白了要解决的问题,才能写出更加高效的SQL语句,才能优雅的解决问题,获得更多的快乐! 在写一个SQL语句的时候不妨像优化器一样思考,问自己以下的这些问题, ...
- 模糊查询&&日期时间操作
一.模糊查询 1.采用“_”.“%”通配符进行查询 select * from Students where stu_name like '张_';--一个‘_’表示一个字符 select * fro ...
- hdu 1872 稳定排序
题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=1872 稳定排序 Description 大家都知道,快速排序是不稳定的排序方法.如果对于数组中出现的任 ...
- BASE64与单向加密算法MD5&SHA&MAC
言归正传,这里我们主要描述Java已经实现的一些加密解密算法,最后介绍数字证书. 如基本的单向加密算法: BASE64 严格地说,属于编码格式,而非加密算法 MD5(Message Diges ...