函数指针
定义方式
typedef 返回值类型(* 新类型名称)(参数列表)
typedef char (*PTRFUN)(int); PTRFUN pFun; char glFun(int a){ return;} void main() { pFun = glFun; (*pFun)(2); }
调用方式:
- 直接把函数指针变量当作函数名,然后填入参数
- 将函数指针的反引用作为函数名,然后填入参数
上面的PTRFUN也可直接进行以下调用:
PTRFUN(2);
函数调用方式
函数名就是函数的地址,是一个编译时的常量,“函数连接”的本质就是把函数地址绑定到函数的调用语句上。
静态连接:一般的函数调用语句可以在编译的时候就完成这个绑定,这就是静态连接。
运行时连接&动态连接:
函数指针可以理解为一个新的类型,由其定义的变量,与其他变量一样,在编译期间是没有值的,因此函数指针与函数体的绑定关系只有到了运行时才确定。
对于函数指针数组,此特征表现的尤为明显,见《高质量程序设计指南》P135
class CTest { public: void f(void){cout<<"CTest::f()"<<endl;} //普通成员函数 static void g(void) {cout<<"CTest::g()"<<endl;} //静态成员函数 virtual void h(void) {cout<<"CTest::h()"<<endl;} //虚拟成员函数 private: //.......... }; void main() { typedef void (*GFPtr)(void); //定义一个全局函数指针类型 GFPtr fp = CTest::g; //取静态成员函数地址的方法和取一个全局函数的地址相似 fp(); //通过函数指针调用类静态成员函数 typedef void (CTest::*MemFuncPtr)(void)//声明类成员函数指针类型 MemFuncPtr mfp_1 = &CTest::f; //声明成员函数指针变量并初始化 MemFuncPtr mfp_2 = &CTest::h; //注意获取成员函数地址的方法 CTest theObj; (theObj.*mfp_1)(); //使用对象和成员函数指针调用成员函数 (theObj.*mfp_2)(); CTest* pTest = &theObj; (pTest->*mfp_1)(); //使用对象指针和成员函数指针调用成员函数 (pTest->*mfp_2)(); }
vtable与vptr类型问题与初始化问题
- 类型问题
vptr:是C++对象的隐含数据成员。多态类的每一个对象中安插一个vptr,其类型为指向函数指针的指针,它总是指向所属类的vtable,也就是说:vptr当前所在的对象是什么类型的, 那么它就指向这个类型的vtable.
vtable: 就是一个函数指针的数组.按道理说只能在同一数组中存放统一类型的数据。可是一个class中可能有各式各样的的虚函数,它们的原型都可能不一样,因此也不可能是一种函数指针类型,不同class的虚函数就更不可能了。
那么虚函数表到底怎么定义呢?参考一下:MFC 消息映射、分派和传递
按照MFC的思路,我们设想C++编译器构建vtable的方法是这样的:
1、定义如下通用的虚函数指针类型(实际上是经过Name-Mangling处理后对应的全局函数指针类型)和vtable类型。
typedef void(__cdecl* PVFN)(void); //通用的虚函数指针类型 typedef struct { type_info * _pTypeInfo; PVFN _arrayOfPvfn[]; //虚函数个数由初始化语句确定 } VTABLE;
2、在每一个继承分支中的第一个多态类中插入vptr,而在每一个多态类中都插入vtable的声明,如下:
class Shape { PVFN *_vptr; static VTABLE _vtable; public: Shape() :m_color(0){} virtual ~Shape(){} float GetColor() const{ return m_color; } void SetColor(float color){ m_color = color; } virtual void Draw() = 0; private: float m_color;//颜色 }; class Rectangle :public Shape { static VTABLE _vtable;//在实现文件中初始化 public: Rectangle() :m_length(1), m_width(1){...} virtual ~Rectangle(){...} //.... virtual void Draw(){...} static unsigned int GetCount(){ return m_count; } protected: Rectangle(const Rectangle& copy){...} Rectangle& operator=(const Rectangle& assign){...} private: float m_length;//长 float m_width;// 宽 static unsigned int m_count;//对象计数,在实现文件中初始化 };
编译器编译完每一个class,都会进行如下工作:
1、把class中所有虚函数的指针(实际就是被转换为全局函数后的地址)都强制转换为PVFN类型,并用它们初始化_vtable中的_arrayOfPvfn[],就像ON_WM_XXX所做的那样
2、_vptr则将被初始化为指向_vtable对象或者_vtable._arrayOfPvfn[0],具体指向哪里取决于编译器的实现.
3、vptr的初始化和改写在class的各个构造函数和析构函数中完成.
但是MFC中是枚举了所有的可能的消息处理函数处理类型(AfxSig_xxx),C++编译器显然不可能如此,而是在遇到通过指针或者引用调用虚函数的语句时:
1、首先根据指针或引用的静态类型来判断所调用的虚函数是否属于该class或者它的某个public基类
2、然后进行静态类型检查,例如:
Shape * pShape = new Rectangle; //pShape的静态类型是 Shape* pShape->Draw(); // 根据Shape::Draw()执行类型检查 delete pShape; //根据 Shape::~Shape()执行类型检查
3、改写虚函数调用语句,怎么改?
(*(p->_vptr[slotnum]))(p, arg-list);//指针当作数组来用,最后改写为指针运算
其中,p是基类型指针,vptr是p指向的对象的隐含指针,而slotnum就是调用虚函数在vtable中的编号,这个数组元素的索引号在编译时就确定了下来,并且不会随着派生层次的增加而改变。arg-list是参数列表。
对应上面Shape则改写为下面的样子:
(*(pShape->_vptr[2]))(pShape); //pShape->Draw(); (*(pShape->_vptr[1]))(pShape); //delete pShape;
扩展:
以上也就是动态绑定技术的一个实现,由于在运行时才进行的函数寻址,以及各个虚函数的参数列表都不尽相同,编译时根本无法对一个具体的虚函数调用执行静态的参数类型检查。
那么C++编译器是如何对虚函数调用语句的参数类型进行检查的呢?
派生类定义中的名字(对象或函数名)将义无反顾地遮蔽(即隐藏)掉基类中任何同名的对象或函数。
隐藏:如果派生类定义了一个与其基类的虚函数同名的虚函数,但是参数列表有所不同,那么这就不会被编译器认为是对基类虚函数的改写(Overrides),而是隐藏,所以不可能发生运行时绑定。
协变:要想达到运行时绑定,派生类和基类中同名的虚函数必须具有相同的原型,也即相同的Signature(参数列表),返回值类型可以不同,此为协变。
4、函数指针类型转换:语句pShape->_vptr[n]从vtable中取出来的函数指针类型都是通用类型PVFN,与实际调用的虚函数的类型一般是不匹配的(编译器知道我们定义的每一个虚函数的类型),所以还应该有一个反向类型强制转换的过程。
以下仅作示意,待找到MFC的反向转换过程,以MFC的代码进行说明。
typedef void(__cdecl* PVFN_Draw)(void); typedef void(__cdecl* PVFN_~Shape)(void); (*(PVFN_Draw)(pShape->_vptr[2]))(pShape); //pShape->Draw(); (*(PVFN_Draw)(pShape->_vptr[1]))(pShape); //delete pShape;
以上的过程中并没有派生类改写的虚函数Rectangle::Draw()和Rectangle::~Rectangle()参与,那么怎么会调用到这两个改写的虚函数呢?奥妙就在vtable的构造及pShape当前实际执行的对象。
虽然pShape的静态类型是Shape*,但在运行时却指向一个Rectangle对象,而该对象的vptr成员指向Rectangle::_vtable,而不是Shape::_vtable;这个vtable中存放的也都是Rectangle改写过的虚函数或者新增的虚函数的地址,除非有的虚函数Rectangle没有改写。
vtable中虚函数指针的排列顺序,具体规则详见《高质量程序设计指南-C++/C语言》P222.
1、vptr在哪里被初始化?
由于vptr并非static成员,因此只能在构造函数中初始化---在哪个类的构造函数中就被初始化为指向哪个类的vtable。
2、它的值会不会改变?
会
3、为什么需要改变?
4、在哪里改变?
类的每一个构造函数中。
具体如下:
由于一个派生类对象的构造函数会从每一个继承分支的根类开始向下依次调用它的每一个基类的构造函数,所以除了在根类的构造函数中vptr是被初始化的外,在后来的基类构造函数中实际上都是在不断地被改写以指向当前构造函数对应的基类的vtable。编译器必须保证在类的每一个构造函数中(包括拷贝构造函数)都要重新初始化或改写vptr。
vptr必须随着对象类型的变化而不断的改变它的指向,以保证其值和当前对象的实际类型是一致的。
构造函数和析构函数中如何调用虚函数?
应尽量避免在构造函数和析构函数调用虚函数!!!
构造函数中
vptr肯定是第一个被初始化和改写的成员---在所有用户代码前执行。
如果在多态类的构造函数中调用了某个虚函数,不过这个虚函数是新增的还是来自于多态基类里或是改写自多态基类,因当程序执行到这个构造函数中的时候,可以肯定的是当前对象已经存在了,而且vptr已经被正确初始化了,只要保证这个虚函数用到的所有其他数据成员都在它被调用之前初始化好即可。
析构函数中
vptr则是在所有用户代码执行完之后被改写为指向其直接基类的vtable,或者说在所有用户代码执行之前被重新改写为指向当前类的vtable。这是必须的,因为一个对象在析构的过程中是沿着继承分支向上依次退化为各个基类对象---直至根类对象的,所以析构函数中的虚函数调用不应该绑定到当前类的某个派生类的该虚函数的改写版本上。
一句话就是:可以vptr指向当前类的vtable(所有用户代码之前),也可以指向直接基类(所有用户代码之后)的vtable,但不能指向派生类的vtable。
此处参考对象的构造和析构过程便可明白,如同网络消息的打包和解包过程,如图:
是否有必要在最终的二进制代码(LIB,DLL和EXE)中为一个抽象基类产生实际的vtable呢?
没有必要
因为抽象基类不可能实例化,也就不可能有指针或引用指向它的对象,也不可能有vptr指向它的vtable。