虚继承
虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:
- 虚继承的派生类,如果定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。(非虚继承时,派生类新的虚函数直接扩展在基类虚函数表的下面。)
- 虚继承的派生类有单独的虚函数表,基类也有单独的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。
- 虚继承的派生类对象中,含有四字节的虚基表指针。
在C++对象模型中,虚继承而来的派生类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由上面的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。我们通过一张图来更好地理解。

代码
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" ; }
virtual void Bf() { cout << "B::Bf()" ; }
};
class B1 : virtual public B
{
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" ; }
virtual void f1() { cout << "B1::f1()" ; }
virtual void Bf1() { cout << "B1::Bf1()" ; }
};
对象模型
代码演示
typedef void(*Fun)(void);
int main()
{
B1 a;
cout << "B1对象内存大小为:" << sizeof(a) << endl;
//取得B1的虚函数表
cout << "[0]B1::vptr";
cout << "\t地址:" << (int*)(&a) << endl;
//输出虚表B1::vptr中的函数
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*(int*)(&a) + i);
fun1();
cout << "\t地址:\t" << *((int*)*(int*)(&a) + i) << endl;
}
//[1]
cout << "[1]vbptr ";
cout << "\t地址:" << (int*)(&a) + 1 << endl; //虚表指针的地址
//输出虚基类指针条目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int*)((int*)*((int*)(&a) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]B1::ib1=" << *(int*)((int*)(&a) + 2);
cout << "\t地址:" << (int*)(&a) + 2;
cout << endl;
//[3]
cout << "[3]值=" << *(int*)((int*)(&a) + 3);
cout << "\t\t地址:" << (int*)(&a) + 3;
cout << endl;
//[4]
cout << "[4]B::vptr";
cout << "\t地址:" << (int*)(&a) + 4 << endl;
//输出B::vptr中的虚函数
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*((int*)(&a) + 4) + i);
fun1();
cout << "\t地址:\t" << *((int*)*((int*)(&a) + 4) + i) << endl;
}
//[5]
cout << "[5]B::ib=" << *(int*)((int*)(&a) + 5);
cout << "\t地址: " << (int*)(&a) + 5;
cout << endl;
}
结果
这个结果与我们的C++对象模型图完全符合。这时我们可以来分析一下虚表指针的第二个条目值12的具体来源了,回忆上文讲到的:
第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。
在我们的例子中,也就是B类实例内存地址相对于vbptr的偏移值,也即是:[4]-[1]的偏移值,结果即为12,从地址上也可以计算出来:00F8FDE4-00F8FDD8结果的十进制数是12。现在,我们对虚基类表的构成应该有了一个更好的理解。
菱形继承
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : virtual public B
{
public:
int ib2;
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
public:
D(int i = 10000) :id(i) {}
virtual void f() { cout << "D::f()" << endl; }
virtual void f1() { cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:
- 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)。
- D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
- 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
- 共同虚基类B的内容放到了派生类对象D内存布局的最后。
代码演示
typedef void(*Fun)(void);
int main()
{
D d;
cout << "D对象内存大小为:" << sizeof(d) << endl;
//取得B1的虚函数表
cout << "[0]B1::vptr";
cout << "\t地址:" << (int*)(&d) << endl;
//输出虚表B1::vptr中的函数
for (int i = 0; i < 3; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*(int*)(&d) + i);
fun1();
cout << "\t地址:\t" << *((int*)*(int*)(&d) + i) << endl;
}
//[1]
cout << "[1]B1::vbptr ";
cout << "\t地址:" << (int*)(&d) + 1 << endl; //虚表指针的地址
//输出虚基类指针条目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int*)((int*)*((int*)(&d) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]B1::ib1=" << *(int*)((int*)(&d) + 2);
cout << "\t地址:" << (int*)(&d) + 2;
cout << endl;
//[3]
cout << "[3]B2::vptr";
cout << "\t地址:" << (int*)(&d) + 3 << endl;
//输出B2::vptr中的虚函数
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 3) + i);
fun1();
cout << "\t地址:\t" << *((int*)*((int*)(&d) + 3) + i) << endl;
}
//[4]
cout << "[4]B2::vbptr ";
cout << "\t地址:" << (int*)(&d) + 4 << endl; //虚表指针的地址
//输出虚基类指针条目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int*)((int*)*((int*)(&d) + 4) + i);
cout << endl;
}
//[5]
cout << "[5]B2::ib2=" << *(int*)((int*)(&d) + 5);
cout << "\t地址: " << (int*)(&d) + 5;
cout << endl;
//[6]
cout << "[6]D::id=" << *(int*)((int*)(&d) + 6);
cout << "\t地址: " << (int*)(&d) + 6;
cout << endl;
//[7]
cout << "[7]值=" << *(int*)((int*)(&d) + 7);
cout << "\t\t地址:" << (int*)(&d) + 7;
cout << endl;
//间接父类
//[8]
cout << "[8]B::vptr";
cout << "\t地址:" << (int*)(&d) + 8 << endl;
//输出B::vptr中的虚函数
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 8) + i);
fun1();
cout << "\t地址:\t" << *((int*)*((int*)(&d) + 8) + i) << endl;
}
//[9]
cout << "[9]B::id=" << *(int*)((int*)(&d) + 9);
cout << "\t地址: " << (int*)(&d) + 9;
cout << endl;
getchar();
}
数据成员如何访问(直接取址)
跟实际对象模型相关联,根据对象起始地址+偏移量取得。
函数成员如何访问(间接取址)
跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。
多态如何实现?
多态(Polymorphisn)在C++中是通过虚函数实现的。如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于:虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。
为什么析构函数设为虚函数是必要的
析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。
如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。例如,通过new一个派生类对象,赋给基类指针,然后delete基类指针,缺少了派生类的析构函数调用。把析构函数声明为虚函数,调用就正常了。