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

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

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实现对虚基类的访问
    构造责任:最派生类负责调用虚基类的构造函数
    内存布局:虚基类成员通常位于派生类对象的末尾
    性能开销:每次访问虚基类成员都需要查表计算偏移量
    这种机制确保了即使在复杂的多重继承结构中,虚基类也只有一份实例,解决了菱形继承的二义性和数据冗余问题。
相关推荐
王老师青少年编程2 小时前
信奥赛C++提高组csp-s之搜索进阶(迭代加深IDDFS)
c++·csp·信奥赛·csp-s·提高组·iddfs·埃及分数
liulilittle2 小时前
我从 BBRv1 到 KCC 的思考
网络·c++·tcp/ip·计算机网络·tcp·bbr·通信
落羽的落羽2 小时前
【项目】JsonRpc框架——开发实现1(细节功能、字段定义、抽象层、具象层)
linux·服务器·网络·c++·人工智能·算法·机器学习
handler012 小时前
【算法】并查集(普通/扩展/带权)模板与例题
数据结构·c++·笔记·算法·c·图论·查并集
繁星蓝雨3 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余3 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习
a诠释淡然3 小时前
C++ vs Rust:哪个更适合你的下一个项目?
开发语言·c++·rust
小小de风呀3 小时前
de风——【从零开始学C++】(十二):stack和queue的基本使用和模拟实现
开发语言·c++
汉克老师4 小时前
GESP6级C++考试语法知识(五十三、动态规划----背包问题(六、分组背包)
c++·动态规划·背包问题·gesp6级·gesp六级·分组背
雪度娃娃4 小时前
转向现代C++——保证const成员函数的线程安全性
开发语言·c++