继承:复杂的菱形继承与虚继承

目录

前言

复杂的菱形继承及菱形虚拟继承

继承方式

virtual关键字

虚拟继承的原理

原理:

额外消耗:

构造顺序为什么是ABCD

不允许使用间接非虚拟基类原理

假设只有A B

为什么virtual加在B C中而不是D中?

如何实现一个不能被继承的类(继承无意义的类)

​编辑继承的总结和反思


前言

上文已经完成了前六种继承知识的介绍,下面是继承的复杂情况介绍。本文将着重介绍复杂的菱形继承与虚继承

复杂的菱形继承及菱形虚拟继承

继承方式

单继承:一个子类只有一个直接父类时称这个继承关系为单继承(箭头指向基类)
多继承:在第一个基类的后面加一个,即可连接
class D : public C, public B,public A这就是一种多继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承的缺陷:存在数据的冗余和二义性的问题。那如何解决这几个问题呢?

virtual关键字

virtual关键字可以解决菱形继承中基类对象冗余的问题。

在C++中,virtual 关键字主要用于两个方面:定义虚函数和实现虚拟继承。以下是关于虚拟继承用途的详细介绍:

#include<iostream>
using namespace std;
class A {
public:
	A(const char* s) { cout << s << endl; }
	~A() {}
};

//class B :virtual public A
class B : public A
{
public:
	B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};

class C : public A
{
public:
	C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};

class D : public C, public B,public A
{
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;
}

这就是一个菱形继承的典型实例。我们在初始化D时,A一共被调用了三次。因此存在大量的数据冗余与二义性问题。

由于A的冗余,我们需要把继承A的部分加上virtual,变成虚继承。使得最终只有一个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 C, public B, virtual public A

继承A的部分都得加上virtual,这样只有一个A,共享一个A副本。

不允许一部分加一部分不加。原因是加上virtual之后,A变成虚拟基类(只允许存在一个A),但是后面有不是virtual的,这样就又产生了A对象,这就出现了矛盾

虚拟继承的原理

虚拟继承是C++中用于解决多继承时可能出现的菱形继承问题(即一个类多次继承自同一个基类)的一种机制。虚拟继承允许在继承层次中共享基类的一个实例,而不是为每个直接或间接的基类副本都保留一份基类实例。以下是虚拟继承产生额外消耗的原因及其原理:

原理:

  1. 共享基类实例: 在虚拟继承中,无论基类在继承层次中被继承多少次,都只存在一个共享的基类实例。这意味着所有继承了这个虚拟基类(被virtual继承的类)的派生类都会引用同一个基类部分

  2. 虚基类表: 为了实现这种共享,编译器会为每个含有虚基类的对象插入一个额外的指针,这个指针指向一个虚基类表(vtable)。虚基类表包含了虚基类的偏移量信息,用于在运行时调整指针,使得能够正确地访问共享的基类实例。

  3. 调整: 当通过指针或引用访问虚基类成员时,编译器生成的代码会使用虚基类表来调整指针,确保指向正确的共享基类实例。

这个 A 同时属于 B C ,那么 B C 如何去找到公共的 A 呢? 这里是通过了 B C 的两个指针,指
向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量
可以找到下面的 A

额外消耗:

  1. 空间开销

    • 虚基类指针:每个含有虚基类的对象都需要额外的空间来存储指向虚基类表的指针。
    • 虚基类表:需要额外的存储空间来维护每个对象的虚基类偏移信息。
  2. 时间开销

    • 访问调整:每次访问虚基类的成员时,都需要进行指针调整,这增加了访问时间。
    • 构造和析构:在构造和析构过程中,需要确保虚基类部分只被初始化和清理一次,这可能导致更复杂的构造函数和析构函数调用序列,从而增加时间开销。
  3. 复杂性开销: 虚拟继承增加了编译器实现的复杂性,可能导致生成的代码更加复杂,这可能会间接影响程序的性能。

总之,虚拟继承虽然解决了菱形继承问题,但其机制带来的额外空间和时间开销,以及在复杂性和性能上的潜在影响,使得程序员在考虑使用虚拟继承时需要权衡其利弊。在设计继承体系时,如果可以避免,通常推荐不使用虚拟继承。

构造顺序为什么是ABCD

当进行虚拟继承之后,虚拟基类必须被显式初始化(最终派生类也是如此),当进行虚拟继承之后,构造顺序变成ABCD这是为什么呢?

构造顺序:在虚继承中,虚拟基类的构造函数必须在所有其他基类之前被调用。这是因为在派生类中只有一个虚拟基类的实例,所以需要首先构建它
单一共享实例:虚拟继承确保 class A 在 class D 中只有一个共享的实例。因此,无论 class B 和 class C 如何继承 class A,class D 中只有一个 class A 的副本。
阻止重复初始化:如果不直接在 class D 的构造函数初始化列表中初始化 class A,那么 class B 和 class C 的构造函数可能会尝试初始化 class A,这会导致重复初始化错误。所以虚拟继承之后,必须在D中显式初始化A
明确初始化:由于 class B 和 class C 都虚拟继承自 class A,class D 需要确保 class A 的构造函数只被调用一次,并且是在正确的顺序下。因此,class D 必须在它的构造函数初始化列表中直接初始化 class A

		:B(s1, s2)
		, C(s1, s3)
		, A(s1)

在初始化列表中,虽然A在最后,但是列出来的顺序,并不是初始化的顺序!!!

不允许使用间接非虚拟基类原理

在菱形继承中,不允许使用间接非虚拟基类(这个基类是间接的、非虚拟的)

class D : public C, public B
{
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;
	}
};

当我们去掉全部的虚拟继承之后,该构造函数会报错。因为A是间接、非虚拟继承的基类,不允许直接使用

如下就可以:

class D : public C, public B,public A
{
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;
	}
};

原因是A是直接继承的类,虽然全都不是虚拟继承 ,结果就是多了几个A的副本。

满足了:不允许使用间接非虚拟基类(这个基类是间接继承的、非虚拟的)原理

假设只有A B

假设只有A B,B虚拟继承A之后,那么可以保证使用B时,正常编译,并保证存在A吗?


答案是是的,如果只有 class A 和 class B,且 class B 虚拟继承自 class A,那么可以保证在使用 class B 时,代码可以正常编译,并且 class B 的实例中将存在一个 class A 的实例。虚拟继承确保了即使在多继承的情况下,也只有一个共享的基类实例。
 


以下是代码示例:
class A {
public:
	A() { std::cout << "A constructor called" << std::endl; }
	~A() { std::cout << "A destructor called" << std::endl; }
};

class B : virtual public A {
public:
	B() { std::cout << "B constructor called" << std::endl; }
	~B() { std::cout << "B destructor called" << std::endl; }
};

int main() {
	B b; // 创建B的实例,这将自动创建一个A的实例
	return 0;
}

需要注意的是,虚拟继承会引入一些额外的开销,因为它需要额外的机制来保证只有一个基类实例,并且需要处理虚继承层次中的指针调整。因此,虚拟继承通常只在必要时使用

为什么virtual加在B C中而不是D中?
 

答案:

在C++中,虚拟继承关键字 virtual 应用于那些想要共享其基类实例的派生类而不是在最终的派生类上。这是因为虚拟继承的目的是确保在多重继承的情况下,最终的派生类只拥有一个共享的基类实例,而不管这个基类被继承了多少次

以下是为什么 virtual 关键字加在 B 和 C 中而不是 D 中的原因:

共享基类实例:当我们使用虚拟继承时,我们的目的是确保在最终的派生类中只有一个基类实例。为了达到这个目的,必须要在那些直接继承基类并且想要共享基类实例的派生类上使用 virtual 关键字。
控制共享:通过在 B 和 C 上使用 virtual 关键字,我们告诉编译器,无论 B 和 C 被如何继承,它们都应当共享它们共同的基类 A 的单一实例。如果我们在 D 上使用 virtual,这不会产生任何效果,因为 D 是最终的派生类,它并不继承其他基类来共享实例。
构造函数的调用:由于 B 和 C 虚拟继承自 A,它们的构造函数不会尝试创建 A 的实例。相反,它们会依赖最终的派生类(在这个例子中是 D)来初始化共享的 A 实例。因此,D 的构造函数负责初始化 A,并且必须确保只初始化一次
菱形继承问题:虚拟继承解决了菱形继承问题,即一个类通过多个路径继承自同一个基类。在这个例子中,D 通过 B 和 C 继承 A,形成一个菱形结构。为了确保 D 只有一个 A 的实例,B 和 C 必须虚拟继承 A。

如何实现一个不能被继承的类(继承无意义的类)

可以考虑,把基类构造函数私有即可(1.显式调用父类调不动 2.默认调用父类也调不动)因此建立派生类对象时,建立不成功。

当然C++11引入了final关键字,不允许该类被继承(继承时就会报错)。


继承的总结和反思

1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
3. 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
// 组合
class C
{
public:
	void func()
	{}
protected:
	int _c;
};

class D
{
public:
	void f()
	{
		_c.func();
		//_c._a++;
	}
protected:
	C _c;
	int _d;
};

在D中包含C类。此时D只能访问C类的public成员(前提是C对D可见)

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。( 具体怎么用,就看是has-a还是is-a
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值