C++面向对象中,虚函数与虚继承是两个完全不同的概念。
一、虚函数
C++程序中只要类中含有虚拟函数,编译程序都会为此类生成一个对应的虚拟函数跳转表(vtbl),该虚拟函数跳转表是一个又若干个虚拟函数体入口地址组成的一个线性表。派生类的虚拟函数跳转表的前半部分由父类的vtbl得出,但是里面的内容不一定相同,后半部分则对应着自己新定义的虚拟函数。
class Employee
{
protected:
char *Name;
int Age;
public:
void changeAge(int newAge);
virtual void retire(); //虚函数
Employee( char *n, int a );
~Employee();
};
class Manager: public Employee
{
int Level;
public:
void changeLevel( int l);
void retire(); //情况一、子类覆写了虚函数
Manager( char *n, int a, int l );
~Manager();
};
情况一、子类覆写了父类的虚函数,则此时子类和父类的虚函数表分别为:
情况二、子类没有覆写父类的虚函数,则此时子类和父类的虚函数表分别为:
class Manager: public Employee
{
int Level;
public:
void changeLevel( int l);
//void retire(); //情况二、子类没有覆写虚函数
Manager( char *n, int a, int l );
~Manager();
};
情况三、子类中自己的虚函数,则此时子类和父类的虚函数表分别为:
class Manager: public Employee
{
int Level;
public:virtual void changeLevel(int l); //情况三、子类有自己的虚拟函数
void retire();
Manager( char *n, int a, int l );
~Manager();
};
二、虚继承
虚继承是为了解决多重继承中的问题而出现的。要理解虚继承的实现机制,首先看一般继承:
class A
{
char k[3];
public:
virtual void aa()
{
};
};
class B:public A
{
char j[3];
public:
virtual void bb()
{
};
};
class C:public B
{
char i[3];
public:
virtual void cc()
{
};
};
int main()
{
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
return 0;
}
得到的答案是:8 12 16
分析如下:
1、 对于类A,有一个虚函数,需要一个虚函数表vtbl来记录函数的入口地址,每个地址一个虚指针,指针的大小为4,类中还有一个成员变量char k[3],则根据数据对齐原则,可以得到sizeof(A)的大小为8;
2、 对于类B,也有一个虚函数,需要一个虚函数表vtbl来记录函数的入口地址,每个地址一个虚指针,指针的大小为4,类中还有一个成员变量j[3],但是类B是继承自类A的,是一般继承,不是虚继承,所以可以得到sizeof(B)的大小为: 4(指向虚函数的虚指针)+4(自己的数据成员)+4(父类A的数据成员)=12;
3、 对于类C,也有一个虚函数,需要一个虚函数表vtbl来记录函数的入口地址,每个地址一个虚指针,指针的大小为4,类中还有一个成员变量i[3],同样的,类C是继承自类B的,是一般继承,不是虚继承,所以可以得到sizeof(C)的大小为4(指向虚函数的虚指针)+4(自己的数据成员)+8(父类的数据成员)=16;
再来看一下虚继承的情况:
class A
{
char k[3];
public:
virtual void aa()
{
};
};
class B:public virtual A
{
char j[3];
public:
virtual void bb()
{
};
};
class C:public virtual B
{
char i[3];
public:
virtual void cc()
{
};
};
int main()
{
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
return 0;
}
得到的答案是:8 20 32
分析如下:
1、对于类A,有一个虚函数,需要一个虚函数表vtbl来记录函数的入口地址,每个地址一个虚指针,指针的大小为4,类中还有一个成员变量k[3],则根据数据对齐原则,可以得到sizeof(A)的大小为8;
2、对于类B,也有一个虚函数,需要一个虚函数表vtbl来记录函数的入口地址,每个地址一个虚指针,指针的大小为4,类中还有一个成员变量j[3],但是类B是继承自类A的,而且是虚继承,虚继承的实现是通过一个虚基类指针列表,比如vptr_B_A指向虚基类,同时还包含了父类的所有内容,所以可以得到sizeof(B)的大小为:4(指向自己虚函数的虚指针)+4(自己的数据成员)+4(指向虚基类的指针vptr_B_A)+8(父类A的内容大小)=20;
3、对于类C,也有一个虚函数,需要一个虚函数表vtbl来记录函数的入口地址,每个地址一个虚指针,指针的大小为4,类中还有一个成员变量i[3],同样的,类C是继承自类B的,而且是虚继承,虚继承的实现是通过一个虚基类指针列表,比如vptr_C_B指向虚基类,同时还包含了父类的所有内容,所以可以得到sizeof(C)的大小为:4(指向自己虚函数的虚指针)+4(自己的数据成员)+4(指向虚基类的指针(vptr_C_B)+20(父类B的内容大小)=32;
class A
{
char k[3];
public:
virtual void aa()
{
};
};
class B:public virtual A
{
char j[3];
public:
virtual void bb()
{
};
};
class C:public virtual A
{
char i[3];
public:
virtual void cc()
{
};
};
class D: public B,public C
{
char n[3];
public:
virtual void dd()
{
};
};
int main()
{
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
cout<<sizeof(D)<<endl;
return 0;
}
得到的结果为:8 20 20 36
分析如下:
1、类sizeof(A,B,C),从前面的分析可以得到结果为8 20 20;
2、sizeof(D)=4(指向自己虚函数的虚指针)+4(自己的数据成员)+
12(父类B的内容,包括数据成员、指向虚函数的虚指针、以及虚基类指针列表vptr_B_A)+
12(父类C的内容,包括数据成员、指向虚函数的虚指针、以及虚基类指针列表vptr_C_A)+
4(类A的数据成员)=36。
可见,类D中只包含了类A的一份副本,虚继承很好地解决了多重继承中的二义性问题。