关于函数指针和类型
关于函数指针和类型
C++语言本质上由两部分组成:数据和代码。编程语言主要关注于操作数据,它可以对数据进行最精细的调整,但一般并不关心生成什么样的代码。事实上代码的具体形式是由编译器控制的,编程语言只要求完成相应的功能。
数据分数据类型和数据实例,前者是数据的格式,后者是数据在内存中的实体。当我们定义了一个数据类型时,同时也自动生成了一个相应的指针类型,所以没有必要再显式的声明一个指针类型。而对于函数类型则并非如此。首先函数类型的数据类型的一个显著不同是:同一数据类型不同实例可以有不同的数据值,每一个实例的大小是确定的;而同一函数类型的不同实例是不同函数代码,它的大小是不确定的,甚至,C++并不关心函数的大小,编译器自动生成具体的代码。我们可以一下子声明一系列同一类型的数据(比如数组声明),然后再对实例进行操作。但我们无法一下子声明一系列的函数实例,必须对每一个函数实例一一定义。
C++的指针类型大小是固定的。指针就象实例对象的名字,每一个实例都有一个唯一的名字,这个名字其实就是实例在内存中的位置。因为对计算机而言,内存大小是有限制的,从而可以生成的实例数量也是有限制的,那么它们的名字是可以用一个固定长度的整数来表示的(这个值不会大于内存的最大值)。函数指针也不例外,它也是函数在内存中的位置,具有相同的内存长度。一般来说,我们调用的函数名就是这个函数实例的指针。我们可以声明一个函数的指针类型,它是由函数的参数和返回值构成的,也就是说,只要函数的参数和返回值相同,就可以看作相同类型的函数(这里先不考虑调用方式的区别)。
如果函数指针是函数的指针类型的实例,那么什么是函数的类型呢。本质上C++没有函数类型的概念,我们常说的函数类型是指函数的指针类型。但是一方面为了和数据类型保持一致,更重要的是实际的需要,我们确实应该定义函数的类型。
在提及函数类型的一个应用之前,我们不得不讨论一下类的成员函数。类(class或struct)本质上是一个数据类型,它的实例是一系列特定格式的数据。而它的成员函数并不纳入类的大小尺度的计算。因为不管是静态函数还是成员函数,它们都在内存中有固定的位置,这个位置和类的实例没有任何的关系。类的成员函数和普通的函数也没有任何的不同。当然,这是对编译器而言,对程序员来说,并非如此。C++语言中调用一个类的成员函数,意味着这个函数可以访问这个实例的成员变量,而且不用显式的在参数中传递这个实例的指针。但实际上,对编译器而言,调用一个类的成员函数,就是调用一个普通的函数,只不过多传递了一个实例指针。正如a.Func()和Func(&a)这两种形式,只不过书写形式不同而已(Func是一个普通函数,但比a.Func多一个参数)。换言之,即使C++没有成员函数的概念,我们几乎可以编出同样功能的程序,我们可以把一个类的成员函数以类名为前缀(容易识别),声明为一系列的普通函数,这些函数的第一个参数都是一个对应类的指针类型。当我们需要调用类的成员函数时,只要调用特定的函数,并且传递实例的指针作为第一个参数。
当然C++类的成员函数有它的方便之处,这就是我们使用它的原因。而且C++有类的成员函数指针的概念,但就C++本身来说,简直是多此一举。下面是一个类的成员函数的类型声明:
typedef int (A::FUNC)(int);
FUNC是A类的一个成员函数的指针类型,那么什么函数的指针可以是这个类型呢?首先它必须是A的成员函数(这正是问题的所在),然后它必须是一个有一个int类型参数且返回int类型的函数。假设Func是一个满足此条件的函数,
FUNC func = A::Func;
在VC 8.0中要求用以下格式:
FUNC func = &A::Func;
如何调用func呢?首先既然是类的成员函数,一定要指明是哪个实例在调用它,
A a;
A* pa = &a;
int i = (pa->*func)(3);
也就是说必须有类A的一个实例指针pa,但是,我们既然有pa,为什么不用(int i = pa->Func(3))这种形式呢?
如果仅限于此,我想类的成员指针是没有多大用处的。当然,C++允许对成员函数指针进行转换,这种转换必须是在成员函数指针之间,不能转换为普通函数的指针。如果我们使用类的成员函数指针,大多是因为调用者并不知道这个指针是哪个类型的,但是知道它的参数和返回值,并且有一个实例的指针。事实上,对于函数调用来说这就足够了。但是C++不这么认为,它一定要知道那个实例指针的类型,这是有原因的,指针的类型直接控制成员变量的访问方式。成员函数在执行时,可能会访问实例的数据成员,而这种访问是由实例指针传递的,如果传递了一个不正确的指针,就可能造成内存访问错误。
模拟Windows回调函数的机制,假设一个类Control需要把事件通知外部的一个A类。而且这个类(Control)是封装好了的,它要通知的类的类型是不确定的。我们不能在实际使用时,再回头更改代码。一种解决方案是声明一个通用类Common,它没有数据成员,甚至不必实例化,但是它有特定的成员函数正好符合要通知的信息的格式,但这些成员函数没有必要有代码,总之一切只是为了应对C++的类型检查,我们只是使用这个类的类型,不使用它的任何实际的东西。在封装类Control的内部声明一个Common指针pc,再声明一个Common的成员函数指针cf。这样,只要把要接收信息的类的指针传递给pc,再把相应格式的成员函数指针传递给cf,Control类只要调用(pc->*cf)(param),就可以把信息传递给任何外部的类。对于静态函数,可以不必给pc赋值(或赋任何一个值),但C++不允许把一个静态函数的指针传递给cf,但是可以用一些技巧性的转换。实际情况比这要复杂,还要考虑虚函数,虚继承等。
这个解决方案还有一个重要的问题,就是如果pc和cf传递了错误的值,后果会非常严重。这有两方面:cf并不是pc的成员,cf不是一个对应类型的函数。以下是这个问题的一个解决方案:
template<typename R,typename T> void SetFuncPointer(R* pr,T func)
{
typedef void (R::*FUNC)(TYPE1,TYPE2);
FUNC Func = func;
………….
}
当func不是FUNC类型,或不是R类型(这个类型由pr参数确定)的成员函数时,这个函数一定会编译报错。而且省去了类型转换的麻烦。
但是在FUNC定义的那一行,对于不同函数,是不同的。我们希望有一个更通用的定义方式,把这个定义抽象化,公式化,而无须每一种函数都不得不写下定义式。比如以下的形式:
typedef R::*FUNCTYPE FT;(C++中并没有这个语法)
其中FUNCTYPE是代表函数参数和返回类型的标识,FT是与之对应的类的成员函数指针定义(格式相同的函数,但是是成员函数)。
事实上,是否定义函数的类型概念并不是根本的,对函数而言,指针类型完全可以代表函数的类型。数据需要数据类型和指针类型是因为我们需要定义相应类型的实例,而对函数来说,它的实例定义本身也就定义了一种隐含的类型(并不需要先有类型才可以定义)。这一方面省去了定义类型的麻烦,但在使用它的类型时,又不得不翻回头来重新定义。所以为了统一和方便,还是加入函数类型概念比较好,以下是一个示例语法:
typedef FUNC (void,int);//第一个参数表示返回类型。
FUNC就是定义的函数类型,它是一个返回void类型,有一个int类型参数的函数。关于函数的返回类型,事实上并不规范。函数的返回值,本质上是一个out参数,也就是一个无须初始化,无须定义的参数。它与其它参数没有本质区别,它不需声明,是因为编译器只是把这个值存在eax寄存器中。函数没有返回值机制,仍然会工作的很好。不过鉴于函数的返回值是数学和其它领域的普遍规范,还是保留这种形式。
允许函数的类型定义自然有一个问题。如果两个函数类型定义参数相同,那么它们是否是一种类型呢?现在的C++对于函数类型指针的规定是可以自由的赋值无须显示转换。这一点和数据类型是不一致的,对于数据结构完全相同的类,事实上是可以通用的,尽管这时,它们的成员的名称可能不同。出现这种规定是自然而然的,但不是必须的。至少它们是可以直接相互赋值的。