杨玉磊 · 更新于 2018-08-18 06:00:38

函数指针

函数指针就是函数的指针。它是一个指针,指向一个函数。(即函数在内存中的起始位置地址)
实际上,所有的函数名在表达式和初始化中,总是隐式地退化为指针。

例:
int r , (*fp)( ) , func( ) ;
fp= func ; //函数名退化为指针
r= (*fp)( ) ; //等价于r=fp( ) ;
无论fp是函数名还是函数指针,都能正确工作。因为函数总是通过指针进行调用的!

--

例:
int f(int) ; //函数声明
int (*fp)(int) = &f ;//此取地址符是可选的。编译器就把函数名当做函数的入口地址。
int ans ;
//以下三种方式可调用函数
ans= f(25) ;
ans= (*fp)(25) ;
ans= fp(25) ;
函数名就是一个函数指针常量,函数调用操作符(即一对括号)相当于解引用。
函数的执行过程:
函数名首先被转换为一个函数指针常量,该指针指定函数在内存中的位置。然后函数调用操作符调用该函数,执行开始于这个地址的代码。

强制类型转换

void fun() { printf("Call fun "); }  
int main(void)  
{  
    void(*p)( ) ;  
    *(int*)&p = (int)fun ;  
    (*p)() ;  
    return 0 ;  
}

指针的强制类型转换只不过是改变了编译器对二进制位的解释方法罢了。
*(int *)&p = (int)fun ;中的fun是一个函数地址,被强制转换为int数字。 左边的(int *)&p是把函数指针p转换为int型指针。
*(int *)&p = (int)fun ;表示将函数的入口地址赋值给指针变量p。
(*p)( ) ;表示对函数的调用。

函数指针的用途

1,转移表(转移表就是一个函数指针数组)
即可用来实现“菜单驱动系统”。系统提示用户从菜单中选择一个选项,每个选项由不同的函数提供服务。
【若每个选项包含许多操作,用switch操作,会使程序变得很长,可读性差。这时可用转移表的方式】

例:
void(*f[3])(int) = {function1, function2, function3} ; //定义一个转移表
(*f[choice])( ) ; //根据用户的选择来调用相应的函数

2,回调函数
(用函数指针做形参,用户根据自己的环境写个简单的函数模块,传给回调函数,这样回调函数就能在不同的环境下运行了,提高了模块的复用性)
【回调函数实现与环境无关的核心操作,而把与环境有关的简单操作留给用户完成,在实际运行时回调函数通过函数指针调用用户的函数,这样其就能适应多种用户需求】

例:C库函数中的快速排序函数
void qsort(void *base, int nelem, size_t width, int (*fcmp)(void*, void*) );//fcmp为函数指针。
这样,由用户实现fcmp的比较功能(用户可根据需要,写整型值的比较、浮点值的比较,字符串的比较 等)这样qsort函数就能适应各种不同的类型值的排序。

使用函数指针的好处在于:
可以将实现同一功能的多个模块统一起来标识,这样一来更容易后期维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦合度以及使接口与实现分开。

指针的复杂声明

函数指针数组的指针

例:char *(*(pf)[3])(char )

这个指针指向一个数组,这个数组里存储的都是指向函数的指针。它们指向的是一种返回值为字符指针,参数为字符指针的函数。

解读复杂声明的方式

从外到内,层层剥开,先找核心,再向右看
找到核心变量后,从右向左读。

  • 读作”指向…的指针”
    [] 读作”…的数组”
    () 读作”返回…的函数”

简单的例子:
int *f() ; // f: 返回指向int型的指针
步骤:
1)找标识符f:读作”f是…”
2)向右看,发现”()”读作”f是返回…的函数”
3)向右看没有什么,向左看,发现*,读作”f是返回指向…的指针的函数”
4)继续向左看,发现int,读作”f是返回指向int型的指针的函数”

int (pf)() ; // pf是一个指针——指向返回值为int型的函数
1)标识符pf,读作“pf是…”
2)向右看,发现),向左看,发现\
,读作 “pf是指向…的指针” 3)向右看,发现”()”,读作“pf是指向返回…的函数的指针” 4)向右看,没有,向左看发现int,读作”pf是指向返回int型的函数的指针

复杂指针的举例: void (*b[10]) (void (*)());

首先找到核心:b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是“void(*)()”【 这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”】 返回值是“void”。完毕!

使用typedef简化声明

“建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”。

举例:

1、
void (*b[10]) (void (*)());
typedef void (*pfv)(); //先把上式的后半部分用typedef换掉
typedef void (*pf_taking_pfv)(pfv); //再把前半部分用typedef换掉
pf_taking_pfv b[10]; //整个用typedef换掉

跟void (*b[10]) (void (*)());的效果一样!

2、
doube(*)() (*pa)[9];
typedef double(*PF)(); //先替换前半部分
typedef PF (*PA)[9]; //再替换后半部分
PA pa;

跟doube(*)() (*pa)[9];的效果一样!

指针的反思

1、我们为什么需要指针?
因为我们要访问一个对象,我们要改变一个对象。要访问一个对象,必须先知道它在哪,也就是它在内存中的地址。地址就是指针值。
所以我们有
函数指针:某块函数代码的起始位置(地址)
指针的指针:因为我要访问(或改变)某个变量,只是这个变量是指针罢了

2、为什么要有指针类型?
因为我们访问的对象一般占据多个字节,而代表它们的地址值只是其中最低字节的地址,我们要完整的访问对象,必须知道它们总共占据了多少字节。而指针类型即向我们提供这样的信息。
注意:一个指针变量向我们提供了三种信息:
①一个首字节的地址值(起始位置)
②这个指针的作用范围(步长)
③对这个范围中的数位的解释规则(解码规则)
【编译器就像一个以步数测量距离的盲人。故你要告诉它从哪开始走,走多少步,并且告诉他如何理解这里面的信息】

3、强制类型转换的真相?
学过汇编的人都知道,什么指针,什么char,int,double,什么数组指针,函数指针,指针的指针,在内存中都是一串二进制数罢了。只是我们赋予了这些二进制数不同的含义,给它们设定一些不同的解释规则,让它们代表不同的事物。
(比如1000 0000 0000 0001 是内存中某4个字节中的内容,如果我们认为它是int型,则按int型的规则解释它为-2^31+ 1;如果我们认为它是unsigned int ,则被解释为2^31+ 1;当然我们也可把它解释为一个地址值,数组的地址,函数的地址,指针的地址等)

如果我们使用汇编编程,我们必须根据上下文需要,用大脑记住这个值当前的代表含义,当程序中有很多这样的值时,我们必须分别记清它们当前代表的含义。这样极易导致误用,所以编译器出现了,让它来帮我们记住这些值当前表示的含义。
当我们想让某个值换一种解释的方案时,就用强制类型转换的方式来告诉编译器,编译器则修改解释它的规则,而内存中的二进制数位是不变的(涉及浮点型的强制转换除外,它们是舍掉一些位,保留一些位)。

上一篇: 数组与指针(二) 下一篇: 类型转换