在游戏中,很多时候我们要求不同的对象做相同的工作,例如CCLabelTTF和CCLabelTTFColor都继承自CCLabelProtocol,CCLabelProtocol就是一个接口类,只有setString()和getString()两个纯虚函数,派生类CCLabelTTF和CCLabelTTFColor对他们进行复写之后都可以完成自己的功能,当父类的指针指向子类时就可以调用子类的同名函数,这就是c++多态的体现,而真正的原理就是虚函数列表。
一、虚函数列表
首先我们先看两张图,感受一下实现的机制。
1、无复写虚函数:
存储原理:
2、有复写函数:
存储原理:
从上面的图片我们大致已经了解了虚函数列表工作的原理。子类本身类型指针指向自己时调用自己的函数应该没有什么问题,当父类指针指向子类对象时,调用的却是子类的同名函数,原因就是通过虚函数列表找到的是子类的函数入口地址,所以调用的就是子类的函数,这就是真正原因。
我们现在看一下什么是虚函数列表以及虚函数表指针。首先我们应该明确几点:1、虚函数列表中存放的是该类的虚函数的入口地址;2、虚函数指针指向该类的虚函数列表;3、虚函数表是类所拥有的或者说是该类实例化出来的所有对象所共有的,不是某一个对象独有的;4、虚函数表指针在每一个对象实例化时由编译器帮助自动加入到对象中,即如果有虚函数的话对象会拥有虚函数指针占有的空间。
好了,虚函数表实现机制的前提条件我们已经知道了,那么实现的时候,在对象构造时,虚函数列表中首先填写的是父类的入口函数地址,然后子类如果复写了父类函数就将子类函数地址覆盖原地址,上面的蓝色部分就是覆盖的结果,其中子类自己的虚函数补充到后面。
二、虚继承
虚继承相对于一般继承和虚函数又有一些不同,所以理解起来还是有一些复杂的,那么现在我们已经理解了虚函数表了再理解虚继承就比较容易了。
虚继承的提出主要是为了避免多重继承时的基类数据重复的问题。比如在菱形继承情况(类A派生出类B和类C,然后B、C同时被D继承)时,A的数据在最后的子类D的对象中只有一份数据,而不会重复,这就是虚继承 的功劳。而要不出现重复数据,实现的原理仍然是地址使然。
具体实现:首先虚继承派生出来的子类实例对象时,在对象中首先会有指向父类虚函数表的指针,以及指向父类成员变量(父类数据)列表的指针,表中存放的当然是函数或者数据的地址。在菱形继承中能保证只有一份数据而不重复就是保存的都是A类数据的地址,所以存储数据是检查到数据地址相同则存放时只保存一份数据了。另外如果子类还有自己的虚函数,那么子类还有自己的虚函数列表指针,指向自己的虚函数列表,也就是说虚继承时派生类中可能会有虚继承引起的虚函数表指针、数据表指针以及自己的虚函数表指针。实际情况大家来看一下执行情况就知道了。
普通虚函数:
#include <iostream>
#include <vector>
using namespace std;
class A
{
public:
virtual void fun1() { }
virtual void fun2() { }
private:
char a[4];//内存对齐
};
class B: public A
{
public:
virtual void fun1() { }
virtual void fun2() { }
};
int main(void)
{
A a;
B b;
//cout<<"int size is "<<sizeof(int)<<endl;
cout<<"A's size is "<<sizeof(A)<<endl;
cout<<"B's size is "<<sizeof(B)<<endl;
cout<<"a's size is "<<sizeof(a)<<endl;
cout<<"b's size is "<<sizeof(b)<<endl;
//cout<<"B's size is "<<sizeof(a.a)<<endl;
system("pause");
return 0;
}
输出结果为
A's size is 8
B's size is 8
a's size is 8
b's size is 8
请按任意键继续. . .
虚继承:
#include <iostream>
#include <vector>
using namespace std;
class A
{
public:
virtual void fun1() { }
virtual void fun2() { }
private:
char a[4];//内存对齐
};
class B: virtual public A
{
public:
virtual void fun1() { }
virtual void fun3() { }
};
int main(void)
{
A a;
B b;
//cout<<"int size is "<<sizeof(int)<<endl;
cout<<"A's size is "<<sizeof(A)<<endl;
cout<<"B's size is "<<sizeof(B)<<endl;
cout<<"a's size is "<<sizeof(a)<<endl;
cout<<"b's size is "<<sizeof(b)<<endl;
//cout<<"B's size is "<<sizeof(a.a)<<endl;
system("pause");
return 0;
}
输出结果:
A's size is 8
B's size is 16
a's size is 8
b's size is 16
请按任意键继续. . .
前一个结果size大小是8是因为虚函数指针占4字节,数据内存对齐也占4字节。后面结果增加8是因为B拥有虚继承引起的虚函数指针、虚基表以及自己的虚函数指针,再加上数据占4 字节总共16字节。(我使用的编译器是VS2012)
那对于c++的多态我们其实不用纠结于艰深的概念名词,我们只需抓住这一点就可以了,父类之所以能调用子类函数,不是多态在起作用,多态只是名词而已,他什么也不能做,真正要调用一个函数必须拿到该函数的入口地址,这才是多态实现的真正原理。父类能调用子类函数就是因为父类指针通过虚函数表找到的是复写后填入的子类的函数指针,所以才能调用子类函数。