【C++从0到王者】第二十二站:一文讲透多继承与菱形继承(上)

简介: 【C++从0到王者】第二十二站:一文讲透多继承与菱形继承

前言

在我们前面所说的继承其实在C++中也叫做单继承

即一个子类只有一个直接父类的时候称这个继承关系为单继承


一、多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承

多继承即认为一个对象可能同时有其他两个或以上对象的属性所设计出来的。

class Student
{
protected:
  int _num; //学号
};
class Teacher
{
protected:
  int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
  string _majorCourse; // 主修课程
};
int main()
{
  Assistant at;
  return 0;
}

如上代码所示,Assistant继承了Student,Teacher两个类的属性

二、菱形继承

虽然多继承看似很合理,但是多继承引发了一种新的问题——菱形继承

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

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份

菱形继承导致的问题也正是如此。如下代码所示,就会产生二义性

此处的二义性,我们还可以通过类域指定访问去处理

监视显示为

还有一种情形要注意:我们如果指定的是父类的父类的话,编译器是可以通过的

但是此时的结果究竟指向哪个Student里的父类还是Person的父类呢?其实是跟继承的顺序有关的,我们写多继承的时候先是继承了Student,所以Student里面的Perosn中的_name会被修改为王五

三、菱形虚拟继承

在前面,我们得知了由多继承导致的菱形继承中的一些问题:数据冗余和二义性

为了解决这个问题,C++又出来了一个菱形虚拟继承

菱形虚拟继承只需要在菱形继承中的腰部位置(即Student和Teacher类)添加关键词virtual即可

有了菱形虚拟继承,我们可以不需要指定类域去访问_name,我们的Person在监视窗口看好像是存了三份,并且我们修改数据的时候三份同时进行修改。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

四、菱形虚拟继承的底层原理

我们用如下代码进行研究

class A
{
public:
  int _a;
};
class B : public A
{
public:
  int _b;
};
class C : public A
{
public:
  int _c;
};
class D : public B, public C
{
public:
  int _d;
};
int main()
{
  D d;
  d.B::_a = 1;
  d.C::_a = 2;
  d._b = 3;
  d._c = 4;
  d._d = 5;
  return 0;
}

上面是一个普通的菱形继承,它的运行结果应为如下所示

这里我们要注意,我们看似这里好像是给包了起来。实际中是连续的内存进行存放的。

如上是菱形继承的底层内存分布

接下来看菱形虚拟继承的底层分布

我们还是上面的例子,只不过将其改为菱形虚拟继承。

class A
{
public:
  int _a;
};
class B : virtual public A
{
public:
  int _b;
};
class C : virtual public A
{
public:
  int _c;
};
class D : public B, public C
{
public:
  int _d;
};
int main()
{
  D d;
  d.B::_a = 1;
  d.C::_a = 2;
  d._b = 3;
  d._c = 4;
  d._d = 5;
  return 0;
}

这样的话,我们从监视窗口看好像是有三份,其实不然,这里并非三份。而是一份

我们从内存窗口可以更加精确的观测到内存的变化

我们发现,a似乎只有一份,且对象模型发生非常大的变化

也就是说,现在的A既不在B也不在C。这里倒是还可以理解,因为为了解决数据冗余二义性,它需要放到其他位置上,具体方最上面还是最下面是取决于编译器自己规定的

但是B和C里面似乎又多了一些东西,这些东西又是什么呢?如下图青色部分所示

我们从模样上来看,这两个有点像指针

于是我们从内存中找到这里指向的位置,如下所示(注意是小端机器)

我们可以注意到,这个指针指向的位置存的是一个0,然后下一个位置存储的是一个有效值

其中,前者为20,后者为12我们不难注意到以下关系。他们距离A的地址正好相差这么多

所以这里指针存储的是距离A的偏移量。那么现在可能我们会好奇,为什么要搞一个指针,不直接存的。直接存不可以吗?其实是可以的。但是为什么我们编译器没有这样做呢?

我们注意到我们的偏移量是存在第二个里面,第一个里面是0,这个0是为其他值预留的。如果还有其他值的话,可以直接存进来。 因为菱形虚拟继承的,可能不止这一个。

而且这里仅仅只是一个类型,我们可能要实例化很多个对象,每个对象如果都是直接存储的话,那么代价太大了,不如直接开辟一个空间将偏移量全部放进去,然后每个对象只有一个指针指向即可。

如下就是两个对象的时候,他们都是存储相同的指针

这里的映射,我们有时候也称之为虚基表(寻找基址偏移量的表)

而且在如下的场景下,我们的这里是菱形虚拟继承的情况下,不仅仅是D的对象模型是前面的样子,B的对象模型也是类似于D的,它有一个指针指向一个虚基表,然后存储一个偏移量,根据这个偏移量就可以找到A的部分

以及类似的下面指针的情形,切割的场景,也是类似的,这个指针指向的仅仅只有B的那一部分,pb指针是切割出来的。为了能够找到对应的_a,必须通过偏移量进行寻找

,不通过偏移量编译器控制不住

还有如下所示的样例

int main()
{
  B b;
  b._a = 1;
  b._b = 2;
  D d;
  d._a = 1;
  B* pb = &b;
  pb->_a++;
  pb = &d;
  pb->_a++;
  return 0;
}

后面的四行代码想必我们都已经了解了,由于此处是菱形虚拟继承,所以pb在寻找a的时候会先通过偏移量来进行找到的,之后我们将pb接收d的地址,此处就是子类给父类的切割了。就是我们上面的样例了。也就是说他们寻址的底层都是一样的。都是通过偏移量去找到的,我们看下面的汇编,也会发现是一模一样的

也就是说,在这里的情景就是无论如何,编译器始终先取到偏移量,然后计算出_a的地址,最后在访问。

五、菱形虚拟继承对于空间的优化

相关文章
|
12天前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
55 6
|
2月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
68 16
|
2月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
71 5
|
4月前
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
48 1
【C++】继承
|
1月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
9天前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
37 16
|
1天前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
1天前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
1月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1天前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!

热门文章

最新文章