深入理解函数指针

函数指针虽然在语法上让人有些迷惑,但不失为一种有趣而强大的工具。本文将从C语言函数指针的基础开始介绍,再结合一些简单的用法和关于函数名称和地址的趣闻。在最后,本文给出一种简单的方式来看待函数指针,让你对其用法有一个更清晰的理解。

函数指针和一个简单的函数

我们从一个非常简单的Hello World函数入手,来见识一下怎样创建一个函数指针。

  1. #include <stdio.h>
  2. // 函数原型
  3. void sayHello();
  4. //函数实现
  5. void sayHello(){
  6. printf("hello world\n");
  7. }
  8. // main函数调用
  9. int main() {
  10. sayHello();
  11. }

我们定义了一个名为sayHello的函数,它没有返回值也不接受任何参数。当我们在main函数中调用它的时候,它向屏幕输出出hello
world
非常简单。接下来,我们改写一下main函数,之前直接调用的sayHello函数,现在改用函数指针来调用它。

  1. int main() {
  2. void (*sayHelloPtr)() = sayHello;
  3. (*sayHelloPtr)();
  4. }

第二行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. int main() {
  2. void (*sayHelloPtr)() = sayHello;
  3. sayHelloPtr();
  4. }

跟之前一样,我们将sayHello函数赋给函数指针。但是这一次,我们用调用普通函数的方法调用了它。稍后讨论函数名的时候我会解释这一现象,现在只需要知道(*syaHelloPtr)()syaHelloPtr()是相同的即可。

带参数的函数指针

好了,这一次我们来创建一个新的函数指针吧。它指向的函数仍然不返回任何值,但有了参数。

  1. #include <stdio.h>
  2. //函数原型
  3. void subtractAndPrint(int x, int y);
  4. //函数实现
  5. void subtractAndPrint(int x, int y) {
  6. int z = x - y;
  7. printf("Simon says, the answer is: %d\n", z);
  8. }
  9. //main函数调用
  10. int main() {
  11. void (*sapPtr)(int, int) = subtractAndPrint;
  12. (*sapPtr)(10, 2);
  13. sapPtr(10, 2);
  14. }

跟之前一样,代码包括函数原型,函数实现和在main函数中通过函数指针执行的语句。原型和实现中的特征标变了,之前的sayHello函数不接受任何参数,而这次的函数subtractAndPrint接受两个int作为参数。它将两个参数做一次减法,然后输出到屏幕上。

在第14行,我们通过(*sapPtr)(int, int)创建了sapPtr这个函数指针,与之前的区别仅仅是用(int,
int)
代替了原来的空括号。而这与新函数的特征标相符。在第15行,解引用和执行函数的方式与之前完全相同,只是在括号中加入了两个参数,变成了(10, 2)

在第16行,我们用调用普通函数的方法调用了函数指针。

带参数且有返回值的函数指针

这一次,我们把subtractAndPrint函数改成一个名为subtract的函数,让它把原本输出到屏幕上的结果作为返回值。

  1. #include <stdio.h>
  2. // 函数原型
  3. int subtract(int x, int y);
  4. // 函数实现
  5. int subtract(int x, int y) {
  6. return x - y;
  7. }
  8. // main函数调用
  9. int main() {
  10. int (*subtractPtr)(int, int) = subtract;
  11. int y = (*subtractPtr)(10, 2);
  12. printf("Subtract gives: %d\n", y);
  13. int z = subtractPtr(10, 2);
  14. printf("Subtract gives: %d\n", z);
  15. }

这与subtractAndPrint函数非常相似,只是subtract函数返回了一个整数而已,特征标也理所当然的不一样了。

在第13行,我们通过int (*subtractPtr)(int, int)创建了subtractPtr这个函数指针。与上一个例子的区别只是把void换成了int来表示返回值。而这与subtract函数的特征标相符。

在在第15行,解引用和执行这个函数指针,除了将返回值赋值给了y以外,与调用subtractAndPrint没有任何区别。

在第16行,我们向屏幕输出了返回值。

18到19行,我们用调用普通函数的方法调用了函数指针,并且输出了结果。

这跟之前没什么两样,我们只是加上了返回值而已。接下来我们看看另一个稍微复杂点儿的例子——把函数指针作为参数传递给另一个函数。

把函数指针作为参数来传递

我们已经了解过了函数指针声明和执行的各种情况,不论它是否带参数,或者是否有返回值。接下来我们利用一个函数指针来根据不同的输入执行不同的函数。

  1. #include <stdio.h>
  2. // 函数原型
  3. int add(int x, int y);
  4. int subtract(int x, int y);
  5. int domath(int (*mathop)(int, int), int x, int y);
  6. // 加法 x+ y
  7. int add(int x, init y) {
  8. return x + y;
  9. }
  10. // 减法 x - y
  11. int subtract(int x, int y) {
  12. return x - y;
  13. }
  14. // 根据输入执行函数指针
  15. int domath(int (*mathop)(int, int), int x, int y) {
  16. return (*mathop)(x, y);
  17. }
  18. // main函数调用
  19. int main() {
  20. // 用加法调用domath
  21. int a = domath(add, 10, 2);
  22. printf("Add gives: %d\n", a);
  23. // 用减法调用domath
  24. int b = domath(subtract, 10, 2);
  25. printf("Subtract gives: %d\n", b);
  26. }

我们来一步一步分析。

我们有两个特征标相同的函数,addsubtract,它们都返回一个整数并接受两个整数作为参数。

在第六行,我们定义了函数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,并输出了这两次结果。

补充,编程建议:

使用int domath(int (*mathop)(int, int), int x, int y)这样的声明,不管是对阅读者还是代码编写者都是一种折磨,既然mathop只是一中函数指针,那为什么不把它定义为一种函数指针呢。

typedef int (*mathop)(int, int);

那么上述的声明也就变为:

int domath(mathop fptr, int x, int y)

是不是更清晰、简洁了呢?

我们进一步考虑返回函数指针是个什么样子:

  1. mathop select(char opcode)
  2. {
  3. switch(opcode){
  4. case ‘+‘: return add;
  5. case ‘-‘: return subtract;
  6. }
  7. }

现在就可以int b = domath(‘-‘, 10, 2);调用,更加直观。也让我们了解了使用函数指针作为返回值是个什么样子。

再进一步,函数指针数组会是个什么样子呢?

两种声明方式:

  1. typedef int (*mathop)(int, int);
  2. mathop mathtypes[128] = {NULL};

也可以使用原声方式来声明它:

  1. int (*mathtypes[128])(int, int) = {NULL};

函数名和地址

既然有约在先,那我们就讨论一下函数名和地址作为结尾吧。一个函数名(或称标签),被转换成了一个指针本身。这表明在函数指针被要求当作输入的地方,就能够使用函数名。这也导致了一些看起来很糟糕的代码却能够正确的运行。瞧瞧下面这个例子。

  1. #include <stdio.h>
  2. // 函数原型
  3. void add(char *name, int x, int y);
  4. // 加法 x + y
  5. void add(char *name, int x, int y) {
  6. printf("%s gives: %d\n", name, x + y);
  7. }
  8. // main函数调用
  9. int main() {
  10. // 一些糟糕的函数指针赋值
  11. void (*add1Ptr)(char*, int, int) = add;
  12. void (*add2Ptr)(char*, int, int) = *add;
  13. void (*add3Ptr)(char*, int, int) = &add;
  14. void (*add4Ptr)(char*, int, int) = **add;
  15. void (*add5Ptr)(char*, int, int) = ***add;
  16. // 仍然能够正常运行
  17. (*add1Ptr)("add1Ptr", 10, 2);
  18. (*add2Ptr)("add2Ptr", 10, 2);
  19. (*add3Ptr)("add3Ptr", 10, 2);
  20. (*add4Ptr)("add4Ptr", 10, 2);
  21. (*add5Ptr)("add5Ptr", 10, 2);
  22. // 当然,这也能运行
  23. add1Ptr("add1PtrFunc", 10, 2);
  24. add2Ptr("add2PtrFunc", 10, 2);
  25. add3Ptr("add3PtrFunc", 10, 2);
  26. add4Ptr("add4PtrFunc", 10, 2);
  27. add5Ptr("add5PtrFunc", 10, 2);
  28. }

这是一个简单的例子。运行这段代码,你会看到每个函数指针都会执行,只是会收到一些关于字符转换的警告。但是,这些函数指针都能正常工作。

在第15行,add作为函数名,返回这个函数的地址,它被隐式的转换为一个函数指针。我之前提到过,在函数指针被要求当作输入的地方,就能够使用函数名。

在第16行,解引用符作用于add之前,即*add,返回在这个地址的函数。之后跟函数名一样,它被隐式的转换为一个函数指针。

在第17行,取地址符作用于add之前,即&add,返回这个函数的地址,之后又得到一个函数指针。

18到19行,add不断地解引用自身,不断返回函数名,并被转换为函数指针。到最后,它们的结果都和函数名没有区别。

显然,这段代码不是优秀的实例代码。我们从中收获到了如下知识:

  • 函数名会被隐式的转换为函数指针,就像作为参数传递的时候,数组名被隐式的转换为指针一样。在函数指针被要求当作输入的任何地方,都能够使用函数名。
  • 解引用符*和取地址符&用在函数名之前基本上都是多余的。
时间: 2024-11-09 03:00:40

深入理解函数指针的相关文章

理解函数指针

1.直接调用函数 void fun(int x); //此处的声明也可写成:void fun( int ); int main(int argc, char* argv[]) { fun(10); //调用函数 return 0; } /*定义*/ void fun(int x) { printf(“%d\n”,x); } 2.函数指针 void fun(int x); //也可写成:void fun( int ); void (*fp)( int ); //声明一个函数指针,也可声明成void

iOSDay09C语言函数指针

本次主要学习和理解函数指针 1.函数指针 1 void printValue(int number) { 2 printf("number = %d\n", number); 3 } 4 int main(int argc, const char * argv[]) { 5 void (*p1)(int) = NULL; 6 p1 = printValue; 7 p1(5); 8 } 1> 定义 代码第5行 函数类型:int (int, int) 函数指针的类型:int (*p)

转:函数指针数组的妙用(I)

转自:http://blog.sina.com.cn/s/blog_4c78b35f010008hi.html 笔者在开发某软件过程中遇到这样一个问题,前级模块传给我二进制数据,输入参数为 char* buffer 和 int length,buffer是数据的首地址,length表示这批数据的长度.数据的特点是:长度不定,类型不定,由第一个字节(buffer[0])标识该数据的类型,共有256(2的8次方)种可能性.我的任务是必须对每一种可能出现的数据类型都要作处理,并且我的模块包含若干个函数

函数回调以及函数指针

code 其实只是行为的一种描述,code可以编码成为data,将data解释运行的时候,也会变成code.code和data可以不用区分,统一称为信息. ##统一code.data的共性(数据和代码的统一) 有些语言不单可以传递函数,函数里面又用到一些外部信息(包括code, data).那些语言可以将函数跟函数所用到的信息一起传递存储. 这种将函数和它所用的信息作为一个整体,就为闭包. ### 闭包在swift语言中经常会用到. ### 将代码和数据打通,统一起来,是一个槛.比如一些修改自身

函数指针及函数指针数组的妙用

笔者在开发某软件过程中遇到这样一个问题,前级模块传给我二进制数据,输入参数为 char* buffer和 int length,buffer是数据的首地址,length表示这批数据的长度.数据的特点是:长度不定,类型不定,由第一个字节(buffer[0])标识该数据的类型,共有256(28 )种可能性.我的任务是必须对每一种可能出现的数据类型都要作处理,并且我的模块包含若干个函数,在每个函数里面都要作类似的处理.若按通常做法,会写出如下代码: void MyFuntion( char* buff

深入理解C语言函数指针(转)

本文转自:http://www.cnblogs.com/windlaughing/archive/2013/04/10/3012012.html 示例1: void myFun(int x); //声明也可写成:void myFun( int ); int main() { myFun(100);//一般的函数调用 return 0; } void myFun(int x) { printf("myFun: %d\n",x); } 我们一开始只是从功能上或者说从数学意义上理解myFun

深入理解指针—&gt;指针函数与函数指针的区别

一. 在学习过程中发现这"指针函数"与"函数指针"容易搞错,所以今天,我自己想一次把它搞清楚,找了一些资料,首先它们之间的定义: 1.指针函数是指带指针的函数,即本质是一个函数.函数返回类型是某一类型的指针 类型标识符    *函数名(参数表) int *f(x,y); 首先它是一个函数,只不过这个函数的返回值是一个地址值.函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有函数返回值,而且,在主调函数中,函数返回值必须赋给同类型的指针变量. 表示: f

【实习记】2014-08-27堆排序理解总结+使用typedef指代函数指针

过程记录 4个月前C语言版的七大排序算法实践让我在写C++版时轻车熟路.特别是冒泡,插入,希尔,选择这四种排序不用调试即运行成功.输出的效果与C语言做的版本完全一样,其中令我印象深刻的是,cout对浮点的处理远不如printf简单明了.非常让开发者难受. 写C++版时有所改进. #define sortfunc _selsort 可以用 typedef void (*sort_t)(vector<int>& arr); sort_t sortfunc = _selsort; 两句代替.

c语言函数指针的理解与使用

转载:http://www.cnblogs.com/haore147/p/3647262.html 1.函数指针的定义 顾名思义,函数指针就是函数的指针.它是一个指针,指向一个函数.看例子: A) char * (*fun1)(char * p1,char * p2); B) char * *fun2(char * p1,char * p2); C) char * fun3(char * p1,char * p2); 看看上面三个表达式分别是什么意思? C)这很容易,fun3是函数名,p1,p2