虚继承实现原理详解及案例

一个经典的菱形继承问题:

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
  1. 只能通过 Animal 访问 weight
    虚继承下,CatDog里的weight不再属于 Cat,也不属于 Dog,而是共同归属到最顶层的基类 Animal。
    所有子类共享同一份Animal成员,所以说:weight 属于 Animal,通过 Animal 访问。
  2. 不会出现二义性
    因为只有一份weight,编译器不会再困惑 "到底是哪一个 weight",访问时就不会报歧义错误。

虚继承的内存布局实现原理

  1. vbptr(虚基类指针)
    每个虚继承的类都包含一个vbptr(虚基类指针)
    vbptr指向虚基类表(vbtable)
  2. 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工作机制详解

  1. vbtable内容
    记录虚基类相对于当前对象的偏移量
    每个虚继承的类都有自己的vbtable
    不同类的vbtable可能指向相同的虚基类位置
    实现示意图
  2. 偏移量查找过程
    通过vbptr找到对应的vbtable
    从vbtable中获取虚基类的偏移量
    当前对象地址 + 偏移量 = 虚基类成员地址
    五、关键特点总结
    唯一实例:虚基类在整个继承链中只存在一个实例
    间接访问:通过vbptr和vbtable实现对虚基类的访问
    构造责任:最派生类负责调用虚基类的构造函数
    内存布局:虚基类成员通常位于派生类对象的末尾
    性能开销:每次访问虚基类成员都需要查表计算偏移量
    这种机制确保了即使在复杂的多重继承结构中,虚基类也只有一份实例,解决了菱形继承的二义性和数据冗余问题。
相关推荐
森G7 小时前
34、事件的分发机制---------事件系统
c++·qt
DY009J8 小时前
从 MSYS2 环境中提取独立 MinGW-w64 工具链的技术方案
c++·windows
xiaoye-duck8 小时前
《算法题讲解指南:动态规划算法--子数组系列》--25.单词拆分,26.环绕字符串中唯一的子字符串
c++·算法·动态规划
承渊政道8 小时前
【优选算法】(实战:栈、队列、优先级队列高频考题通关全解)
数据结构·c++·笔记·学习·算法·leetcode·宽度优先
liulilittle8 小时前
OPENPPP2 1.0.0.26145 正式版发布:内核态 SYSNAT 性能飞跃 + Windows 平台避坑指南
开发语言·网络·c++·windows·通信·vrrp
AIminminHu8 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(三-1-(2):当你的CAD代码变得“又大又乱”:从手动编译到CMake,从随性编码到单元测试))
c++·单元测试·cmake·cad·cad开发
xiaoye-duck9 小时前
《算法题讲解指南:动态规划算法--子数组系列》--23.等差数列划分,24.最长湍流子数组
c++·算法·动态规划
消失的旧时光-19439 小时前
C++ 网络服务端主线:从线程池到 Reactor 的完整路线图
开发语言·网络·c++·线程池·并发
cookies_s_s9 小时前
C++ 模板与泛型编程
linux·服务器·开发语言·c++