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.mzph.cn/news/578879.shtml

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

相关文章

效果图渲染电脑渲染好?还是云渲染更好?

效果图的渲染是建筑和室内设计领域中不可或缺的一步&#xff0c;随着技术的发展&#xff0c;云渲染作为一项新技术&#xff0c;正逐渐受到人们关注。今天&#xff0c;让我们深入探讨电脑渲染和云渲染这两种方法的优缺点以及它们的适用场景。 本地电脑渲染 本地电脑渲染是利用用…

往年面试精选题目(前50道)

常用的集合和区别&#xff0c;list和set区别 Map&#xff1a;key-value键值对&#xff0c;常见的有&#xff1a;HashMap、Hashtable、ConcurrentHashMap以及TreeMap等。Map不能包含重复的key&#xff0c;但是可以包含相同的value。 Set&#xff1a;不包含重复元素的集合&#…

库函数atoi的功能及模拟实现

atoi函数的功能 int atoi(const char * str) 参数是字符指针&#xff0c;函数值是转换后的int型数据。使用时要包含头文件stdlib.h。 atoi函数的功能是&#xff1a;跳过不可见(空白)字符(如空格、换页\f、换行\n、回车\r、制表符\t、垂直制表符\v)&#xff0c;碰到正负号或…

在k8s中使用cert-manager部署gitlab集群

写在前面的话&#xff1a;前面有详细的分享过在k8s集群中部署gitlab&#xff0c;不过当时使用gitlab的访问证书是阿里云上免费的ssl证书&#xff0c;今天特意专门介绍下另外一种基于cert-manager发布自签证书的方式实现部署gitlab到k8s集群中。 往期gitlab部署系列如&#xff1…

麒麟信安桌面操作系统顺利上线长沙职业技术学院,深度促进产教融合,赋能信创人才培养

随着信息基础设施国产化进程的加快&#xff0c;信息技术创新产业对人才的需求量激增&#xff0c;为解决信创人才培养难题、深度促进产教融合&#xff0c;近日&#xff0c;麒麟信安、湖南欧拉生态创新中心携手长沙职业技术学院共同组建的“麒麟信安&欧拉(openEuler)国产操作…

【小黑嵌入式系统第十三课】PSoC 5LP第二个实验——中断控制实验

上一课&#xff1a; 【小黑嵌入式系统第十二课】μC/OS-III程序设计基础&#xff08;二&#xff09;——系统函数使用场合、时间管理、临界区管理、使用规则、互斥信号量 文章目录 1 实验目的2 实验要求3 实验设备4 实验原理4.1 中断(1) 中断机制概述(2) 中断源(3) 中断系统的功…

鸿蒙开发语言介绍--ArkTS

1.编程语言介绍 ArkTS是HarmonyOS主力应用开发语言。它在TypeScript (简称TS)的基础上&#xff0c;匹配ArkUI框架&#xff0c;扩展了声明式UI、状态管理等相应的能力&#xff0c;让开发者以更简洁、更自然的方式开发跨端应用。 2.TypeScript简介 自行补充TypeScript知识吧。h…

鸿蒙列表,item组件封装传参问题?@ObjectLink 和@Observerd

鸿蒙列表渲染&#xff0c;封装内容组件&#xff0c;进行item传参会报错&#xff1f; class FoodClass {order_id: number 0food_name: string ""food_price: number 0food_count: number 0 }Entry Component struct Demo07 {State message: string Hello World…

ElasticSearch 架构设计

介绍 ElasticSearchMySQLIndexTableDocumentRowFieldColumnMappingSchemaQuery DSLSQLaggregationsgroup by&#xff0c;avg&#xff0c;sumcardinality去重 distinctreindex数据迁移 参考博客 [1]

ZooKeeper Client API 安装及使用指北

下载 wget https://archive.apache.org/dist/zookeeper/zookeeper-3.5.4-beta/zookeeper-3.5.4-beta.tar.gz解压 tar -zxf zookeeper-3.5.4-beta.tar.gz安装 cd zookeeper-3.5.4-beta/src/c/ ./configure make sudo make install到 make 这一步大概率会出现报错&#xff1a;…

【数据结构初阶】二叉树(2)

二叉树顺序结构 1.二叉树的顺序结构及实现1.1二叉树的顺序结构 1.2 堆的概念及结构1.3 堆的实现1.3.1向上调整1.3.2向下调整1.3.3交换函数1.3.4打印1.3.5初始化1.3.6销毁1.3.7插入1.3.8删除1.3.9获得堆顶元素1.3.10判断是否为空1.3.6 堆的代码实现 1.3.2堆的创建1.3.3 建堆时间…

怎么修复MSVCR110.dll文件?全面解析MSVCR110.dll缺失修复方法

MSVCR110.dll文件缺失问题在Windows操作系统用户中相当普遍&#xff0c;经常导致应用程序启动失败或崩溃。MSVCR110.dll是Microsoft Visual C Redistributable for Visual Studio 2012的一部分&#xff0c;且应用程序通常依赖这个DLL文件来执行C库中的代码。文件的丢失可能源自…

【代码随想录】刷题笔记Day39

前言 下午答疑课过于无聊了&#xff0c;后台在跑代码也写不了作业&#xff0c;再刷点题吧~难得一天两篇 56. 合并区间 - 力扣&#xff08;LeetCode&#xff09; 和之前重叠区间是同个类型&#xff0c;和res里的元素比较&#xff0c;重叠就更新res里最后元素的最右边界 class…

VS2020使用MFC开发一个贪吃蛇游戏

背景&#xff1a; 贪吃蛇游戏 按照如下步骤实现:。初始化地图 。通过键盘控制蛇运动方向&#xff0c;注意重新设置运动方向操作。 。制造食物。 。让蛇移动&#xff0c;如果吃掉食物就重新生成一个食物&#xff0c;如果会死亡就break。用蛇的坐标将地图中的空格替换为 #和”将…

常用JavaScript库

一、认识前端工具库 1.前端工具类库 jQuery是一个快速、小型且功能丰富的 JavaScript 库&#xff0c;它使HTML文档遍历和操作、事件处理、动画和 AJAX 之类的事情变得 更加简单。当时jQuery库不但简化了代码&#xff0c;而且提供出色的跨浏览器支持&#xff0c;其极大的提高了…

黑马头条-day10-xxl-job热点文章计算

目录 一.需求分析 实现思路 定时计算 定时任务框架-xxljob 二. 学习目录 分布式任务调度 1 xxl-Job简介 2 XXL-Job-环境搭建 2.1调度中心环境要求 2.2源码仓库地址 2.3 初始化“调度数据库” 2.4配置部署“调度中心” 3.配置部署调度中心-docker安装 4.简单实例 创建…

每周一算法:邻值查找

给定一个长度为 n n n的序列 A A A&#xff0c; A A A中的数各不相同。 对于 A A A 中的每一个数 A i A_i Ai​&#xff0c;求&#xff1a; m i n 1 ≤ j < i ∣ A i − A j ∣ min_{1≤j<i}|A_i−A_j| min1≤j<i​∣Ai​−Aj​∣&#xff0c;以及令上式取到最小值的…

40G多模光模块QSFP-40G-SR4优势及应用领域介绍

QSFP-40G-SR4光模块是一种常用的光纤传输解决方案。传输速率40G&#xff0c;SR代表短距离多模光纤&#xff08;Short Range Multimode Fiber&#xff09;&#xff0c;4表示有四个光纤通道。这种光模块采用MPO/MTP多模光纤连接器来实现高速传输&#xff0c;传输距离可以达到300米…

WU反走样算法

WU反走样算法 由离散量表示连续量而引起的失真称为走样&#xff0c;用于减轻走样现象的技术成为反走样&#xff0c;游戏中称为抗锯齿。走样是连续图形离散为想想点后引起的失真&#xff0c;真实像素面积不为 零。走样是光栅扫描显示器的一种固有现象&#xff0c;只能减轻&…

【Echarts】使用echarts和echarts-wordcloud生成词云图

一、下载echarts和echarts-wordcloud 地址&#xff1a;https://download.csdn.net/download/qq_25285531/88663006 可直接下载放在项目中使用 二、词云数据 词云数据是对象的格式&#xff0c;可以从后端获取&#xff0c;这里以下面数据为例 {"visualMap": 199,&…