C++中的菱形继承问题

探讨C++继承模型,单继承、多继承,重点讲解菱形继承问题,如何通过虚继承解决,以及继承与组合的适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学到C++时我们知道了继承但是一般都是使用单继承为主,单继承就是一个子类只能继承一个父类而多继承是指一个子类可以同时继承多个父类。

菱形继承

 菱形继承是多继承中的一个特殊情况。当一个子类同时继承两个具有共同父类的类时,就会出现菱形继承问题。但是这种情况下,子类会继承同一个父类的特性和方法两次,导致特性和方法的冗余。

一般继承方式的菱形继承问题

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

此时如果Assistant创建a对象时,如果此时访问a对象的_name成员时(也就是People类里的成员)就产生了歧义,因为此时存在两种情况:访问的是Student类里的_name成员,也可能是Teacher类里的_name成员。所以就产生了二义性的问题。

但是也是有解决方法的:指定类成员

    a.Student::_name = "陈同学";
	a.Teacher::_name = "陈老师";

但是这样就显得代码十分冗余看起来就十分别扭。这种菱形继承就相当于是继承同一父类两次,这是完全没必要的,就像你一个人身份证上不可能有两个名字吧

虚拟继承

 虚继承(virtual inheritance):在父类之间的继承关系中,使用关键字virtual来声明继承关系。这样在派生类中只会有一份共同祖先的数据,从而避免了冗余数据的问题。

//在存在数据冗余的基类下的派生类加上virtual关键字
class Person
{
public:
	string _name; // 姓名
};
class Student : virtual public Person//
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

所以此时就只有一份Person数据了。此时的person类被称为虚基类

验证:(即使指定类初始化也是进行统一初始化)

对比非虚拟与虚拟继承的内存布局

class A
{
public:
    A(int a):_a(a){}
    int _a;
};
class B :virtual public A
{
public:
    B(int a,int b) :A(a),_b(b) {}
    int _b;
};
class C :virtual public A
{
public:
    C(int a,int c) :A(a), _c(c) {}
    int _c;
};
class D : public C, public B // 继承顺序决定构造基类对象的先后顺序
{
public:
    D(int a, int b, int c, int d)
        :B(a, b),
        C(a, c),
        A(a),
        _d(d)
    {}
    int _d;
};
int main()
{
    D d(1, 2, 3, 4);
    cout << sizeof(d) << endl;

    return 0;
}

先看看非虚拟继承数据的存储空间:


 虚继承下的数据空间地址:(X64机器测试)

我们发现虚拟继承时的内存布局很有意思。一般继承中对于B,C类共同继承的A类,当D类访问A类成员数据是具有的二义性的。而对于virtual修饰过后的虚拟继承对于A类中的成员_a是存储在底下的,也就是说在D类中只存在一份A类的成员数据。但是现在的问题其实就是如果找到该数据的位置???其实内存中要想找到一个空间的数据其实就是找地址,找到地址就可以去访问地址空间中的数据。所以仔细观察C类和B类中的前八个字节存储的不恰好就是8字节大小的地址吗。

这里的B和C的两个指针虚基表指针指向的是虚基表

所以再次访问该地址下的内容:

经过分析可以了解到虚拟继承时的共有数据存放的位置在各个类中其实就是一个通过存放一个地址,而该地址下存放的数据恰恰就是与共有的祖先数据的当前地址偏移量(虚基表中存放的就是偏移量)。就相当于为D类创建了一个偏移量表(不属于D对象的空间),以便为D创建多个对象时,其中派生类中的虚基表指针都是不会发生改变的,所以该D类的所有对象都是共用同一张虚基表。

虚拟继承同样可以进行赋值切片:

其实这样做的目的就是以便父子类对象的赋值。因为在继承关系中,子类继承了父类的特性和方法。子类和父类之间是一种"is-a"的关系。"is-a"关系表示子类是父类的一种类型。所以子类对象赋值给父类是天然的,不会产生临时对象。这也叫作父子类赋值兼容规则(切割/切片)。相当于将子类多余父类的数据切割以后再赋值给父类。

而在虚拟继承中同样是满足父子类赋值兼容的,所以就拿上代码来说当我们B b=d;的过程时B类中并没有D类的成员数据,所以说就将类似B类这种类再存一个指针,而指针中存的就是该地址处与A类成员的地址偏移量,所以在赋值的过程中编译器就会以这种方式来寻找A类的成员数据。


 其实虚继承以后不仅仅是D类的对象数据是这样的存储方式,其实B类C类创建的对象也是这种存储方式。

继承和组合

继承是指一个类(称为派生类或子类)可以从另一个类(称为基类或父类)中继承属性。

组合是指一个类可以包含其他类的对象作为自己的成员。


在继承关系中,子类继承了父类的特性和方法,但父类并不是子类的成员对象。public继承关系下子类和父类之间是一种"is-a"的关系,而不是"has-a"的关系。"is-a"关系表示子类是父类的一种类型,而"has-a"关系表示一个类具有另一个类的成员对象。而组合的两个类就是"has-a"的关系。


通过继承,派生类可以重用基类的代码和功能,并可以扩展或修改这些功能。继承可以建立类之间的"是一个"关系,其中派生类是基类的一种类型。继承可以实现代码的重用和面向对象编程的多态性。

通过组合,一个类可以将其他类的对象组合起来实现更复杂的功能。组合可以建立类之间的"具有"关系,其中包含对象是类的一部分。通过组合,可以灵活地构建类之间的关系,实现更灵活和模块化的设计。

选择使用

选择继承还是组合取决于具体的场景和需求。一般来说,当两个类之间存在"是一个"关系,并且派生类需要重用基类的代码和功能时,使用继承更合适。而当两个类之间存在"具有"关系,并且一个类需要使用另一个类的功能,但并不满足"是一个"关系时,使用组合更合适。

但是一般能使用组合尽量使用组合。组合的耦合度低,代码维护性更好。

继承易错点

test_1

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 //先继承的先创建
{
	public: int _d; 
};
int main() {
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}//判断p1 p2 p3的关系???

 分析:先创建Derive类的对象,而Derive类同时也继承了Base1和Base2这两个类,所以地址空间模拟图应该是:

因为Base1继承在Base2的前面,所以Base1先被创建出来,所以p1和p3都是指向起始处,所以地址值是相同的,但是两个指针指向的内容范围是不同。而Base2就显然不同于Base1。


test_2(虚拟继承)

class A {
public:
	A(const char* s)
	{ cout << s << endl; }
	~A() {}
};
class B :virtual public A
{
public:
	B(const char* s1, const char* s2)
		:A(s1) 
	{ cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2) 
		:A(s1) 
	{ cout << s2 << endl; }
};
class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const char* s4) 
		:B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl; 
	}
};
int main() 
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

首先是new并且初始化一个D类的对象,此时不难看出ABCD四个类呈现虚拟菱形继承的关系。所以可以明确类A只会创建一份,所以只会调用一次A类的构造函数因为D的父类B类和C类并没有默认的构造函数,所以D类的初始化列表中显示调用了其父类的构造函数。但是初始化列表的顺序并不是真的调用顺序,这依赖于继承顺序。起先虚基类A是最先被BC类继承的然后看D类的继承顺序是B类在前C类在后,所以先创建B类,而B类虚拟继承了A类所但是A类是虚基类,已经被调用过构造,不会重复构造,所以开始调用B类的构造同理其次就是C类。


成员变量初始化:成员变量走初始化列表进行初始化的顺序是依据于成员变量在类里被声明的顺序

继承类的初始化:先继承的先初始化即先被调用


### C++ 中的菱形继承问题及其解决方案 #### 菱形继承概述 在 C++ 中,当一个派生类通过多重继承两个不同的基类派生而来,而这两个基类又共同继承自同一个祖先类时,就会形成所谓的“菱形继承”。这种结构可能导致二义性问题 (ambiguity),因为编译器无法判断应该使用哪个路径上的成员函数或变量。 例如,在以下情况下会遇到菱形继承: ```cpp class Base { public: void func() {} }; class Derived1 : public Base {}; class Derived2 : public Base {}; class FinalDerived : public Derived1, public Derived2 {}; // 形成菱形继承 ``` 如果 `FinalDerived` 尝试调用 `func()` 函数,则会出现二义性错误,因为编译器不知道是从 `Derived1` 还是 `Derived2` 继承来的版本[^1]。 --- #### 解决方案:虚继承 (Virtual Inheritance) 为了避免上述二义性问题,可以采用 **虚继承** 来解决。虚继承确保所有子类共享同一份基类实例,而不是各自独立复制一份副本。这样即使存在多个继承路径,最终也只会有一条通向公共基类的有效路径。 以下是修正后的代码示例: ```cpp class Base { public: void func() {} }; class Derived1 : virtual public Base {}; // 使用虚拟继承 class Derived2 : virtual public Base {}; // 使用虚拟继承 class FinalDerived : public Derived1, public Derived2 {}; // 不再有二义性 int main() { FinalDerived obj; obj.func(); // 正确解析到唯一的 Base::func() } ``` 在此设计下,无论通过哪一条路径访问 `Base` 类中的成员,都指向相同的单一实例[^3]。 --- #### 实际应用注意事项 尽管虚继承解决了二义性问题,但它引入了一些额外复杂度和开销。具体表现为: - 需要显式指定初始化顺序; - 对象大小可能增加,因存储必要的指针来管理虚表 (vtable) 和动态绑定逻辑。 此外需要注意的是,并非所有的编程语言都支持类似的机制。例如 Java 明确禁止类之间的多重继承,转而依赖接口(interface)实现类似功能[^2]。 对于更复杂的场景,开发者还需考虑如何合理分配职责以及避免不必要的耦合关系[^4]。 --- #### 总结 综上所述,C++ 提供了强大的灵活性允许定义包含菱形继承结构的程序模型;然而伴随此自由度同时也带来了潜在风险——即可能出现名称冲突或者行为未定义等情况。利用好虚继承这一特性能够有效缓解这些问题带来的困扰的同时保持良好的软件工程实践标准[^5]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CR0712

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值