目录
前言
复杂的菱形继承及菱形虚拟继承
继承方式
virtual关键字
虚拟继承的原理
原理:
额外消耗:
构造顺序为什么是ABCD
不允许使用间接非虚拟基类原理
假设只有A B
为什么virtual加在B C中而不是D中?
如何实现一个不能被继承的类(继承无意义的类)
编辑继承的总结和反思
前言
上文已经完成了前六种继承知识的介绍,下面是继承的复杂情况介绍。本文将着重介绍复杂的菱形继承与虚继承
复杂的菱形继承及菱形虚拟继承
继承方式
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的缺陷:存在数据的冗余和二义性的问题。那如何解决这几个问题呢?
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++中用于解决多继承时可能出现的菱形继承问题(即一个类多次继承自同一个基类)的一种机制。虚拟继承允许在继承层次中共享基类的一个实例,而不是为每个直接或间接的基类副本都保留一份基类实例。以下是虚拟继承产生额外消耗的原因及其原理:
原理:
-
共享基类实例: 在虚拟继承中,无论基类在继承层次中被继承多少次,都只存在一个共享的基类实例。这意味着所有继承了这个虚拟基类(被virtual继承的类)的派生类都会引用同一个基类部分。
-
虚基类表: 为了实现这种共享,编译器会为每个含有虚基类的对象插入一个额外的指针,这个指针指向一个虚基类表(vtable)。虚基类表包含了虚基类的偏移量信息,用于在运行时调整指针,使得能够正确地访问共享的基类实例。
-
调整: 当通过指针或引用访问虚基类成员时,编译器生成的代码会使用虚基类表来调整指针,确保指向正确的共享基类实例。
(
额外消耗:
-
空间开销:
- 虚基类指针:每个含有虚基类的对象都需要额外的空间来存储指向虚基类表的指针。
- 虚基类表:需要额外的存储空间来维护每个对象的虚基类偏移信息。
-
时间开销:
- 访问调整:每次访问虚基类的成员时,都需要进行指针调整,这增加了访问时间。
- 构造和析构:在构造和析构过程中,需要确保虚基类部分只被初始化和清理一次,这可能导致更复杂的构造函数和析构函数调用序列,从而增加时间开销。
-
复杂性开销: 虚拟继承增加了编译器实现的复杂性,可能导致生成的代码更加复杂,这可能会间接影响程序的性能。
总之,虚拟继承虽然解决了菱形继承问题,但其机制带来的额外空间和时间开销,以及在复杂性和性能上的潜在影响,使得程序员在考虑使用虚拟继承时需要权衡其利弊。在设计继承体系时,如果可以避免,通常推荐不使用虚拟继承。
构造顺序为什么是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关键字,不允许该类被继承(继承时就会报错)。
继承的总结和反思
// 组合
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可见)