【C++私房菜】面向对象中的多重继承以及菱形继承

news2024/12/23 20:33:30

文章目录

  • 一、多重继承
    • 1、多重继承概念
    • 2、派生类构造函数和析构函数
  • 二、菱形继承和虚继承
    • 2、虚继承后的构造函数和析构函数
  • 三、has-a 与 is-a


一、多重继承

1、多重继承概念

**多重继承(multiple inheritance)**是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。尽管看上去与单继承没有什么区别,但是多个基类交织混合产生的细节会带来错综复杂的设计问题与实践问题。

我们在此再一次对单继承和多继承的概念进行阐述:

  • 单继承:一个派生类只有一个直接基类。
  • 多继承:一个派生类有两个或以上直接基类。

多重继承时,在派生类的派生列表中可以包含多个基类。和单继承相同,多重继承的派生列表页只能包含已经被定义过的类,而且这些类不能是 final 的。要注意的是每个基类都要包含一个访问说明符,举例说明:

// 抽象基类 ZooAnimal
class ZooAnimal {
public:
    ZooAnimal() = default;
	ZooAnimal(const string& name, int age)
		: _name(name), _age(age) {}
	virtual void eat() = 0; // 纯虚函数,需要在派生类中实现
	const string& getName() const { return _name; }
	int getAge() const { return _age; }
protected:
	string _name;
	int _age;
};

// 类 Bear 继承自 ZooAnimal
class Bear : public ZooAnimal {
public:
    Bear() = default;
	Bear(const string& name, int age, const string& furColor)
		: ZooAnimal(name, age), _furColor(furColor) {}
	void eat() override { cout << "Bear " << _name << " is eating." << endl; }
	const string& getFurColor() const { return _furColor; }
private:
	string _furColor;
};

// 派生类 Panda 继承自 Bear 和 Endangered
class Panda : public Bear, public Endangered {
public:
	Panda(const string& name, int age, const string& furColor, int conservationStatus)
		: Bear(name, age, furColor), Endangered(conservationStatus) {}

	void eat() override {
		cout << "Panda " << _name << " is eating bamboo." << endl;
	}
};

// 辅助类 Endangered
class Endangered {
public:
	 Endangered(int conservationStatus)
		: _conservationStatus(conservationStatus) {}
	int getConservationStatus() const { return _conservationStatus; }
private:
	int _conservationStatus;
};

int main() {
	Bear bear("Brown Bear", 5, "Brown");
	bear.eat();
	cout << "Fur Color: " << bear.getFurColor() << endl;
	Panda panda("Giant Panda", 3, "Black and White", 2);
	panda.eat();
	cout << "Fur Color: " << panda.getFurColor() << endl;
	cout << "Conservation Status: " << panda.getConservationStatus() << endl;
	return 0;
}

抽象类和纯虚函数我将在后续关于多态的文章进行详细叙述。此处为了方便叙述,我直接进行使用。

  • ZooAnimal 类是一个抽象基类,包含动物的名称和年龄,并声明了一个纯虚函数 eat(),需要在派生类中实现。同时提供了获取名称和年龄的方法。
  • Bear 类继承自 ZooAnimal 类,表示一种熊,包含了毛色信息(furColor),实现了 eat() 函数并返回熊正在进食的信息。提供了获取毛色的方法。
  • Endangered 类是一个辅助类,用于表示濒危物种,包含了保护等级信息(conservationStatus),并提供了获取保护等级的方法。
  • Panda 类继承自 Bear 类和 Endangered 类,表示一种熊猫,构造函数中初始化了熊猫的名称、年龄、毛色和保护等级信息。重写了 eat() 函数,输出熊猫正在吃竹子的信息。

在这里插入图片描述


2、派生类构造函数和析构函数

在此我要提一下在【C++私房菜】面向对象中的简单继承-CSDN博客文章中没有提到的部分,构造一个派生类将同时构造并初始化它的所有基类子对象。与从一个基类进行的派生一样,多重继承的派生类的构造函数初始值只能初始化它的直接基类:

//显示地初始化所有基类
Panda::Panda(const string& name, int age, const string& furColor, int conservationStatus)
		: Bear(name, age, furColor), Endangered(conservationStatus) {}

//隐式地使用Bear的默认构造函数初始化Bear子对象
Panda::Panda()
    	:Endangered() {}

派生类的构造函数初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。一个Panda对象按照如下次序进行初始化:

  1. ZooAnimal是整个继承体系的最终基类,BearPanda的接基类ZooAnimalBear的基类,所以首先初始化ZooAnimal。。接下来初始化Panda的第一个直接基类Bear
  2. 接下来初始化Panda的第一个直接基类Bear
  3. 然后初始化 Panda的第二个直接基类Endangered
  4. 最后初始化 Panda

派生类析构函数也和往常一样,派生类的析构函数只负责清理派生类本身分配的资源,清理完自己后调用基类的析构函数。

析构函数的调用顺序刚好与构造函数相反,在上述例子中,析构函数调用顺序是 ~Panda~Endangered~Bear~ZooAnimal

需要注意的是,与只有一个基类的继承是一样的,对象、指针和引用的静态类型决定了我们能够使用哪些成员。如果我们使用 ZooAnimal指针,则只有定义在 ZooAnimal中的操作是可以使用的,Panda接口中的BearPandaEndangered特有的部分都不可见。类似的,一个Bear类型的指针或引用只能访问BearZooAnimal的成员,一个Endangered的指针或引用只能访问Endangered的成员。


二、菱形继承和虚继承

1、引入菱形继承和虚继承

菱形继承是多继承的一种特殊情况。我们观察如下代码:

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;
}

在这里插入图片描述

从图中可以明显观察出 D d 对象中存储了两份 _a,造成了数据冗余和二义性的问题。在此对象中A对象存了两份。

尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。

例如IO库的 istreamostream分别继承了一个共同的名为 ios_base 的抽象基类。该抽象基类负责保存流的缓存内容并管理流的条件状态。它提供了从输入设备(如键盘、文件)读取数据的功能。

通过继承 ios_baseistreamostream 类可以共享和继承 ios_base 中定义的功能和状态,以便更方便地进行输入和输出操作。

iostream是另外一个类,它从istreamostream直接继承而来,可以同时读写流的内容。因为istreamostream 都继承自 base_ios,所以 iostream 继承了 base_ios 两次,一次是通过istream,另一次是通过ostream
在默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。
这种默认的情况对某些形如 iostream 的类显然是行不通的。一个 iostream 对象肯定希望在同一个缓冲区中进行读写操作,也会要求条件状态能同时反映输入和输出操作的情况。假如在 iostream 对象中真的包含了base_ios的两份拷贝,则上述的共享行为就无法实现了,导致菱形继承。

为了避免菱形继承,在 C++语言中我们通过虚继承(virtual inheritance)的机制解决上述问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类(virtualbase class)。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

在这里插入图片描述

我们再次讨论上述代码:在这里插入图片描述

观察这个新的继承体系,我们将发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。例如在我们的类中,当我们定义D时才出现了对虚派生的需求,但是如果 BC 不是从 A 虚派生得到的,那么D的设计者就显得不太幸运了。

在实际的编程过程中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个是虚基类,况且新基类的开发者也无法改变已存在的类体系。

虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。

我们使用虚基类修改上述代码,我们将 BC的虚基类定义为 A 。virtual说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例:

class A{
public:
	int _a;
};
class B: virtual public A{	//此处关键字public和virtual的顺序随意
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只存储一份,但继承的BC中有一段未知的地址,我们在对其进行考察(图中类的存放顺序与类的声明顺序相同):

在这里插入图片描述

这里是通过了B和C的两个指针,指向的一张表 。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。即上图中 0x007a7bdc为B的虚基表指针,0x007a7be4为C的虚基表指针。


2、虚继承后的构造函数和析构函数

在这里插入图片描述

假设我们拥有如上各类代码,且构造继承关系。在虚派生中,虚基类是由最低层的派生类初始化的。即创建 Panda对象时,由 Panda的构造函数独自控制 ZooAnimal的初始化过程。

在此例中,虚基类将会在多条继承路径上被重复初始化。以ZooAnimal为例,如果应用普通规则,则 RaccoonBear 都会试图初始化 Panda对象的 ZooAnimal 部分。
当然,继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。只要我们能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。例如在我们的继承体系中,当创建一个Bear(或Raccoon)的对象时,它已经位于派生的最低层,因此Bear(或Raccoon)的构造函数将直接初始化其ZooAnimal基类部分。

含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。当我们创建一个 Panda 对象时:

  • 首先使用 Panda的构造函数初始值列表中提供的初始值构造虚基类 ZooAnimal部分。

  • 接下来构造 Bear部分。

  • 然后构造 Raccoon 部分。

  • 然后构造第三个直接基类Endangered

  • 最后构造 Panda 部分。

如果 Panda 没有显式地初始化 ZooAnimal基类,则 ZooAnimal 的默认构造函数将被调用。如果ZooAnimal没有默认构造函数,则代码将发生错误。

⚠️虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。

一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。编译器会按照直接基类的声明顺序对其进行检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按照声明的顺序逐一构造其他非虚基类。

当然与往常一样,对象的析构顺序与构造顺序正好相反。

三、has-a 与 is-a

面向对象系统中功能复用的两种最常用技术是类继承对象组合(object composition)。正如我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。

但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。

对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承。

从概念上来说,多重继承是继承是一种白箱复用,组合是一种黑箱复用。白指看得见内部,黑指看不见内部。从概念上课,本文的多重继承十分简单:一个派生类可以从多个直接基类继承而来。在派生类对象中既包含派生类部分,也包含与每个基类对应的一类部分。虽然看起来很简单,但实际上多重继承的细节非常复杂。特别是对多个基类的继承可能会引入新的名字冲突并造成来自于基类部分的名字的二义性问题。
如果一个类是从多个基类直接继承而来的,那么有可能这些基类本身又共享了另一个基类。在这种情况下,中间类可以选择使用虚继承,从而声明愿意与层次中虚继承同一基类的其他类其享虚基类。用这种方法,后代派生类中将只有一个共享虚基类的副本。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1465942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SpringBoot的产业园区智慧公寓管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

电路设计(26)——速度表的multisim仿真

1.设计要求 设计一款电路&#xff0c;能够实时显示当前速度。 用输入信号模拟行驶的汽车&#xff0c;信号频率的1hz代表汽车速度的1m/s。最后速度显示&#xff0c;以km/h为单位。 2.电路设计 当输入信号频率为40HZ时&#xff0c;显示的速度应该为144KM/h&#xff0c;仿真结果为…

IDEA Debug框的 show execution point按钮没了

在这里右键&#xff1a; Add Action&#xff1a; 搜索添加&#xff1a; 本文由博客一文多发平台 OpenWrite 发布&#xff01;

[OpenAI]继ChatGPT后发布的Sora模型原理与体验通道

前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff1a;https://www.captainbed.cn/z ChatGPT体验地址 文章目录 前言OpenAI体验通道Spacetime Latent Patches 潜变量时空碎片, 建构视觉语言系统…

【SelectIO】bitslice原语学习记录

基本概念 在Ultrascale (plus)系列上的FPGA中&#xff0c;Xilinx引入了bitslice硬核&#xff0c;它取代了7系列上的IDELAYCTRL/IODELAY/IOSERDES/IODDR系列硬核&#xff0c;用于为HP&#xff08;High Performance&#xff09;类型Bank上的IO接口提供串并转化、信号延时、三态控…

制造业客户数据安全解决方案(数据防泄密需求分析)

机械行业是历史悠久的工业形式&#xff0c;与国民经济密切相关&#xff0c;属于周期性行业&#xff0c;是我国最重要的工业制造行业之一。即使网络经济与IT信息技术在世界范围内占据主导地位&#xff0c;依然离不开一个发达的、先进的物质基础&#xff0c;而机械行业正是为生成…

基于springboot+vue的植物健康系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

小迪安全26WEB 攻防-通用漏洞SQL 注入 SqlmapOracleMongodbDB2 等

#知识点&#xff1a; 1、数据库注入-Oracle&Mongodb 2、数据库注入-DB2&SQLite&Sybase 3、SQL 注入神器-SQLMAP 安装使用拓展 数据库注入&#xff1a; 数据库注入-联合猜解-Oracle&Mongodb 1.Oracle数据库一般会在java上执行 参考:https://www.cnblog…

有ai换脸证件照的工具吗?分享3款好用的工具!

在当今数字化时代&#xff0c;随着人工智能技术的飞速发展&#xff0c;AI换脸技术已经成为了一种热门的应用。从电影特效到日常生活中的照片处理&#xff0c;AI换脸技术都为我们带来了前所未有的便捷和乐趣。而在这其中&#xff0c;AI换脸证件照工具更是受到了广大用户的青睐。…

jetson nano——安装archiconda

目录 1.archiconda3我在这提供了下载链接&#xff0c;点解下面链接即可1.看好文件所在位置&#xff0c;如果装错了&#xff0c;那么环境变量的路径自己进行相应的修改。2.添加环境变量 2.可能部分伙伴输入一些激活&#xff0c;啥的命令激活不了&#xff0c;那么输入下面这些代码…

如何在nginx增加健康检查接口

在docker中部署的nginx或者在nginx部署的nginx一般是需要一个健康检查接口的 这样的话&#xff0c;就可以确定容器当前的状态是否是健康的 那么&#xff0c;如何给nginx增加一个健康检查的接口呢&#xff1f; 接下来呢&#xff0c;我们就演示一个在nginx中如何增加健康检查的…

瑞_23种设计模式_装饰者模式

文章目录 1 装饰者模式&#xff08;Decorator Pattern&#xff09;1.1 介绍1.2 概述1.3 装饰者模式的结构 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 JDK源码解析5 总结5.1 装饰者模式的优缺点5.2 装饰者模式的使用场景5.3 装饰者模式 VS 代理模式 &#x…

Druid无法登录监控页面

问题表现&#xff1a;在配置和依赖都正确的情况下&#xff0c;无法通过配置的用户名密码登录Druid的监控页面 检查配置发现 配置的用户名和密码和请求中参数是一致的&#x1f914; Debug发现 ResourceServlet 是Druid的登录实现&#xff0c; 且调试发现usernameParam是null&am…

Day17_集合与数据结构(链表,栈和队列,Map,Collections工具类,二叉树,哈希表)

文章目录 Day17 集合与数据结构学习目标1 数据结构2 动态数组2.1 动态数组的特点2.2 自定义动态数组2.3 ArrayList与Vector的区别&#xff1f;2.4 ArrayList部分源码分析1、JDK1.6构造器2、JDK1.7构造器3、JDK1.8构造器4、添加与扩容5、删除元素6、get/set元素7、查询元素8、迭…

【安卓基础5】中级控件

&#x1f3c6;作者简介&#xff1a;|康有为| &#xff0c;大四在读&#xff0c;目前在小米安卓实习&#xff0c;毕业入职 &#x1f3c6;本文收录于 安卓学习大全持续更新中&#xff0c;欢迎关注 &#x1f3c6;安卓学习资料推荐&#xff1a; 视频&#xff1a;b站搜动脑学院 视频…

二进制部署k8集群,搭建单机matser和etcd集群

单机matser预部署设计 组件部署&#xff1a; mater节点 mater01 192.168.66.10 kube-apiserver kube-controller-manager kube-scheduler etcd node节点 node01 192.168.66.30 kubelet kube-proxy docker &…

在 Windows 上使用 VC++ 编译 OpenSSL 源码的步骤

在 Windows 上使用 VC 编译 OpenSSL 源码的步骤如下&#xff1a; 准备工作 安装 Visual Studio 2017 或更高版本。安装 Perl 脚本解释器。安装 NASM 汇编器。 编译步骤 下载 OpenSSL 源码。解压 OpenSSL 源码。打开命令行工具&#xff0c;并进入 OpenSSL 源码目录。运行以下…

Linux常见指令(2)

目录 1、tar指令 &#xff01; 2、bc指令 3、uname 4、重要热键 5、关机 1、tar指令 &#xff01; 功能&#xff1a;压缩/解压缩文件或目录,类似zip 我们先来看一下我们的文件即目录&#xff0c;接下来我们输入指令&#xff1a; tar -czf test.tgz test 压缩 -c &#xf…

Linux调试器——gdb的基础使用

目录 1.背景 2.指令的使用 2.1gdb的使用和退出 2.2显示源代码 2.3运行程序 2.4调试 1.打断点 2.查断点 3.去断点 4.运行 5.关闭断点 6.启用断点 7.逐过程 8.进入函数 9.显示变量的值 1.背景 众所周知&#xff0c;我们的程序发布有两种&#xff0c;分别是debug模式和release模式…

【Azure 架构师学习笔记】- Azure Databricks (10) -- UC 使用

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (9) – UC权限 在前面的文章&#xff1a;【Azure 架构师学习笔记】- Azure Databricks (6) - 配置Unity Catalog中演示了如何配置一个UC。 本文…