The Limitations of Classical PID Controller and Its Advanced Derivations

Since founded by N. Weiner in 1947, the control theory has been evolved for more than 60 years and is still full of challenges and opportunities. The most important principle of the control theory, in my opinion, is the feedback mechanism. Without feedback and closed-loop, almost no algorithm and control technique can be implied. The idea of feedback is that by comparing the reference input and the actual output, an error signal can be obtained and then can be used by the controller to trace and eliminate the difference between the input and the output. Apart from Watt’s steam engine, one could say that the first formally implication of (negative) feedback is the amplifier invented by H.S. Black. It is a genius idea when first came out in 1927 and was proved to be an extremely useful way to solve electronic and control problems. The idea of output feedback has also been extended to state feedback and error feedback to achieve state control and estimation in more advanced control techniques.

Classical control is the foundation of control theory and it is more concentrated on analysing the stability and performance of a controlled plant. However, only linear and SISO systems have been discussed in classical control theory. Although traditional control techniques such as PID controller are still widely used in industry, they cannot handle more complex engineering scenarios such as aerospace, chemistry and biology. Another problem of classical control is that all parameters are designed and tuned based on the current system model, in which case the system will be more vulnerable to further disturbance and parameters varying.

In order to solve these problems of classical PID controller which mentioned before, more advanced approaches have been derived nowadays. If using classical approach to control a MIMO system, one should divide the system into different modes and control each mode separately. However if the system inputs and outputs are coupled with each other, it cannot be decoupled and this method will not be practicable anymore. Here comes the state-space method, which solved the limitation of classical control by using state variables. The advantage of state-space is that it can be represented by matrices and such is very computer-friendly. State-space representation is actually defined in time domain instead of frequency domain and every state can have some extend of physical meaning which gives some clues about what is happening inside a controlled plant. One milestone which makes the state-space method more practicable is the invention of Kalman filter. Kalman filter uses a series of history measurements in the presence of noise to estimate the current state of the system. Kalman filter can work as a state estimator or simply a special filter which uses the physical system model to remove the process and the measurement noise.

Optimal control method such as MPC and LQR is another derivation of classical control. In most circumstances, there are more than one possible control inputs which can drive the system to work properly, but we need is to find the optimal one. Optimal control actually transforms the control problem into an optimal problem which tries to minimise an objective function to get the best outcome. Another advantage of optimal control is that it can take constraints into consideration. One defect of PID controller is that it cannot handle system constraints like actuator saturation or output limitation. In the optimal case control, design a controller with constraints could be feasible.

It is also known that no system is constant and some parameters are likely to vary with time or to the working condition. In classical control, the controller is designed just for the current system model and thus may loss performance or even be unstable due to the system change and uncertainties. In such aspect, adaptive control or robust control may be more applicable. Both adaptive control and robust control are designed to cope with uncertainties. The difference is that adaptive control identifies the system model and changes its parameters in real-time, but robust control fixed its parameters after deployed to the plant. For the truth that adaptive control has to calculate the system model every few periods, it needs much more computational time. What’s more, since the control parameters in the adaptive controller are changing every time, it may be difficult to prove its stability.  On the other hand, the gain of robust controller has already been designed before applied to the system, so it doesn’t need to do additional calculation during the operation. Since robust controller is globally optimised and especially designed to handle uncertainties, it may not have a performance as good as other controllers. But since the real control problems are always not ideal, it is meaningful to take uncertainties and disturbance into the system model.

Some more advanced control techniques such as neural network and expert control are being discussed today. In my opinion, these new approaches have the potential to be the next generation of control theory. With the developing of computer science, it is now possible to model extremely complex networks. This kind of controller can actually take all the possible system states and its corresponding solutions into a database and each time just search for the best solution according to the current system data.  New techniques such as machine learning can also be absorbed into the controller and make the controller more flexible which can handle different control problems using a same configuration.

However, no matter how powerful the control method is, there are rarely situations where we do not need to make trade-offs. As human-beings, we always need to make decisions and balance the income and the expense. Being too greedy is like giving an infinite gain to a helicopter, which may work at the beginning but will suddenly crash whenever there is any disturbance. So push yourself while keep in mind that you have limitation. Take it easy, be adaptive to the environment and always try to get the optimal solution of your life.

REFERENCES

[1] R.C. Dorf & R.H. Bishop, Modern Control Systems (Twelfth Edition), Pearson, USA.

[2] Wikipedia, Harold Stephen Black. Available at: http://en.wikipedia.org/wiki/Harold_Stephen_Black. Last accessed 26th Mar 2014

[3] E.F. Camacho and C. Bordons, Modern Predictive Control, Springer, London, 2003

【RPi树莓派使用指南】树莓派官方7寸屏入门指南

1. 引子

在树莓派官方触摸屏发布之前,市场上可用的屏幕有以下三种:

  • 直接和GPIO插口对接的屏幕,使用SPI与CPU进行通信。需要特殊的驱动程序将framebuffer的内容发送到LCD控制器上,一般带有触屏功能,大小以3.5寸为主流。受限于SPI通信速度,刷新速率不高;
  • 专用USB接口的屏幕,如RoboPeak Mini USB Display。这类屏幕通过USB连接,需要本地运行驱动程序;
  • 通用LCD屏幕,通过HDMI和树莓派连接。因其通用性不需要特殊的驱动程序,但是很多都不支持触屏功能,而且都需要额外的转接板,体积较大;

rpilcd-front-with-base
▲ 图.  树莓派官方7寸屏实拍

我自己的需求是将树莓派作为信息显示中心,在屏幕上显示我的HP服务器的运行信息,另外提供一些快捷的传感器监控和控制操作接口。最初一直在官方屏幕和HDMI屏幕之间犹豫,最后还是选择了官方触摸屏。归结起来主要有几个原因:

  • 官方屏的LCD模组最有保证,淘宝上的HDMI LCD一般成像质量不高;
  • 官方屏的触摸功能在所有方案中是支持的最好的,有十点电容触摸(目前Raspbian还只支持单点,以后会升级),且不需要额外驱动。而HDMI接口的LCD如果有触摸功能,都需要额外接一根USB用于提供触摸控制;
  • 官方触屏和树莓派3可以直接通过铜柱物理连接,无需额外的驱动电路板。连线也非常少,只需要一根DSI软排线和供电接口即可。

总体上来说,虽然官方屏的价格高了一些,但是却是所有方案中最可靠、简洁的,所以最后也没有多犹豫就从网上下单了。rpilcd-front-without-base
▲ 图. 树莓派官方LCD屏实拍 – 正面

Read more »

【C语言深入】C/C++变量命名规范

目前主流的C/C++命名风格有两种:一种是Windows风格的匈牙利命名法,主要是采用类型前缀 + 变量名首字母大写,另一个就是Unix/Linux命名习惯。我自己采用的是基于Unix的变种,融合了匈牙利命名法的一些优点,在这里分享给大家。

变量名的组成:(模块名) +  (作用域) + (类型前缀) + 变量名 + (变量名后缀),解释如下:

  • 变量名 以小写的英文字母构成,词与词之间用下划线连接,如key_value, data_src; 不可使用数字,不混用大小写;
  • 模块名 声明该变量属于的模块,防止模块与模块的命名冲突。如timer_prescalar_value, DMA_channel_name等;
  • 作用域前缀 (Scope Prefix) 标注变量的作用域,提高代码可读性:
    g_: 全局变量;
    n_: 局部变量;
    t_: 中间变量;
    s_: static静态变量;
  • 类型前缀 (Type Prefix) 指明变量的数据类型:
    ptr_: 指针变量,在程序中临时需要使用指针时,也常简写为p_,如*p_src;
    h_: 句柄,如h_file;
    n_: 整形,s_: 短整形,l_: 长整形, u_: 无符号整型,可增加数据位数,如u32;
    ch_: 字符型变量;
    f_: 浮点,d_: 双精度浮点;
    b_: boolean;
    by_: byte字节型(关注数据的位特性,需要位操作的情况下使用);
    reg_: 表示寄存器;
  • 后缀 (Suffix) 指明变量的性质:
    _src: 源,_dst: 目的;
    _str: 字符串;
    _t: 在声明数据类型时使用,表示为自定义的数据类型,如u32_t;
    _st: 表示为结构体;
    _buff: 数据缓冲, msg_buff;
    _arr, _a, _m: 数组或矩阵;

变量名的取名规则:

  • 循环控制变量 i, j, k, m, n,除循环控制外应避免使用这些变量名称;
  • 函数名 使用(模块名 + )动词 + 名词的形式,同样小写 + 下划线:sys_find_file(), IO_get_data(). 后者因为IO为专用名词故破例使用大写;
  • 类名或结构体名 使用首字母大写加下划线连接:如Mystring, Datetime_type;
  • 私有类成员 Private使用下划线_前缀,如_data_src_ptr, _init_module();
  • 宏定义或常量 使用全部大写:如MAX_NUMBER, LOOP_NUMBER;
  • 缩写 使用能广泛接受的缩写:如add, ans, avg, chk, cnt, col, ctrl, def, del, dst, disp, err, freq, idx, init, len, min, max, mid, msg, num, opt, pos, ptr, recv, res, ret, src, str, sub, num, ts (timestamp), val等。

本网站的所有实例代码和项目程序都将按此命名规范进行编写。

近日关注的几个KickStarter项目

KickStarter是国外最著名的众筹网站。项目发起者可以在只有基本idea的情况下提前发布产品信息,以获得来自个人的资金支持,达到满意的标准后再进行产品的实际生产,从而减少了产品发售的风险。 这几天在KS上比较热门的科技项目都是智能设备/可穿戴设备,这里我聊一聊几个我最近关注的项目。

1. Sweep激光雷达

项目主页:https://www.kickstarter.com/projects/scanse/sweep-scanning-lidar/description

sweep
▲ 图1. Sweep低成本激光雷达

激光雷达是机器人常用的传感器外设,用于快速扫描、感知环境存在中的障碍物。激光雷达通过激光旋转扫射,再测量激光返回时间的方式,对周围障碍物的距离进行快速估算。传感器输出的点云数据可以进行三维建模,最终辅助机器人进行导航。目前常用的激光传感器品牌为 HOKUYO,售价在几万至几十万元。传统的激光雷达使用光学振镜进行激光扫描,通过计算激光束相位差进行距离计算。而低成本的激光雷达通过电机控制激光的发射角度,使用视觉原理进行距离估算。在牺牲了扫描速度的情况下,大幅度降低了成本。关于低成本激光雷达的设计原理,可以参照CSK兄的博文:自制低成本3D激光扫描测距仪(3D激光雷达)

sweep-demo
▲ 图2. Sweep工作在四轴飞行器上(慢速摄影)

以下是由IEEE Spectrum网站整理的Sweep与其他常见激光雷达的参数对比。Sweep的价格只有专业激光雷达的1/5,但刷新速度(Scan Rate)只有专业传感器的1/4。其检测距离精度为1cm,精度为1 – 2%,可以满足一般的机器人地图构建与导航应用。图表中的 robopeak 现改名为 SLAMTECRPLIDAR 也在最近推出了新款 RPLIAR A2,在性能上相比一代做了很大提升 (10Hz刷新率),价格和Sweep也很相似。目前Sweep的众筹已经结束,最终筹款$272,990,共有1,010个支持者。官网上可以预购,价格为$255,折合人民币1680元。

sweep-spec
▲ 图3. Sweep与其他激光雷达的参数对比

 

2. Pebble 2智能手表

项目主页:https://www.kickstarter.com/projects/597507018/pebble-2-time-2-and-core-an-entirely-new-3g-ultra

pebble_cover
▲ 图4. Pebble智能手表第二代

大名鼎鼎的智能手表先祖Pebble再次回到KickStarter。这次他们带来的是最新的两款手表产品:Pebble 2和Pebble Time 2。前者是廉价型,后者为Premium高级版本。这次新版本的Pebble相比上一代的主要改变有:

1、增加了心率传感器。现在心率传感器已经是智能手表的标配了,Pebble自然要与时俱进;
2、增加了麦克风外设,支持语音信息回复,应该还可以通过Google Voice (Android) 和Siri (iOS) 进行语音控制);
3、核心处理器从Cortex M3升级为M4,有更大的信号处理能力;
4、在增加了额外的传感器之后,防水能力从50m下降到30m,但是依然足够日常使用;
5、Pebble Time 2实际上是上一代的Pebble Time Steel,Pebble Steel这个型号可能不会再推出。

pebble
▲ 图5. Pebble 2和Pebble Time 2

pebble2_colors
▲ 图6. Pebble 2有五种可选颜色

新产品的两个版本Pebble 2和Pebble Time 2两者的主要区别有:

1、Pebble Time 2的屏幕尺寸更大 ,同时使用的是彩色e-ink屏,而Pebble 2是黑白灰度e-ink屏 (e-ink即Kindle所使用的电纸屏,功耗极低);
2、外观和材质上,Pebble Time 2更佳,Pebble 2则看上去比较廉价;
3、当然,Pebble Time 2的价格比Pebble 2多70%。

Read more »

如何做一个好的博客

我从2010年开始写博客。在过去的五年多时间里,除了自然而然增长的博文数量外,我的博文质量也在潜移默化的提高。然而这种质量的提高并不完全是自然形成的,这是需要不断思考博客的核心价值,同时在不断迭代的设计中艰难地产生的。每个人对自己的博客有一个定位,也有自己对于好博客的一套衡量标准。我从我自己以往产生的经验,来谈谈我眼中的好博客应该是什么样子的。

首先,一个好的博客需要是原创的,至少是半原创的。没有人喜欢整个网站全是复制-粘贴来的博文,这不光来自复制过程中产生的版式错误,而是这种复制的方式的本质是欠思考的、不系统的。行为上的懒惰必然导致思维上的懒惰,这样得来的文章只会显得杂乱无章,而不能体现任何博主自身的意图。可惜的是,这样的博客占了很大一部分。我想很多人也许并无意将自己的博客给别人看,而只是作为自己知识的索引。然而即使是这样,系统的整理也是必须的。这是一个吸收与理解知识的最好机会,有时甚至比文章自身带来的价值更大。

其次,是核心主题。所谓主题,就是整个博客中的博文是耦合或者是弱耦合的。制造主题的方式有很多,其中之一就是采用博文系列或专题的方式。比如云飞实验室以机器人和嵌入式为主题,也开发了热门的树莓派、Arduino和STM32等专题。正如写文章一样,博文的内容大多是围绕一个主题的。如果偏题太多,同样就会失去博客自己的特色。记住,你的博客是一个博客,而不是一个分享一切信息的网站。想要很好的做到这一点,你需要有自己假象的用户。比如对我自己而言,我的假象用户是嵌入式工程师,机器人爱好者,Makers。我每考虑创作写一篇博文,都会首先站在读者的角度去考虑。我会去判断这篇博文对读者的价值是多少?他们是否感兴趣?能不能帮助他们/引发更深层的扩展和思考?在这个过程中会有很多取舍。可能会遇到你很想发布,然而偏离主题,远离读者的博文。这是需要避免的。

再者,我就觉得是博客设计。我所说的设计,不仅包括网站的CSS版式,也包含了文章内部的组织。总体上来说,就是要做到简洁。简洁就是去除不必要的元素,避免华而不实的特效、动画、插件等。这些元素有时会破坏博客整体的平衡性,也会让读者分散精力。简洁可以给读者创造出纯净的阅读体验。而美就会略为主观了。我所理解的美,包括文章的段落结构,图片的尺寸、配色、位置,示例代码的优美性。以我的经验来看,美是需要用心才能做到的。需要让自己变得沉静,敏感,细腻。美的产生与其说是与生俱来,不如说是精雕细琢的产物。对美的感受也有进化的过程(但我觉得总体上是向正方向前进的)。很多时候,我会去看自己很久之前的博文,如果觉得做的还不够好,会再仔细修改。我也经常从别人的网站和博客中吸取经验,学习如何更好的排版、安插图片和代码等。这些需要一个人的审美层次,然而更多时候是花费时间和精力去雕琢而成的。

这些是我多年来一直在思考和遵循的原则,今日突然想与诸位分享之,便有此文。

戴晓天
2016年04月21日 于 英国约克

【RPi树莓派使用指南】树莓派3代介绍及历代树莓派比较

树莓派自从12年02月最初发布之后,目前已突破800万的总销量。作为树莓派的早期支持者,云飞实验室也一直在关注着它的发展。如今在经历了4年的设计迭代之后,树莓派于16年02月推出了最新一代的树莓派3。树莓派3的本次发布与之前的2代只相差了整整一年时间,但是得益于目前芯片行业的快速发展,树莓派3的性能将会有很大的提升。这主要表现在以下几点:

  • 更高的处理速度。树莓派3首次采用了64位处理器:基于Cortex-A53的博通BCM2837。BCM2837为四处理器核心,主频也由树莓派2的900MHz提高到了1.2GHz。根据官方提供的数据,这将使树莓派3的处理速度较2代提高50%。如果和1代的700MHz单核相比,提升大约在3 – 4倍。更高的CPU速度使得树莓派可以胜任更大负荷的运算工作:如科学计算,机器人路径规划等。
  • 更高的互联性。树莓派3使用了集成蓝牙4.0和WiFi的设计。集成通信的设计的意义是多方面的。首先,使用者无需再购买额外的USB设备,从一定程度上来说,鼓励了用户在自己的设计中使用这些通信功能;其次,集成的通信模块可以进行更好的功耗管理,同时IO吞吐的性能也会得到提高;最后,可以更进一步的优化内核,只针对板载的芯片专门进行优化。避免可能出现的兼容性或者未优化的驱动导致通信性能下降问题。

rasp-pi-3-board
图1. 树莓派3外观

rasp-3_Model_B
图2. 树莓派3外观 (设计渲染图) 

从这两点来看,树莓派3代将很有可能再次扩展自己的使用领域,同时在物联网和机器人中得到应用。同类产品如果还是单从硬件角度进行提升,已经无法再与之竞争。因为性能的显著提升且维持原价,在树莓派2代出来时没有入手的玩家,这一次也难免蠢蠢欲动。同时因其性价比的纯粹提升,树莓派1代与2代的销量会大幅下降。因为2代只发布了1年,所以很可能成为绝版,如果出于收藏的目的可以尽早买之。总体上来说,这次树莓派3已经具备了IoT所需要的基本条件(性价比,互联性,体积与功耗)。今后的方向可能是增加更多的IO功能,如PWM和ADC等,是被大多数玩家需要却还没有被支持的。

以下是我对历代树莓派版本的整理与对比:

表1. 发布时间及主要特点

型号 发布时间 主要特点
PI 1 Model B 2012年02月 第一代树莓派。Model A不含以太网。
PI Compute Module 2014年04月 模块化设计,使用SODIMM大小的金手指接口。
PI 1 Model B+ 2014年07月 增加了2个USB接口,增加了9个GPIO:26脚->40脚。
使用MicroSD卡。
PI 2 Model B 2015年02月 升级处理器:四核900MHz Cortex-A9。升级为1GB RAM。
PI Zero 2015年11月 无网络通信功能,廉价,小尺寸。
PI 3 2016年02月 升级处理器:64bit四核1.2GHz Cortex-A53。
内置蓝牙4.0和WiFi。

 

表2. 树莓派历代版本硬件比较

型号 处理器 主频 内存 GPIO 互联性 功耗级别
PI 1 Model B BCM2835
(ARM11)
700MHz 512MB 26 2 USB
HDMI
10/100M Ethernet
700mA (3.5W)
PI Compute Module BCM2835
(ARM11)
700MHz 512MB 0 无。需要配合扩展板使用。 200mA (1W)
PI 1 Model B+ BCM2835
(ARM11)
700MHz 512MB 40 4 USB
HDMI
10/100M Ethernet
600mA (3W)
PI 2 Model B BCM2836
(Cortex-A9 四核)
900 MHz 1GB 40 4 USB
HDMI
10/100M Ethernet
800mA (4W)
PI Zero BCM2835
(ARM11)
1 GHz 512MB 40 (无排针) 1 Micro-USB
Mini HDMI
无板载网卡
160mA (0.8W)
PI 3 BCM2837
(Cortex-A53 64位四核)
1.2 GHz 1GB 40 4 USB
HDMI
Bluetooth 4.1
WiFi 802.11n
10/100M Ethernet
800mA (4W)

 

Read more »

机器学习 | 机器学习入门知识

最近正尝试用机器学习的方法解决线性回归和趋势预测问题,这里将自己对机器学习的初步理解整理至此。

机器学习 (Machine Learning) 研究的主题是如何让计算机具备与人类同等的思考和分析能力。机器学习主要基于认知学、计算机科学,统计概率学以及信息决策学。典型的机器学习应用包括照片分类、垃圾邮件识别、自然语言处理等。最近很火热的围棋人工智能AlphaGo就是采用了深度神经网络对大量棋局进行学习,从而具备了顶尖围棋选手的水平。

机器学习的应用领域有:
– 经济学模型建立
– 图像处理和机器视觉
– 生物DNA解码
– 能源负载、使用、价格预测
– 汽车、航空和制造
– 自然语言处理
– … …

Machine Learning从其采用的学习方式来说有以下三大类:
– 监督学习 (Supervised Learning):用于训练的数据包含已知结果(回归与分类问题)。
– 无监督学习 (Unsupervised Learning):用于训练的数据不包含已知结果(聚类问题)。
– 强化学习 (Reinforcement Learning):用于训练的数据不包含已知结果,但是可以用Award函数对其进行评价。

ml_category
▲ 图. 机器学习的分类(图中没有强化学习,一般强化学习会被认为是semi-supervised)[1]

监督学习用于数据中已包含已知标签。言下之意就是用于训练的数据已经具备了对应的输出。比如有一份得癌症与否和肿瘤块大小的对应数据,对单一肿瘤块大小数据而言,其对应的是否患癌症是已知的;再比如需要训练一个神经网络,学习判断一个图片中的主体是猫还是狗。那么用于训练的图片集中,每一张图片都会有已知对应的’这张图片是猫’或’这张图片是狗’的标签。通过对已知数据的学习与理解,从而在新数据出现时具有一定的预测能力。

而非监督学习则是对样本并没有既成标签,而是要通过模式搜索的方式对相似的一些元素进行聚类。典型的聚类问题有:基因序列分析,市场调研,物体识别等。

现在常见的机器学习算法有:

MachineLearningAlgorithms

▲ 图. 常见Machine Learning算法的思维导图,点击放大 (Picture from http://machinelearningmastery.com/)

除了主流的聚类,回归和贝叶斯之外,采用神经网络的Deep Learning深度学习是现在最热门的话题。机器学习的方法很多,在选择使用什么方法时,首先必须要确定自己所面对的是哪一种机器学习问题。在确定了分类后,可以根据下图选择具体方法:


ml_methods
▲ 图. Machine Learning算法的选择 [1]

Reference

[1] Introducing Machine Learning, Mathworks

【C语言深入】C语言的函数指针

函数指针是C语言指针中的一个分支:函数指针是指向函数地址的指针。和一般的指针一样,函数指针可以大大增强编程时的灵活性。这篇博文根据我的理解,简单介绍了自己对于函数指针的理解。

一、函数名的本质

在介绍函数指针之前,我们先来理解一下究竟什么是函数。以下一段代码定义了名为fun1(),接受int并返回int参数的函数:

int fun1(int x) {	// fun1是函数名
	return x + 1;	// 函数体
}

从一般的理解角度来看,fun1是函数的函数名。之所以要定义函数名,是因为在程序的其他位置要调用该函数时,可以直接使用这个别名。这种使用别名的方法和变量相似,但又不完全一样。对一个变量x而言,其意义如下:

物理地址 数值  说明
0x20000000        30 <– x是该数值单元的别名

x是内存单元0x20000000所对应的内存单元。此处x = 30,而x的地址&x = 0x20000000.

那么对一个函数而言,函数名又代表什么呢?假设之前的fun1函数在编译后被放置在0x08000200地址,则fun1对应的内存结构如下所示:

物理地址 数值 说明
fun1 (0x08000200) fun1_entry <– fun1是函数的入口地址

在这里,函数名代表一个记录了函数入口地址的存储单元的物理地址。可见,函数名的本质是地址。在编译阶段,函数名被转化成为对应的地址。在使用 xxxx() 函数调用的语法时,该地址被载入程序计数器PC,函数参数及当前现场被弹入堆栈。最后进行函数的实际跳转和执行。

二、函数指针是什么

对于一般变量而言,指针可以指向变量的地址,并修改变量的内容:

物理地址 数值  说明
0x20000000         30 <– x是整形变量
0x20000004  0x20000000 <– p是指向x的指针

这里p是指向x的指针,(即p = &x)。此时p的内存单元所存储的是x单元的物理地址,通过 *p 解析地址之后就可以访问或修改x单元的内容。同样的,函数也有其对应的指针 – 函数指针。函数指针是一种特殊的指针,其指向的对象不是变量而是函数。函数指针指向目标函数的入口地址(首地址)。这里我们定义一个指向fun1函数的函数指针pf:

int fun1(int);		// fun1是一个(含int输入参数和int返回参数的)函数
int (*pf)(int);		// pf是(指向返回int型,含int参数函数的)函数指针
pf = &fun1;

/* 也可以直接写成: */
int (*pf)(int) = &fun1;

/* 如果需要调用函数指针对应的函数,可以写为:*/
(*pf)();		// 等价于fun1()

到这里,我们回顾上一节中说到的函数名。其实函数名也可以理解成为const型的函数指针。所以在c语言中,以下调用也是合法的:

(*fun1)()		// 等价于fun1()
pf = fun1		// 等价于pf = &fun1;
/* 但是fun1的值不能被修改 */
fun1 = fun2		// 错误!fun1是const类型的指针

函数指针的声明较为冗长,如果需要定义多个同类型的函数指针。可以通过typedef定义一个函数指针类型,从而进行简化:

typedef int (*PF)(int); // 声明PF是一个函数指针类型
PF pf1 = fun1;
PF pf2 = fun2;

三、函数指针的应用场景

以上说明了如何定义函数指针,下面介绍函数指针的应用场景。函数指针最常见的应用还是作为回调函数的参数。一般在事件驱动的程序框架中,当对应事件发生时,需要触发对应的处理函数。以下代码实现了在初始化阶段,将事件与对应的处理函数关联(假设set_event_callback()是实现该功能的系统函数):

typedef int (*CALLBACK)(int);

#define EVENT_1		(0x01)
#define EVENT_2		(0x02)

int set_event_callback(const int e, CALLBACK);

void init_callbacks() {
	CALLBACK pf1, pf2;
	set_event_callback(EVENT_1, pf1);
	set_event_callback(EVENT_2, pf2);
}

在set_event_callback中,函数指针pf作为参数传递给函数使用。在基于事件编程的框架中(如一般的GUI库),函数指针经常以作为回调函数的方式出现。函数指针的另一个应用就是,根据当前程序进程的不同,要在不同条件下调用不同的处理函数:

void change_function(int nEvent, PF *ppf) {

	switch (nEvent) {
		case 0: 
			*ppf = fun1;
			break;
		case 1: 
			*ppf = fun2;
			break;
		default:
			*ppf = fun3;
	}
}

这里利用了函数指针指向的函数是可以改变的(而非函数名是const型的)。change_function() 将函数指针的地址(指向函数指针的指针)作为参数传入,并根据当前nEvent的情况更改原函数指针指向的函数。

函数指针的另一个应用场景,是允许程序在只知道函数物理地址(但不知道具体的函数名)的情况下进行函数跳转。相信大家都还记得这个经典的C语言面试题:

(*(void(*)(void))0)();
// 或
((void(*)(void))0)();

此语句的本质就是将0内存位置强制转换成为了函数指针,并调用了该函数指针指向的函数。以下代码解释了此处是如何将0地址转换成函数指针的:

typedef void (*PF)(void);

// (*(void(*)(void))0)() 等价于
(*(PF)0)()
// 或
((PF)0)()

函数指针的更高级用法就是组成函数指针数组,或和指向函数指针的指针配合使用。在此不再继续介绍,感兴趣的读者可自行研究。

参考资料

[1] Kenneth A.Reek, C和指针(第二版), 2008, 人民邮电出版社

[2] Brian W. Kernighan / Dennis M. Ritchie, The C Programming Language (Second Edition), 1989, Prentice Hall

[3] C语言中文网,C语言函数名与函数指针详解http://c.biancheng.net/cpp/html/496.html