无情的搬砖机器= =
一、函数指针的概念
1.1 什么是函数指针
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
- 函数指针 和 函数名 本质上是一样的,都是指向函数调用地址的指针;
- 只是 函数名 是常量指针,函数指针 是变量指针。
1.2 如何定义函数指针
那么这个指针变量怎么定义呢?
虽然同样是指向一个地址,但指向函数的指针变量和指向变量的指针变量的定义方式是不同的。例如:
1 | int(*p)(int, int); |
这个语句就定义了一个指向函数的指针变量p。首先它是一个指针变量,所以要有一个“*”,即(*p);其次前面的 int 表示这个指针变量可以指向返回值类型为int
型的函数;后面括号中的两个int
表示这个指针变量可以指向有两个参数且都是int
型的函数。所以合起来这个语句的意思就是:定义了一个指针变量p
,该指针变量可以指向返回值类型为int
型,且有两个整型参数的函数。p
的类型为int(*)(int,int)
。
所以函数指针的定义方式为:
函数返回值类型 (* 指针变量名) (函数参数列表);
- “函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;
- “函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。
1.3 注意要点
函数指针定义时,(*指针变量名)”两端的括号不能省略
我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数(指针函数)。
如何判断 指针变量 是 指向 变量 还是 函数 ?
- 首先看变量名前面有没有“*”,如果有“*”说明是指针变量;
- 其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量;
- 最后需要注意的是,指向函数的指针变量没有 ++ 和 – 运算。
二、举例
函数指针怎么用?
2.1 调用例子
要点分为两个:
- 指针 赋值 为函数地址:
函数指针是需要把一个函数的地址赋值给它,有两种写法:
1 | fun = &Function; |
取地址运算符&
不是必需的,因为一个函数标识符就表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表。
- 调用 函数指针:
调用函数指针的方式也有两种:
1 | x = (*fun)(); |
调用函数的写法(*func)()、func()均可;而我们大多数情况下都会写成前者(看上去和普通的函数调用没啥区别),应该是(C/C++标准制定者)为了方便大家对函数的调用。如果可以的话,建议使用第一种,因为可以清楚的指明这是通过指针的方式来调用函数。
简单例子
1 | int Func(int x); /*声明一个函数*/ |
- 赋值时函数 Func 不带括号,也不带参数。
- 由于函数名Func代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。
2.2 实际例子
1 |
|
输出结果是:
1 | please enter a and b:3 4 |
三、函数指针的优点
有的通用函数中要涉用到另一个函数,但函数名称未定,是让用户编的,当然要做成函数指针,即API(application programming interface,应用编程接口)。函数指针的优点,其实就是函数指针的应用:回调函数
回调函数,本质上都是“你想让别人的代码执行你的代码,而别人的代码你又不能动”这种需求下产生的。
举例1
别人给你的不是源代码,是一个已经编译好的模块,并且不会给你源代码,那是商业机密,留给你一个接口,你把你要执行的代码以回调函数的形式交给这个接口,由别人编写的模块在需要的时候调用。
举例2
进行通用定积分计算,被计算函数是待定的,是由调用者确定的函数,这也得是函数指针。
四、回调函数
对指针的应用是C语言编程的精髓所在,而回调函数就是C语言里面对函数指针的高级应用。简而言之,回调函数是一个通过函数指针调用的函数。
- 函数指针和函数名本质上是一样的,都是指向函数调用地址的指针;
- 只是函数名是常量指针,函数指针是变量指针。
4.1 回调函数的定义
把函数指针(函数的入口地址)传递给另一个函数(的形参),当这个函数指针被用来调用它所指向的函数时,我们就说这个函数是回调函数。
4.2 回调函数的意义(简易)
- 其他情况:当一个函数要调用另外一个函数,直接在函数体调用对应的函数;
- 回调情况:当一个函数要调用另外一个函数,将另外函数的指针作为形参,当需要调用对应函数时,调用指针进而可调用对应函数。
很多朋友可能会想,为什么不像普通函数调用那样,在回调的地方直接写函数的名字呢?这样不也可以吗?为什么非得用回调函数呢?在网上看到解析回调函数的很多例子,其实完全可以用普通函数调用来实现的。
要回答这个问题,我们先来了解一下回到函数的好处和作用:那就是解耦。
对,就是这么简单的答案,就是因为这个特点,普通函数代替不了回调函数。
当你在库函数传入其他函数指针,只要函数指针的函数类型相同,就能做到改动函数指针指向的函数功能,而且还不影响传入(函数)指针形参的库函数(不需要改动)。甚至你可以调用同类型的函数指针 传入 库函数。(例如多驱动,Uart4和Uart5两个串口驱动,只要传入函数指针变动传入指针就行)。
4.3 回调函数的意义(深入)
函数指针的语法理解上并不难,难就难在对函数指针实现设计模式和设计方法上的运用。
原因在于,难的不是函数指针的概念和语法本身,而是在什么时候,什么地方该使用它。函数指针不仅是语法上的问题,更重要的是它是一个设计范畴。真正的高手当然不单应该懂得语法层面上的技巧,更应该懂得设计上的方法。不懂设计,能算高手吗?怀疑我在夸大其辞吗?那我们先看看函数指针与哪些设计方法有关:
与分层设计有关。分层设计早就不是什么新的概念,分层的好处是众所周知的,比较明显好处就是简化复杂度、隔离变化。采用分层设计,每层都只需关心自己的东西,这减小了系统的复杂度,层与层之间的交互仅限于一个很窄的接口,只要接口不变,某一层的变化不会影响其它层,这隔离了变化。
分层的一般原则是,上层可以直接调用下层的函数,下层则不能直接调用上层的函数。这句话说来简单,在现实中,下层常常要反过来调用上层的函数。比如你在拷贝文件时,在界面层调用一个拷贝文件函数。界面层是上层,拷贝文件函数是下层,上层调用下层,理所当然。但是如果你想在拷贝文件时还要更新进度条,问题就来了。一方面,只有拷贝文件函数才知道拷贝的进度,但它不能去更新界面的进度条。另外一方面,界面知道如何去更新进度条,但它又不知道拷贝的进度。怎么办?常见的做法,就是界面设置一个回调函数给拷贝文件函数,拷贝文件函数在适当的时候调用这个回调函数来通知界面更新状态。
与抽象有关。抽象是面向对象中最重要的概念之一,也是面向对象威力强大之处。面向对象只是一种思想,大家都知道,用C语言一样可以实现面向对象的编程。这可不是为了赶时髦,而是一种实用的方法。如果你对此表示怀疑,可以去看看GTK+、linux kernel等开源代码。
接口是最高级的抽象。在linux kernel里面,接口的概念无处不在,像虚拟文件系统(VFS),它定义一个文件系统的接口,只要按照这种接口的规范,你可以自己开发一个文件系统挂上去。设备驱动程序更是如此,不同的设备驱动程序有自己一套不同的接口规范。在自己开发设备开发驱动程序时,只要遵循相应的接口规范就行了。接口在C语言中如何表示?很简单,就是一组函数指针。
与接口与实现分开有关。针对接口编程,而不是针对实现编程,此为《设计模式》的第一条设计准则。分开接口与实现的目标是要隔离变化。软件是变化的,如果不能把变化的东西隔离开来,导致牵一发而动全身,代价是巨大的。这是大家所不愿看到的。
C语言既然可以实现面向对象的编程,自然可以利用设计模式来分离接口与实现。像桥接模式、策略模式、状态模式、代理模式等等,在C语言中,无一不需要利用函数指针来实现。
与松耦合原则有关。面向过程与面向对象相比,之所以显得苍白无力,原因之一就是它不像面向对象一样,可以直观的把现实模型映射到计算机中。面向过程讲的是层层控制,而面向对象更强调的对象间的分工合作。现实世界中的对象处于层次关系的较少,处于对等关系的居多。也就是说,对象间的交互往往是双向的。这会加强对象间的耦合性。
耦合本身没有错,实际上耦合是必不可少的,没有耦合就没有协作,对象之间无法形成一个整体,什么事也做不了。关键在于耦合要恰当,在实现预定功能的前提下,耦合要尽可能的松散。这样,系统的一部分变化对其它部分的影响会很少。
函数指针是解耦对象关系的最佳利器。Signal(如boost的signal和glib中的signal)机制是一个典型的例子,一个对象自身的状态可能是在变化的(或者会触发一些事件),而其它对象关心它的变化。一旦该对象有变化发生,其它对象要执行相应的操作。
如果该对象直接去调用其它对象的函数,功能是完成了,但对象之间的耦合太紧了。如何把这种耦合降到最低呢,signal机制是很好的办法。它的原理大致如下:其它关注该对象变化的对象主动注册一个回调函数到该对象中。一旦该对象有变化发生,就调用这些回调函数通知其它对象。功能同样实现了,但它们之间的耦合度降低了
在C语言中,要解决以上这些问题,不采用函数指针,将是非常困难的。在编程中,如果你从没有想到用函数指针,很难想像你是一个C语言高手。
4.4 回调函数的意义(总结)
我们对回调函数的使用无非是对函数指针的应用,函数指针的概念本身很简单,但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。
在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路,回调函数会绝对的变化系统的运行轨迹,执行顺序,调用顺序。回调函数的出现会让读到你的代码的人非常的懵头转向。
那么什么是回调函数呢,那是不得以而为之的设计策略,想象一种系统实现:在一个下载系统中有一个文件下载模块和一个下载文件当前进度显示模块,系统要求实时的显示文件的下载进度,想想很简单在面向对象的世界里无非是实现两个类而已。但是问题恰恰出在这里,显示模块如何驱动下载进度条?显示模块不知道也不应该知道下载模块所知道的文件下载进度(面向对象设计的封装性,模块间要解耦,模块内要内聚),文件下载进度是只有下载模块才知道的事情,解决方案很简单给下载模块传递一个函数指针作为回调函数驱动显示模块的显示进度。
在面向对象的世界中这样的例子还真不少,造成这样的问题的根源,相信大家已经从上面的叙述中体会到了,就是面向对象的程序设计思想,就是设计模式中要求的模块独立性,高内聚低耦合等特性。
封装变化的编程策略给编程人员第一位的指导思想就是面向接口编程,即设计模式中提到的面向虚拟编程而不是面向实现。这样的编程思想极大地革新了编程世界,可以说没有这一原则就没有面向对象的程序设计,这一原则给程序设计一种指导思想即如何更高的将现实模型映射成程序模型。这样的设计思想在极大地催生高度独立性模块的同时削弱了模块间的协作性,也就是耦合性,它使得模块间更多的从事着单向的调用工作,一个模块需要某种服务就去找另一个模块,这使得程序呈现出层次性,高层通过接口调用底层,底层提供服务。但是现实世界中严格遵循现层次特性的系统是很少见的,绝对的MVC是不存在的,因为更多的模块要求通并协作,可见没有耦合就没有协作没有好的调用关系,耦合真的不是错。
既然我们需要模块间的协作,同时我们又厌恶的摒弃模块间你中有我我中有你的暧昧关系那如何生成系统呢,答案是函数指针(不一定一定是函数指针)也就是使用回调的方式。如果一个对象关心另一个对象的状态变化那么给状态的变化注册回调函数让它通知你这类状态的改变,这样在封装了模块变化的同时实现了模块间的协作关系另辟独径的给对象解耦。
五、回调函数应用
5.1 typedef void (*pFunc)();
事实上,为了代码的移植考虑,一般使用typedef定义函数指针类型。
1 | //typedef int (*funcptr)(); |
有了函数指针类型,以后我们就可以象变量一样声明函数指针,如下例:
1 |
|
5.2 怎么使用带参数的回调函数?
1 |
|
运行结果:
1 | Entering Main Function. |
终端显示:
1 | 1000 C++ |
Ps:无论是什么类型(结构体也是)的指针,都要主要初始化(或赋内存)。
5.3 结构体拓展——协议类
c语言中,如何在结构体中实现函数的功能?把结构体做成和类相似,让他的内部有属性,也有方法,
这样的结构体一般称为协议类,提供参考:
1 | typedef struct |
每次都要注意:需要初始化。
该回调函数的用法为:
- 甲方进行结构体的定义(成员中包括回调函数的指针)
- 乙方定义结构体变量,并向甲方注册,
- 甲方收集N个乙方的注册形成结构体链表,在某个特定时刻遍历链表,进行回调。
- 当函数指针做为函数的参数,传递给一个被调用函数,被调用函数就可以通过这个指针调用外部的函数,这就形成了回调
- 一般的程序中回调函数作用不是非常明显,可以不使用这种形式
- 最主要的用途就是当函数不处在同一个文件当中,比如动态库,要调用其他程序中的函数就只有采用回调的形式
- 通过函数指针参数将外部函数地址传入来实现调用函数的代码作了修改,也不必改动库的代码(只是函数指针指向的函数修改变动,库的处理没有变动),就可以正常实现调用便于程序的维护和升级