前言
前面文章中我们讨论了虚函数的内存模型,已经了解了包含虚函数的类是通过虚函数表指针、虚函数表和虚函数指针来找到代码段中的函数,本文我们讨论在各种情况下,类在内存中的布局。
无继承关系
我们看下面的代码:
class base1{
public:
virtual void foo1(){
std::cout << "call base1 foo1" << std::endl;
}
virtual void foo2(){
std::cout << "call base1 foo2" << std::endl;
}
virtual void foo3(){
std::cout << "call base1 foo3" << std::endl;
}
};
这是一个无继承的类,按照我们之前讨论的,这个类的内存模型可以用下图表示。这里我们用虚函数指针直接代表虚函数,省去指向代码段的部分。
实际上虚表中在base::foo3()的下面还有一格,为0表示本虚表为最后一张虚表,为1表示后续还有其他虚表,base::foo1()的前面也还有一个表项,储存了type_info信息,为简化讨论,这两项后续都不画出。
下面的代码可以证明图中的内存模型。
typedef void(*Fun)(void);
int main(){
base1 d;
long long* vptr = (long long*)&d;
for(int i=0;i<3;++i){
Fun pFun = (Fun)*(((long long*)*vptr)+i);
pFun();
}
return 0;
}
运行结果:
单一继承关系
下面是一段具有单一继承关系的代码:
#include <bits/stdc++.h>
class derive1:public base1{
virtual void foo3(){
std::cout << "call derive1 foo3" << std::endl;
}
virtual void foo4(){
std::cout << "call derive1 foo4" << std::endl;
}
};
类derive1继承了类base并重写了base类的foo3()函数,另外定义了自己的foo4()函数,新增加的foo4()会直接增加在虚表的末尾,其内存布局如下图。
同样的我们用一段代码来证明图中的内存布局。
int main(){
derive1 d;
long long* vptr = (long long*)&d;
for(int i=0;i<4;++i){
Fun pFun = (Fun)*(((long long*)*vptr)+i);
pFun();
}
return 0;
}
运行结果如下:
多继承
多继承时,实际上会有多张虚表,有几个父类就有几张虚表。
#include <bits/stdc++.h>
class base2{
public:
virtual void goo1(){
std::cout << "call base2 goo1" << std::endl;
}
virtual void goo2(){
std::cout << "call base2 goo2" << std::endl;
}
virtual void goo3(){
std::cout << "call base2 goo3" << std::endl;
}
};
class derive2:public base1,public base2{
virtual void foo5(){
std::cout << "call derive2 foo5" << std::endl;
}
};
int main(){
derive2 d;
long long** vptr = (long long**)&d;
for(int i=0;i<4;++i){
Fun pFun = (Fun)*(((long long*)vptr[0]+i));
pFun();
}
for(int i=0;i<3;++i){
Fun pFun = (Fun)*(((long long*)vptr[1]+i));
pFun();
}
return 0;
}
运行结果如下:
上面的代码中,类中仅有虚函数表,不存在其他成员变量,当我们在第一个父类中增加成员变量时,上面的代码就不能顺利打印第二个父类的虚函数,比如我们将类base1修改为:
class base1{
public:
virtual void foo1(){
std::cout << "call base1 foo1" << std::endl;
}
virtual void foo2(){
std::cout << "call base1 foo2" << std::endl;
}
virtual void foo3(){
std::cout << "call base1 foo3" << std::endl;
}
private:
int num;
};
再运行上面的main()函数,输出结果为:
这是因为多继承实际上的内存布局应该如下图:
此时两个虚函数表指针之间增加了一个成员变量,不能再通过这种方法直接访问第二个虚函数表指针。为了简化讨论,我们后面不使用成员变量。
菱形继承(虚继承)
多继承中,当子类的多个父类有相同的父类时,被称为菱形继承,菱形继承时,为避免存在多个共同父类实例,通常使用虚继承。比如下面的代码中,类derive3的两个父类derive1和derive2都继承自base1,此时如果重写base1中的方法,将会覆盖哪张虚表中的指针?derive3中新增的虚函数又存在于哪张虚表中?
class derive1:virtual public base1{
virtual void foo3(){
std::cout << "call derive1 foo3" << std::endl;
}
virtual void foo4(){
std::cout << "call derive1 foo4" << std::endl;
}
};
class derive2:virtual public base1,virtual public base2{
virtual void foo5(){
std::cout << "call derive2 foo5" << std::endl;
}
};
class derive3:public derive1,public derive2{
virtual void foo3(){
std::cout << "call derive3 foo3" << std::endl;
}
virtual void foo6(){
std::cout << "call derive3 foo6" << std::endl;
}
};
此时derive3中应该有三张虚表,一张继承自derive1,两个来自derive2,我们通过下面的代码来测试结果:
int main(){
derive3 d;
long long** vptr = (long long**)&d;
for(int i=0;i<5;++i){
Fun pFun = (Fun)*(((long long*)vptr[0]+i));
pFun();
}
for(int i=0;i<4;++i){
Fun pFun = (Fun)*(((long long*)vptr[1]+i));
pFun();
}
for(int i=0;i<3;++i){
Fun pFun = (Fun)*(((long long*)vptr[2]+i));
pFun();
}
return 0;
}
运行结果如下:
可以看到,当我们在derive3中重写foo3()方法时,两张虚表中的虚函数均被重写了,而新增的虚函数增加到了第一张虚表中。