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

  版权信息:
● 本博客使用CC 3.0协议,转载请保留该信息。
● 原文作者: 戴晓天 @ 云飞机器人实验室
● 原文地址: 【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

%d bloggers like this: