您的位置:首页 > 编程语言 > C语言/C++

C++独孤九剑第三式——鱼跃于渊(多态机制实现)

2016-06-06 16:45 495 查看
鱼跃于渊,过而成龙,变幻万千。

我们都知道面向对象的三大特征:

1.封装 2.继承 3.多态

多态是建立在前面两个特征的基础之上的,可以算得上是面向对象的“终极应用”。从小处来看,多态拥有根据不同“环境”调用不同函数的能力;从大处来看,多态可以实现类似操作系统中调用回调函数的效果(cocos2d-x框架中就利用了C++多态的特性)。在本式中,将主要和大家一起探索函数多态机制的实现。

一般情况下,基类希望派生类直接继承的函数不会被定义为虚函数;而基类希望派生类重新定义的函数将被定义为虚函数(virtual)。对于非虚函数的调用,在编译期的时候就已经确定了;而虚函数的调用则需要在运行时动态决议(除了虚函数机制被压制的时候)。除了构造函数之外,任意的非static成员函数都可以是虚函数。在派生类中无法改变基类中已定义为虚函数的事实。

虚函数机制的三个要点:1.继承 2.定义虚函数 3.使用基类的指针或引用调用虚函数。

每一个有virtual function的class都会有一个virtual
table,内含class中有作用的virtual function的地址,然后每个类实例有一个vptr,指向本类中的virtual
table。

下面就从单继承、多继承、虚继承这三种情况入手,对虚函数机制的实现进行探究。

1. 单继承

例如我们有如下继承链代码:

class First
{
public:
int  first;

virtual  void  method1();
virtual  void  method2() = 0;
};

class Second:public First
{
public:
int  second;

virtual  void  method2();
};

class Third:public Second
{
public:
int  third;

virtual  void  method1();
virtual  void  method3();
};

则这几个类的virtual table内容如下所示:



说明:

1.虚函数在虚函数表中的位置是按其声明的顺序排列的。纯虚函数虽然没有实现地址,但是会在虚函数表中预留一个位置(如First::method2)。最后,虚函数表将以NULL(0)结束。

2.子类中如果重写了父类中的虚函数,则从父类中继承的虚函数表的相应函数实现地址项将被修改为子类虚函数的实现地址(如Second::method2)。

3.子类新增了虚函数,则会在从父类继承的虚函数表中增加一项新的虚函数实现地址(如Third最后一项)。

其实,在VC下,Third的虚函数表是紧跟在Second虚函数表后面。

简单的实现以下虚函数,如:

void First::method1()
{
cout<<"In First method1"<<endl;
}
void Second::method2()
{
cout<<"In Second method2"<<endl;
}
void Third::method3()
{
cout<<"In Third method3"<<endl;
}
void Third::method1()
{
cout<<"In Third method1"<<endl;
}

然后进行如下调用:

typedefvoid(*pFun)();

//main------------------------------------------------------

Second s;

s.first = 10;

Thired t;

cout<<((long)&(s.first) -(long)&s)<<endl;

pFun pf =(pFun)*((int*)*((int*)&s));//First::method1

pf();

pf = (pFun)*((int*)*((int*)&s) + 1);//Secon::method2

pf();

pf =(pFun)*((int*)*((int*)&t));//Third::method1

pf();

pf = (pFun)*((int*)*((int*)&t) + 1);//Second::method2

pf();

pf = (pFun)*((int*)*((int*)&t) +2);//Third::method3

pf();

First因为存在纯虚函数是一个抽象基类,所以无法实例化。经过查看上面的调用结果,也可以进一步证明上图虚表的正确性。

其中需要注意的是,虚函数其实是有this指针的,我们在前面并没有进行设置。如想自己手动传入参数,也可以像下面这样进行调用:

typedefvoid(*pFun)(Second *);

//main---------------------------------------------------

pFun pf = (pFun)*((int*)*((int*)&s));//First::method1

pf(&s);//传入this指针

上面的调用方式仅仅是为了证明前面虚函数表的正确性,正常情况下的调用可能是下面这样的:

ptr->method1();

此时ptr会根据其指向对象的真实类型去调用虚函数表中相应位置的实现代码。

即:(*ptr->vptr[0])(ptr) 其中0是method1实现地址在虚函数表的位置。

单继承的情况相对来说要简单点,那我们进入下一个环节吧(~ ̄▽ ̄)~

2. 多继承

我们先搭建一个多继承的框架如下:

class Base1
{
public:
int base1;
virtual void method1();
};
class Base2
{
public:
int base2;
virtual void method2();
};
class Successor:public Base1,public Base2
{
public:
int successor;
virtual void method1();
virtual void method3();
};

则其对应每个类的虚函数表中内容如下图示:



左边两个分别是Base1和Base2的虚函数表,右边的是Successor的对象内存布局和两个虚函数表指针指向的具体虚函数表。

注意:上图是VC6.0的实验结果,若是在高版本下情况会有所不同。在Base2的虚表处,例如VS2013就没有Successor相关的函数。这样的处理可以提高安全性,同时也能节约内存,毕竟我们不可能从Base2部分来动态调用Base2中不存在的虚函数。

VS2013下,Successor的对象内存布局和两个虚函数表指针指向的具体虚函数表如下图所示:



可用如下代码进行验证:

typedefvoid(*pFun)();

//main---------------------------------------------------------------------

//Base1的虚函数表

pFun pf =(pFun)*((int*)*((int*)&s));//Successor::method1

pf();

pf = (pFun)*((int*)*((int*)&s) +1);//Successor::method3

pf();

cout<<*((int*)*((int*)&s) +2)<<endl;//0;虚表结束

//Base2的虚函数表

pf = (pFun)*((int*)*((int*)&s +2));//Base2::method2

pf();

pf = (pFun)*((int*)*((int*)&s + 2) +1);//Successor::method1;此语句在高版本编译器可能会报执行期错误

pf();

pf = (pFun)*((int*)*((int*)&s + 2) +2);//Successor::method3;;此语句在高版本编译器可能会报执行期错误

pf();

cout<<*((int*)*((int*)&s + 2) +3)<<endl;//0;虚表结束,高版本编译器可能需要调整。

其中第一个是主表,后面的都是副表。上面的图示其实更侧重于讲清楚虚函数表的构造,在实际应用中的多态不会在最终子类新添加虚函数,毕竟这达不到多态的功效,而且还可能使执行的效率降低。

当有如下语句时:

Base1*pb1;

Base2*pb2;

Successors;

pb1= &s;//指向__vptr_Base1处

pb2= &s;//指向__vptr_Base2处

注意,当把s的地址赋给Base2的指针时,地址会发生偏移,使其指向Base2部分的开始处。既有:(long)pb2 - (long)pb1 = 8;

调用函数时的转换如下:

pb1->method1 ====> (*pb1->__vptr_Base1[0])(pb1);

pb2->method2 ====> (*pb2->__vptr_Base2[0])(pb2);

3. 虚继承

在进入虚继承环节的时候,我不由得深呼吸了一下,以缓解我澎湃的心情

<(* ̄▽ ̄*)/

我们有如下的代码构建虚继承的环境:

class VirtualBase
{
public:
int vbi;
virtual void same();
};
class Base1 : public virtual VirtualBase
{
public:
int base1;
virtual void same();
};
class Base2 : public virtual VirtualBase
{
public:
int base2;
virtual void same();
};
class Little : public Base1,public Base2
{
public:
int little;
virtual void same();
};

则有各类对象的内存如下:



上图中Little对象的内存情况可以用如下代码进行验证(另外两个读者可用类似的方法自行验证):

cout<<(long)&l.base1- (long)&l <<endl;//4

cout<<(long)&l.base2- (long)&l<<endl;//12

cout<<(long)&l.little- (long)&l<<endl;//16

cout<<(long)&l.vbi- (long)&l<<endl;//24

//关于虚基类指针的验证请参考第一式相关内容

虚基类没有虚函数的情况我已在第一式中详细介绍了对象的内存情况,此处不再重复。关于__vbptr_Base1和__vbptr_Base2指向的内容,也在第一式有介绍不再重复。这里关键就看虚函数指针__vptr_VirtualBase部分,及程序运行的时候如何调用相关的虚函数。

用如下语句可以调用虚函数表中的第一个虚函数(也是表中唯一的一个):

typedef void (*pFun)();

//main------------------------------------------------------------------

int **ptr = (int**)&l;

pf = (pFun)ptr[5][0];

pf();

下面我们来分析如何通过基类来调用Little对象的虚函数。例如

Base1 *pb1;

Base2 *pb2;

Little l;

pb1 = &l;//指向__vbptr_Base1处

pb2 = &l;//指向__vbptr_Base2处

pb1->same();// 1

pb2->same();//2

上面标号为1、2的语句调用类似,我就只阐述语句1的调用情况。

编译器处理的步骤基本如下:

1. 获得当前指针所指对象虚函数表中的相关偏移量

*((int *)*(int *)pb1 + 1);//从虚基类指针到虚基类的偏移字节数

*((int *)*(int *)pb1 );//从虚基类指针到当前所指对象开始地址的偏移量

2. 当前指针加上偏移量指向虚基类的虚函数表指针

(char *)pb1 + *((int *)*(int *)pb1 + 1) - *((int *)*(int *)pb1);//将该式设为X在第3步中使用。

3. 通过虚函数的序号调用对应的虚函数

pf = (pFun)*((int *)*(int *) X + N);
//其中N代表虚函数表中第N个函数(0开始)

pf();

函数的调用过程全部结束。

到此,第三式终于修炼成功(*^__^*)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: