C++的多继承和虚继承

目录

  • 多继承的定义和用法
    • 定义多继承
    • 多继承中派生类对象的内存布局
    • 访问基类成员
    • 多继承带来的问题
  • 虚继承
    • 虚继承的语法
    • 虚继承对象的内存布局
    • 虚继承中的构造
    • 虚继承的缺点

多继承的定义和用法

C++支持多继承,即一个派生类可以有多个基类。

很多时候,单继承就可以满足开发需求,但在特定的情况下就不行。比如有两个类A和B,现在要有一个类C,它同时具有A和B的属性和行为,这种情况下单继承就不能满足要求。

用鸭嘴兽来举例:

从形态学上来讲,鸭嘴兽应该属于鸟类,原因是鸭嘴兽具有扁平的、像鸭子一样的嘴巴,而且是角质的,不像哺乳动物那种肉质的口唇,关键是鸭嘴兽通过下蛋来繁殖后代,这明显是鸟类的特征。然而,鸭嘴兽也靠乳汁来哺育幼仔,浑身密布着浓褐色的短兽毛,这又是哺乳动物的重要特征。所以鸭嘴兽既是鸟类,又是哺乳动物。

如果要在程序中定义一个鸭嘴兽类,采用单继承肯定是不行的。否则,鸭嘴兽要么是鸟类,要么是哺乳动物,显然不符合实际情况。所以此时应当采用多继承,让鸭嘴兽同时继承鸟类和哺乳动物类的属性和行为。这样,一个鸭嘴兽就既是鸟类,又是哺乳动物,符合实际情况。

定义多继承

多继承的语法同单继承类似,只需要在定义类时在类名后面依次罗列继承方式和基类即可。继承方式同单继承一样,也有public,protected和private。在多继承中,针对不同的基类可以使用不同的继承方法。其语法如下:

      class  派生类名 : 继承方式1  基类名1,继承方式2    基类名2,⋯⋯{派生类新增成员};

多继承的类图如下:
在这里插入图片描述例如:定义一个鸭嘴兽类,应该继承鸟类和哺乳动物类

#include <iostream>// 鸟类
class Bird
{
public:Bird(){std::cout << "鸟类的构造函数" << std::endl;}~Bird(){std::cout << "鸟类的析构函数" << std::endl;}
};// 哺乳动物类
class Mammal
{
public:Mammal(){std::cout << "哺乳动物类的构造函数" << std::endl;}~Mammal(){std::cout << "哺乳动物类的析构函数" << std::endl;}
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:Duckbill(): Mammal(), Bird(){std::cout << "鸭嘴兽类的构造函数" << std::endl;}~Duckbill(){std::cout << "鸭嘴兽类的析构函数" << std::endl;}
};void Test()
{Duckbill duckbill;
}int main()
{Test();system("pause");return 0;
}

vs2022下的运行结果:

鸟类的构造函数
哺乳动物类的构造函数
鸭嘴兽类的构造函数
鸭嘴兽类的析构函数
哺乳动物类的析构函数
鸟类的析构函数

同定义单继承派生类的构造函数一样,定义多继承派生类时也要注意基类的初始化。如果基类没有默认的构造函数,那么在派生类构造函数的初始化列表里就要依次调用各个基类的构造函数。无论开发者如何安排,基类构造函数的调用次序总是按照其定义时的次序。

我们在初始化列表中先调用哺乳动物的构造函数,再调用鸟类的构造函数,运行结果按照定义时的顺序进行,析构函数相反。

多继承派生类对象在析构时按照与构造相反的顺序进行,即先调用派生类自己的析构函数,再析构各个数据成员,然后按照相反的顺序,依次调用各个基类的析构函数

多继承中派生类对象的内存布局

同单继承一样,通过多继承派生类将拥有基类所有的属性和行为。在多继承派生类的对象中,将依次排列各个基类的非静态数据成员以及派生类新增的数据成员。派生类对象内存中的数据是按照定义时的顺序排列的。也就是说,在定义派生类时,排在前面的基类,其数据在派生类对象中也排在前面。

一个多继承类图如下:
在这里插入图片描述
它的派生类的内存布局如下:

在这里插入图片描述举例说明:

#include <iostream>// 鸟类
class Bird
{
public:Bird(){std::cout << "鸟类的构造函数" << std::endl;}~Bird(){std::cout << "鸟类的析构函数" << std::endl;}char a;int b;char c;
};// 哺乳动物类
class Mammal
{
public:Mammal(){std::cout << "哺乳动物类的构造函数" << std::endl;}~Mammal(){std::cout << "哺乳动物类的析构函数" << std::endl;}private:char a;char c;char b;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:Duckbill(): Mammal(), Bird(){std::cout << "鸭嘴兽类的构造函数" << std::endl;}~Duckbill(){std::cout << "鸭嘴兽类的析构函数" << std::endl;}
};void Test()
{Duckbill duckbill;std::cout << "Bird: " << sizeof(Bird) << std::endl;std::cout << "Mammal: " << sizeof(Mammal) << std::endl;std::cout << "Duckbill: " << sizeof(Duckbill) << std::endl;
}int main()
{Test();system("pause");return 0;
}

vs2022运行结果:

鸟类的构造函数
哺乳动物类的构造函数
鸭嘴兽类的构造函数
Bird: 12
Mammal: 3
Duckbill: 16
鸭嘴兽类的析构函数
哺乳动物类的析构函数
鸟类的析构函数

Duckbill的内存布局是

	char a;int b;char c;char a;char c;char b;

按照结构体的内存对齐方式计算得出结果为16,而不是12+3。

派生类对象也可以转换为其基类类型的对象。对于多继承的情况,在转换时编译器可以根据要转换的类型进行适当的转换。例如,对于上面的多继承类,如果要将Derived类对象转换成Base2类的对象,编译器会从Derived对象中按照内存排列的顺序,从中截取出从Base2类继承来的部分构成新对象。

 Derived  d;Base2  b2 = static_cast<Base2>( d );

在这里插入图片描述举例说明:
将上述例子中的duckbill转换成基类Mammal对象

Mammal mammal = static_cast<Mammal>(duckbill);
std::cout << "Mammal: " << sizeof(mammal) << std::endl;	// Mammal: 3

访问基类成员

在多继承中,如果多个基类拥有同名成员,那么在访问基类成员时,仅通过成员名并不能区分是哪个基类的成员。解决的方法是在成员名前用域运算符::指明成员所属的基类。通过这种方法访问数据成员和函数成员的语法如下:

      基类名 :: 数据成员名;                          // 在派生类成员函数中访问基类成员数据基类名 :: 函数成员名( 参数列表 );              // 在派生类成员函数中访问基类成员函数派生类对象 . 基类名 :: 数据成员名;派生类对象 . 基类名 :: 函数成员名( 参数列表 );派生类指针 -> 基类名 :: 数据成员名;派生类指针 -> 基类名 :: 函数成员名( 参数列表 );

前提是基类的成员变量是公有变量

举例:

duckbill.Mammal::a = 'a';
duckbill.Bird::a = 'A';void Duckbill::Foo()
{Mammal::a = 'a';Bird::a = 'A';
}

多继承带来的问题

多继承虽然功能强大,可以让派生类同时具有多个基类的属性和行为,但是多继承同时也会带来一些严重的问题。其中比较常见的问题就是多继承会导致数据重复,并由此带来数据不一致的问题。

在这里插入图片描述举例:

#include <iostream>// 动物类
class Animal
{
public:int weight;
};// 鸟类
class Bird: public Animal
{
public:char a;int b;char c;
};// 哺乳动物类
class Mammal: public Animal
{
public:char a;char c;char b;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:};void Test()
{Duckbill duckbill;std::cout << "Bird: " << sizeof(Bird) << std::endl;std::cout << "Mammal: " << sizeof(Mammal) << std::endl;std::cout << "Duckbill: " << sizeof(Duckbill) << std::endl;}int main()
{Test();system("pause");return 0;
}

运行结果:

Bird: 16
Mammal: 8
Duckbill: 24

很明显,Duckbill中的weight有两份。

比较典型的情况是一个派生类D从两个基类B和C中派生,而这两个基类又有一个共同的基类A,这就会导致A的数据在D中被重复两次,如图16-7所示。D多继承B和C,将B和C的数据复制到D中。由于A的数据已经分别被B和C继承,所以A的数据在D中将重复两次。而且在定义D类的成员函数时,或者通过D类对象和指针访问成员数据a时,必须用域运算符::指明a所在的类,即:

    B::a = 1;               // 在D的成员函数中访问A类的数据成员C::a = 2;D  dObj;                // D类对象dObj.B::a = 3;          // 通过D类对象访问A类的数据成员dObj.C::a = 4;D  *pObj = new D();     // D类指针pObj->B::a = 5;         // 通过D类指针访问A类的数据成员pObj->C::a = 6;

从编译器的设计角度来讲,当D从B和C继承时并不知道基类A的存在。D只能全盘接受来自B和C的数据,而无法区分其中的数据a到底是从B继承而来的,还是从C继承而来的。所以要访问数据a,只能由用户来指明。

从逻辑的角度来讲,在D类的对象中A的数据应当只有一份。比如有一个动物基类Animal,它具有重量属性。鸟类(Bird)和哺乳动物类(Mammal)都从Animal派生,然后鸭嘴兽类(DuckBill)又从鸟类和哺乳动物类派生。从继承的语义来讲,一个DuckBill对象也是一个Animal,所以鸭嘴兽应当具有重量属性。但是,由于多继承导致数据冗余,所以基类的一份数据,在其间接派生类中产生了多份副本。所以在上述的鸭嘴兽对象中将具有“两”个重量属性。这显然是不符合逻辑的。而且由于数据冗余,也容易导致数据的不一致。例如上例的D类,其中继承自B的数据a和继承自C的数据a可以分别访问,如果开发者不能始终保证每次修改两个数据使其完全一样,那么就很容易导致数据不一致。

    D  dObj;                // 定义D类对象⋯⋯dObj.B::a = 1;          // 修改继承自B的数据a⋯⋯dObj.C::a = 2;          // 修改继承自C的数据a

显然,在上述代码中很容易导致一个数据a有两个不同值,而这种情况是多继承无法克服的一个缺点。另外,如果A类的构造函数带有参数(而且没有默认构造函数),那么在B类和C类构造时就必须调用这个构造函数。假设由于开发者的疏忽,导致B类和C类在调用A类的构造函数时不一致,那么D类中的两个数据a也就会不一致。

为了解决多继承导致的数据冗余和数据不一致的问题,可以采用虚拟继承机制,也可以禁止最初的基类带有数据。一个不带有任何数据(仅有函数成员)的基类也称做接口。

虚继承

虚拟继承是解决多继承带来的问题的一个重要机制。通过虚拟继承,基类的数据在派生类中将只有一份副本,从而避免了多继承导致的数据冗余和数据不一致问题。

虚继承的语法

虚拟继承是在定义派生类时将基类指明为虚基类,或者说派生类以虚拟的方式从基类派生。虚拟继承的方法是在普通继承的基类名前加上virtual关键字,如下所示:

  class派生类名 : 继承方式  virtual  基类名{派生类的定义};

例如B类从A类虚拟继承,则B类定义如下:

    class  B : public  virtual  A       // B类从A类虚拟继承{⋯⋯private:int b;                          // B类新增的成员数据};

虚继承对象的内存布局

虚继承除了常规的数据成员内存,还会有虚表指针。

#include <iostream>// 动物类
class Animal
{
public:int weight;
};// 鸟类
class Bird: public virtual Animal
{
public:int b;
};// 哺乳动物类
class Mammal: public virtual Animal
{
public:char a;int c;int b;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:};void Test()
{Duckbill duckbill;Bird bird;Mammal mammal;std::cout << "Bird: " << sizeof(bird) << std::endl;std::cout << "Mammal: " << sizeof(mammal) << std::endl;std::cout << "Duckbill: " << sizeof(duckbill) << std::endl;}int main()
{Test();system("pause");return 0;
}

vs2022、x64位运行结果

Bird: 24
Mammal: 32
Duckbill: 48

Duckbill对象的内存布局如下:

在这里插入图片描述

与普通继承不同,在虚拟继承中,派生类对象并不是在其内存中保留一份虚基类数据的副本,而是通过一种间接的引用方式,即将虚基类子对象的数据单独存放,在派生类对象中设置一个指针指向基类子对象。这样,当一个派生类通过多个继承路径继承同一个虚基类时,并不需要产生多个数据副本,而只要维护这个虚基类指针即可。

虚继承中的构造

由于在虚拟继承中,虚基类的数据只有一份,所以在间接派生类构造时需要特殊处理,即只能初始化虚基类一次。

假设Vehicle类有一个带有参数的构造函数(而且没有默认构造函数)Vehicle ::Vehicle(int number),那么在中间派生类(虚拟继承)Tank和Boat的构造函数中都要显式调用Vehicle(int number)。但是在AmphiTank类多继承自Tank类和Boat类之后,如果仍然通过两个基类来初始化Vehicle,那么Vehicle将被初始化两次,从而可能导致数据不一致。

所以在C++中,对于虚基类的初始化进行了特殊处理。**如果是在一级派生中,比如Tank类虚拟继承Vehicle类,那么其初始化同一般继承一样。如果是在多级派生中,那么虚基类的初始化将由最终一级的派生类负责。**所以,在水陆两栖坦克的类层次结构中,虚基类Vehicle的初始化应当由最终一级派生类AmphiTank负责,即Vehicle的构造函数应当放在AmphiTank的初始化列表中。

举例:

#include <iostream>// 动物类
class Animal
{
public:Animal(int _num): weight(_num){}int weight;
};// 鸟类
class Bird: public virtual Animal
{
public:Bird(int _num): Animal(_num), b(_num){}int b;
};// 哺乳动物类
class Mammal: public virtual Animal
{
public:Mammal(int _num): Animal(_num), b(_num){}char a = ' ';int c = 1;int b = 2;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:Duckbill(int animal, int bird, int mammal): Animal(animal), Bird(bird), Mammal(mammal){}
};void Test()
{Duckbill duckbill(1,2,3);// Bird的weightstd::cout << duckbill.Bird::weight << std::endl;// Mammal的weightstd::cout << duckbill.Mammal::weight << std::endl;// Duckbill的weightstd::cout << duckbill.weight << std::endl;}int main()
{Test();system("pause");return 0;
}

vs2022运行结果:

1
1
1

虽然我们在构造Bird和Mammal时用不同的值都构造了Animal,但是只会由最后一级Duckbill负责。

如果一个派生类既有虚基类(不一定是直接基类),又有非虚基类,那么无论初始化列表如何排列,虚基类总是先初始化。如果有多个虚基类,那么排在前面的先初始化。

派生类的析构顺序总是与构造顺序相反,所以如果一个派生类有虚基类,则虚基类总是在最后析构。

虚继承的缺点

虚拟继承虽然可以解决多继承带来的数据冗余和数据不一致的缺点,但虚拟继承本身也存在一些问题,具体问题如下:

◆ 增加内存。为了保证虚基类的数据在派生类中只出现一次,采用虚继承的方式引入了虚基类指针,额外增加了类的占用内存。

◆ 派生类要显式初始化其虚拟基类。通常从开发者的角度来讲,设计一个派生类只要初始化其直接基类即可。但是如果在类的派生层次中存在虚拟基类,那么派生类始终要负责这些虚拟基类的初始化,这在一定程度上导致了设计的复杂化。

多继承容易导致数据冗余和数据不一致,而虚拟继承在解决了这个问题的同时又引入了新的问题。对于类层次结构的设计者来讲,可以采取另外一种方法来解决多继承的问题,即只允许一个基类有数据,其他基类只有方法,这样就消除了数据冗余和数据不一致的问题。只有方法没有数据的类也称做接口。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL日期查询 今天、明天、本月、下月、星期、本周第一天、本周最后一天、本周七天日期

文章目录 今天日期明天日期本月第一天本月最后一天下个月第一天当前月已过几天当前月天数当前月所有日期获取星期本周第一天本周最后一天获取本周的七天日期 今天日期 select curdate()明天日期 select DATE_SUB(curdate(),INTERVAL -1 DAY) AS tomorrow本月第一天 select d…

2023年度总结(找到工作)

转眼2023年结束了&#xff0c;今天已经12月29日了。从2022年12月25日考研失败后&#xff0c;2023年就变成了找工作以及上班度日的时光了。针对2023年&#xff0c;我想对自己所说的是&#xff1a;终于找到工作了。作为一个普通的专升本&#xff0c;考研落榜生来说&#xff0c;能…

JavaScript(简写js)常用事件举例演示

目录 1.窗口事件onblur :失去焦点onfocus:获得焦点onload:窗口加载事件onresize:窗口大小缩放事件 二、表单事件oninput &#xff1a;当文本框内容改变时 &#xff0c;立即将改变内容 输出在控制台onchange&#xff1a; 内容改变事件onclick&#xff1a;鼠标单击时触发此事件 三…

模版匹配历劫之路2-探究空间金字塔对于匹配速度的影响

1 方法一 在合适的金字塔层数上&#xff0c;低步长旋转角度&#xff0c;逐层缩小旋转范围&#xff0c;达到提高匹配速度的效果 金字塔越高&#xff0c;模版越模糊&#xff0c;但是只要模版不会被降级很严重&#xff0c;那么模版的边缘方向不会受到太大的影响。高层级别的金字塔…

Spring系列学习四、Spring数据访问

Spring数据访问 一、Spring中的JDBC模板介绍1、新建SpringBoot应用2、引入依赖&#xff1a;3、配置数据库连接&#xff0c;注入dbcTemplate对象&#xff0c;执行查询&#xff1a;4&#xff0c;测试验证&#xff1a; 二、整合MyBatis Plus1&#xff0c;在你的项目中添加MyBatis …

OCP NVME SSD规范解读-4.NVMe IO命令-2

NVMe-IO-3&#xff1a; 由于设备具有掉电保护功能&#xff08;如Power Loss Protection&#xff0c;PLP&#xff09;&#xff0c;因此在以下情况下&#xff0c;性能不应降低&#xff1a; FUA&#xff08;Force Unit Access&#xff09;&#xff1a;是计算机存储设备中的一种命…

Vue - 使用Element UI Upload / importExcelJs进行文件导入

1 情景一 需求背景&#xff1a;后端配合&#xff0c;点击"导入"按钮&#xff0c;弹出“导入”弹窗&#xff0c;将电脑本地Excel表格数据导入到页面中表格位置&#xff08;需要调用后端接口&#xff09;&#xff0c;而页面中表格通过后端接口获取最新数据。 实现思路…

Factory Method工厂模式(对象创建)

Factory Method&#xff08;对象创建&#xff09; 链接&#xff1a;工厂模式实例代码 解析 目的 在软件系统中&#xff0c;经常面临着创建对象的工作&#xff1b;由于需求的变化&#xff0c;需要创建的对象的具体类型经常变化。 如何应对这种变化&#xff1f;如何绕过常规的…

深眸科技创新工业AI视觉系统,与机械臂协同工作实现视觉引导功能

工业AI视觉系统&#xff1a;工业AI视觉系统能够在工业环境中进行缺陷检测、视觉分拣、物流供包、拆码垛、工业上料等应用。 随着国内工业企业不断进步和发展&#xff0c;传统机器视觉无法满足企业对复杂操作流程的需求&#xff0c;多数制造企业对于机器视觉系统的需求增长。而…

如何优化旋转花键的装配方式?

花键轴与花键套的装配在工业上广泛应用&#xff0c;装配的质量受花键轴与花键套间的接触状态、对应的受力情况及相对位置关系影响&#xff0c;那么&#xff0c;我们应该如何优化旋转花键的装配方式呢&#xff1f; 确保轴和孔的配合精度是关键&#xff0c;可以采用高精度的加工和…

VM虚拟机及WindowsServer2012安装及激活

一 、虚拟机是什么? 虚拟机&#xff08;VM&#xff09;是一种创建于物理硬件系统&#xff08;位于外部或内部&#xff09;、充当虚拟计算机系统的虚拟环境&#xff0c;它模拟出了自己的整套硬件&#xff0c;包括 CPU、内存、网络接口和存储器。通过名为虚拟机监控程序的软件&…

万物简单AIoT物联网平台快速开始

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; 万物简单AIoT物联网提供一站式的AI物联网的学习平台&#xff0c;以及物联网SaaS私有化部署的解决方案。从终端硬件系统、云平台到APP前端的物联网能力&#xff0c;助力企业和开发者的设备具备1分钟快速上云的…