一个经典的菱形继承问题:
cpp
#include<iostream>
using namespace std;
class animal{
public:
int weight;
animal(int w=0):weight(w){
cout<<"animal constructor called"<<endl;
}
//等价写法
// animal(int w=0){
// weight=w;
// cout<<"animal constructor called"<<endl;
// }
};
//普通继承-导致菱形继承问题
class cat:public animal{
public:
string catname;
cat(const string&name,int w=0):animal(w),catname(name){
cout<<"cat constructor called"<<endl;
}
//等价写法:
// cat(const string&name,int w=0):animal(w){
// catname=name;
// cout<<"cat constructor called"<<endl;
// }
};
class dog:public animal{
public:
string dogname;
dog(const string&name,int w=0):animal(w),dogname(name){
cout<<"dog constructor called"<<endl;
}
};
class catdog:public cat,public dog{
public:
string nickname;
catdog(const string&cn,const string&dn,const string&n,int w=0):cat(cn,w),dog(dn,w),nickname(n){
cout<<"catdog constrctor called"<<endl;
}
};
int main() {
// 创建 catdog 对象(传入4个参数:猫名、狗名、昵称、体重)
catdog myPet("小猫", "小狗", "猫猫狗", 10);
// cout<<endl;
// catdog p("a","b","c",1);
// cout<<endl;
// dog d("a",2);
return 0;
}
//结果:
//animal constructor called
//cat constructor called
//animal constructor called
//dog constructor called
//catdog constrctor called
你的继承关系是 菱形继承:
animal
/ \
cat dog
\ /
catdog
规则:
创建子类对象时,构造函数执行顺序:先父类,再自己!
而且:
在普通继承的情况下,CatDog 对象中会包含两份 Animal 的副本,这就是菱形继承问题。
cat 有一个父类 animal,dog 也有一个父类 animal → 所以会构造 2 次 animal!
虚继承解决
cpp
#include <iostream>
using namespace std;
class animal{
public:
int weight;
animal(int w=0):weight(w){
cout<<"animal constructor called"<<endl;
}
void setweight(int w){weight=w;}
int getweight() const{return weight;}
};
class cat:virtual public animal{
public:
string catname;
cat(const string&n,int w=0):animal(w){
catname=n;
cout<<"cat:"<<catname<<" "<<weight<<endl;
}
};
class dog:virtual public animal{
public:
string dogname;
dog(const string&n,int w=0):animal(w),dogname(n){
cout<<"dog:"<<dogname<<" "<<weight<<endl;
}
};
class catdog:public cat,public dog{
public:
string nickname;
catdog(const string&cn,const string&dn,const string&n,int w=0):animal(w),cat(cn,w),dog(dn,w),nickname(n){
cout<<"catdog:"<<catname<<" "<<dogname<<" "<<nickname<<" "<<weight<<endl;
}
};
int main(){
// catdog("tom","spike","tommy",10);
catdog cd("Tom", "Spike", "Tommy", 10);
// 只能通过Animal访问weight,不会出现二义性
cout<<"weight:"<<cd.getweight()<<endl;
cd.setweight(20);
cout << "Updated Weight: " << cd.getweight() << endl;
// cd.weight=30;
// cout<<"weight:"<<cd.getweight()<<endl;
return 0;
}
//结果:
//animal constructor called
//cat:Tom 10
//dog:Spike 10
//catdog:Tom Spike Tommy 10
//weight:10
//Updated Weight: 20
- 只能通过 Animal 访问 weight
虚继承下,CatDog里的weight不再属于 Cat,也不属于 Dog,而是共同归属到最顶层的基类 Animal。
所有子类共享同一份Animal成员,所以说:weight 属于 Animal,通过 Animal 访问。 - 不会出现二义性
因为只有一份weight,编译器不会再困惑 "到底是哪一个 weight",访问时就不会报歧义错误。
虚继承的内存布局实现原理
- vbptr(虚基类指针)
每个虚继承的类都包含一个vbptr(虚基类指针)
vbptr指向虚基类表(vbtable) - vbtable(虚基类表)
存储虚基类相对于当前对象的偏移量
表项包含偏移量信息,用于定位虚基类成员
cpp
#include <iostream>
using namespace std;
class A {
public:
int a_data;
A() : a_data(10) {}
};
class B : virtual public A {
public:
int b_data;
B() : A(), b_data(20) {} // 必须显式调用虚基类构造函数
};
class C : virtual public A {
public:
int c_data;
C() : A(), c_data(30) {} // 必须显式调用虚基类构造函数
};
class D : public B, public C {
public:
int d_data;
D() : A(), B(), C(), d_data(40) {} // 最派生类负责调用虚基类构造函数
};
int main() {
cout << "Size of A: " << sizeof(A) << endl; // 4 bytes
cout << "Size of B: " << sizeof(B) << endl; // 12 bytes (4 for data + 8 for vbptr)
cout << "Size of C: " << sizeof(C) << endl; // 12 bytes (4 for data + 8 for vbptr)
cout << "Size of D: " << sizeof(D) << endl; // 28 bytes
D obj;
// 所有的虚基类成员访问都是唯一的
obj.a_data = 100;
obj.b_data = 200;
obj.c_data = 300;
obj.d_data = 400;
cout << "obj.a_data: " << obj.a_data << endl; // 100
cout << "obj.b_data: " << obj.b_data << endl; // 200
cout << "obj.c_data: " << obj.c_data << endl; // 300
cout << "obj.d_data: " << obj.d_data << endl; // 400
// 通过不同路径访问虚基类成员,结果相同
B* b_ptr = &obj;
C* c_ptr = &obj; // C指针 也指向同一个 obj
b_ptr->a_data = 500;
cout << "c_ptr->a_data: " << c_ptr->a_data << endl; // 500,证明只有一个A实例
return 0;
}
//结果
//Size of A: 4
//Size of B: 12
//Size of C: 12
//Size of D: 24
//obj.a_data: 100
//obj.b_data: 200
//obj.c_data: 300
//obj.d_data: 400
//c_ptr->a_data: 500
四、vbptr和vbtable工作机制详解
- vbtable内容
记录虚基类相对于当前对象的偏移量
每个虚继承的类都有自己的vbtable
不同类的vbtable可能指向相同的虚基类位置
实现示意图

- 偏移量查找过程
通过vbptr找到对应的vbtable
从vbtable中获取虚基类的偏移量
当前对象地址 + 偏移量 = 虚基类成员地址
五、关键特点总结
唯一实例:虚基类在整个继承链中只存在一个实例
间接访问:通过vbptr和vbtable实现对虚基类的访问
构造责任:最派生类负责调用虚基类的构造函数
内存布局:虚基类成员通常位于派生类对象的末尾
性能开销:每次访问虚基类成员都需要查表计算偏移量
这种机制确保了即使在复杂的多重继承结构中,虚基类也只有一份实例,解决了菱形继承的二义性和数据冗余问题。