C++继承与派生——(8)多继承

归纳编程学习的感悟,
记录奋斗路上的点滴,
希望能帮到一样刻苦的你!
如有不足欢迎指正!
共同学习交流!
🌎欢迎各位→点赞 👍+ 收藏⭐ + 留言​📝
苦难和幸福一样,都是生命盛开的花朵!

一起加油!

目录

一、前言:

二、多继承的定义:

三、多继承的构造函数以及调用顺序:

💦例:多继承下构造函数和析构函数的调用顺序 。

🔑说明:

四、多继承中的同名隐藏和二义性问题: 

⚡注意:

💦例:多继承同名隐藏示例。 

🔑说明:

 ?思考:

🔑说明:

💦例:复杂版本的多继承同名隐藏示例。

五、虚基类:

💦例:虚基类应用示例。

⚡注意:

 六、总结:

七、共勉:


一、前言:

        根据派生类继承基类的个数,将继承分为单继承和多继承。之前我们主要以单继承为例学习了派生类的定义以及使用中应注意的问题。多继承可以看成是单继承的组合,它们有很多相似的特征。

二、多继承的定义:

        多继承的基类不只一个,而是有多个,派生类与每个基类之间的关系可以看作是一个单继承。多继承的定义格式如下:


class <派生类名>:<继承方式><基类名 1>,..,<继承方式><基类名 n>

                                {
                                                <派生类新定义的成员>

                                }

三、多继承的构造函数以及调用顺序:

        在多继承方式下,派生类构造函数要负责为每一个基类构造函数传入初始化的参数,派生类的构造函数格式如下:

class  <派生类名>(<总参数表>):<基类名 1>(<参数表 1>),...,<基类名 n>(<参数表 n>)

                                        {

派生类数据成员的初始化

                                        }

        其中,<总参数表>必须包含完成所有基类初始化所需的参数。
        由于存在多个基类,多继承规定派生类包含多个基类时,构造函数的调用顺序是:先调用所有基类的构造函数,再调用对象成员的构造函数(如果有对象成员 ),最后调用派生类自己的构造函数。其中,处于同一继承层次的各基类构造函数的调用顺序取决于定义派生类时所指定的基类的顺序,与派生类构造函数中所定义的成员初始化列表顺序无关。如果类中有对象成员,那么,对象成员构造函数的调用顺序与对象在类中声明的顺序一致。 

💦例:多继承下构造函数和析构函数的调用顺序 。

#include<iostream>
using namespace std;
class Base1
{public:Base1(int i){b1=i;cout<<"construct Base1"<<endl;}void display(){cout<<"b1="<<b1<<endl;}~Base1(){cout<<"destruct Base1"<<endl;}private:int b1;
};
class Base2
{public:Base2(int i){b2=i;cout<<"construct Base2"<<endl;}void display(){cout<<"b2="<<b2<<endl;}~Base2(){cout<<"destruct Base2"<<endl;}private:int b2;
};
class Derive:public Base2,public Base1
{public:Derive(int m):Base1(m+2),Base2(m-2){d=m;cout<<"construct Derive"<<endl;}void display(){Base1::display();Base2::display();cout<<"d="<<d<<endl;}~Derive(){cout<<"destruct Derive"<<endl;}private:int d; 
};
int main()
{Derive d(10);d.display();return 0;
}

🔑说明:

        构造函数的调用顺序是: Base2、Basel、Derive,析构函数的调用顺序是: Derive、Base1、Base2。

四、多继承中的同名隐藏和二义性问题: 

        上例中派生类中定义了与基类同名的函数 display,对于在不同作用域声明的标识符,可见性原则是:如果存在两个或多个包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层依然可见;如果在内层声明了同名标识符,则外层标识符在内层不可见,此时称内层标识符隐藏了外层同名标识符,这种现象被称为同名隐藏规则
        在类的派生层次结构中,基类的成员和派生类新增加的成员都具有类作用域,两者的作用范围不同,是相互包含的两个层,派生类在内层。这时,在基类 Basel、Base2 中都定义了 display函数,在派生类中也定义了 display 函数。如果在类外通过派生类对象 d 去调用 display 函数,派生类新成员就会隐藏外层同名的成员,直接使用成员名只能访问派生类的成员。若派生类中声明了与基类成员函数同名的新函数,即使函数的形参表不同,也不构成重载的关系,从基类继承过来的同名函数也会被隐藏。若要访问被隐藏的成员,就需要使用作用域操作符和基类名来限定。

⚡注意:

        当派生类中定义了与基类同名但具有不同的形参的函数时(形参个数不同,或者形参类型不同),不属于函数重载,这时派生类中的函数使基类中的函数隐藏,调用父类中的函数必须使用父类名称来限定。只有在相同作用域中定义的函数才可以构成重载。 

💦例:多继承同名隐藏示例。 

#include<iostream>
using namespace std;
class Base1
{public:Base1(int i){b1=i;cout<<"construct Base1"<<endl;}void display(){cout<<"b1="<<b1<<endl;}~Base1(){cout<<"destruct Base1"<<endl;}private:int b1;
};
class Base2
{public:Base2(int i){b2=i;cout<<"construct Base2"<<endl;}void display(){cout<<"b2="<<b2<<endl;}~Base2(){cout<<"destruct Base2"<<endl;}private:int b2;
};
class Derive:public Base2,public Base1
{public:Derive(int m):Base1(m+2),Base2(m-2){d=m;cout<<"construct Derive"<<endl;}void display(){cout<<"d="<<d<<endl;}~Derive(){cout<<"destruct Derive"<<endl;}private:int d; 
};
int main()
{Derive d(10);d.Base1::display();d.Base2::display();d.display();return 0;
}

🔑说明:

        在主函数中、定义了派生类对象 d,根据同名隐藏规则,如果通过派生类对象访问display 函数,只能访问派生类新添加的成员,从基类继承过来的成员由于处于外层作用域而被隐藏。此时要通过d 访问从基类继承过来的成员,就必须使用类名和作用域操作符;访问 Base1中的 display,使用 Base1::display;访问 Base2 中的 display,使用Base2::display。

        通过作用域操作符,明确且唯一地标识了派生类中由基类继承过来的成员,解决了同名隐藏的问题。

 ?思考:

        如果在派生类中没有定义 display,是不是就不存在同名隐藏,那么,通过d可以访问到的是 Base1的display,还是 Base2的display?请改写程序,验证你的想法。

        假如我们把派生类定义的 display 函数删除,此时派生了继承了来自 Base1的 display 和 Base2的 display,由于 display 存在二义性,依然无法直接通过派生类对象d直接访问基类的成员 display。

        如果某个派生类的部分或者直接基类是从另一个共同的基类派生而来,在这些间接基类中从上一级基类继承来的成员拥有相同的名称,在派生类中也会产生同名的现象。这种同名也需要通过作用域操作符来进行标识,而且必须用直接基类来进行限定。 

🔑说明:

        基类 A 中声明了数据成员a、构造函数、析构函数和函数 fun0,A 派生出了 B1和B2,再以 B1、B2 作为基类共同派生出新类 C,在派生类中都没有添加新的同名成员。这时的C类,包含通过B1,B2 继承过来的基类A 中的同名成员 fun0,类的关系图及派生类的结构图如图所示,其中“+”号表示公有,“-”号表示私有,保护的用“#”号表示。

多层继承下的派生类关系图及成员构成图

        对于派生类中成员 a 和 fun0 的访问,只能通过直接基类 B1或者 B2 的名称来限定才可以不能通过基类A 来限定,因为通过 A 限定无法表明成员是从 B1继承的,还是从 B2 继承的。

💦例:复杂版本的多继承同名隐藏示例。

#include<iostream>
using namespace std;
class A
{public:int a;void fun0(){ cout<<"A function is called"<<endl;}A(){cout<<"construct A"<<endl;}~A(){cout<<"destruct A"<<endl;}
}; 
class B1:public A
{public:int b1;B1(){cout<<"construct B1"<<endl;}~B1(){cout<<"destruct B1"<<endl;}
}; 
class B2:public A
{public:int b2;B2(){cout<<"construct B2"<<endl;}~B2(){cout<<"destruct B2"<<endl;}
}; 
class C:public B1,public B2
{public:int c;void fun0(){ cout<<"C function is called"<<endl;}	C(){cout<<"construct C"<<endl;}~C(){cout<<"destruct C"<<endl;}			
};
int main()
{C c;c.B1::a=10;c.B1::fun0() ;c.B2::a=20;c.B2::fun0() ;return 0;
}

        在主函数中定义了派生类对象 c,如果只通过成员名称来访问该类的成员 a 和 fun0,系统就无法唯一确定要引用的成员。这时必须通过作用域操作符,通过直接基类来确定要访问的从基类
继承来的成员。

        此时,在内存中,派生类对象同时拥有两个 a 的空间,这两个a 可以分别通过 B1和B2调基类 A 的构造函数进行初始化,能够存放不同的数值。也可以使用作用域操作符通过直接基类行区分,分别进行访问。但是,在大多数情况下,我们不需要两个同名副本,只需要保留一个即可,C++提供了虚基类技术来解决此问题。 

五、虚基类:

        上例中当我们定义一个派生类对象 c 时,它会构造 B1 类,B2 类,B1,B2 类都有一个父类,因此A 类被构造了两次,在 c中,A 中的数据成员 a 有两个副本,A 中的成员函数 fun0 也有两个映射。一般可以将共同基类 A 设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个空间,同一个函数名也只有一个映射,在构造派生类对象 c 时,A 类只会构造一次。

💦例:虚基类应用示例。

#include<iostream>
using namespace std;
class A
{public:int a;void fun0(){ cout<<"A function is called"<<endl;}A(){cout<<"construct A"<<endl;}~A(){cout<<"destruct A"<<endl;}
}; 
class B1:virtual public A
{public:int b1;B1(){cout<<"construct B1"<<endl;}~B1(){cout<<"destruct B1"<<endl;}
}; 
class B2:virtual public A
{public:int b2;B2(){cout<<"construct B2"<<endl;}~B2(){cout<<"destruct B2"<<endl;}
}; 
class C:public B1,public B2
{public:int c;void fun0(){ cout<<"C function is called"<<endl;}	C(){cout<<"construct C"<<endl;}~C(){cout<<"destruct C"<<endl;}			
};
int main()
{C c;c.a=10; c.fun0();return 0;
}

注意:

        虚基类并不是将基类声明为虚基类,只是在类的派生过程中使用了 virtual 关键字 

        在具体程序设计过程中,如果不需要重复的副本,可以选择虚基类,如果需要更多副本空间存在不同数据,则可以采用作用域操作符方式区别访问。一般采用虚基类可以使得程序更加简洁同时节省更多内存空间。 

 六、总结:

  • 根据派生类继承基类的个数,将继承分为单继承和多继承。
  • 多继承可以看成是单继承的组合。
  • 处于同一继承层次的各基类构造函数的调用顺序取决于定义派生类时所指定的基类的顺序,与派生类构造函数中所定义的成员初始化列表顺序无关。
  • 如果在内层声明了同名标识符,则外层标识符在内层不可见,此时称内层标识符隐藏了外层同名标识符。
  • 若派生类中声明了与基类成员函数同名的新函数,即使函数的形参表不同,也不构成重载的关系。
  • 只有在相同作用域中定义的函数才可以构成重载。 
  • 通过作用域操作符,明确且唯一地标识了派生类中由基类继承过来的成员,解决了同名隐藏的问题。
  • 虚基类并不是将基类声明为虚基类,只是在类的派生过程中使用了 virtual 关键字 。
  • 一般采用虚基类可以使得程序更加简洁同时节省更多内存空间。 

七、共勉:

        以上就是我对C++继承与派生——(8)多继承的理解,希望本篇文章对你有所帮助,也希望可以支持支持博主,后续博主也会定期更新学习记录,记录学习过程中的点点滴滴。如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++继承与派生的理解,请持续关注我哦!!! 

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

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

相关文章

Java API 操作Docker浅谈

背景&#xff1a; 使用com.github.docker-java库可以很方便地在Java中操作Docker。下面是一个详细的教程&#xff0c;包括创建镜像、创建容器、启动容器、停止容器和删除容器的步骤以及每一步的说明。 前提&#xff1a; 首先&#xff0c;在你的Java项目中添加com.github.doc…

gRPC之内置Trace

1、内置Trace grpc内置了客户端和服务端的请求追踪&#xff0c;基于golang.org/x/net/trace包实现&#xff0c;默认是开启状态&#xff0c;可以查看事 件和请求日志&#xff0c;对于基本的请求状态查看调试也是很有帮助的&#xff0c;客户端与服务端基本一致&#xff0c;这里…

c++写入数据到文件中

假设你想编写一个C程序&#xff1a;当你在调试控制台输入一些数据时&#xff0c;系统会自动存入到指定的文件中&#xff0c;该如何操作呢&#xff1f; 具体操作代码如下&#xff1a; #include<iostream> #include<string> #include<fstream> using namespa…

服务器硬件及RAID配置实战

目录 1、RAID的概念 2、RAID的实现方式 3、标准的RAID 3.1 RAID 0 3.2 RAID 1 3.3 RAID 5 3.4 RAID 10 4、建立硬件 RAID的过程步骤 1、进入RAID 1.1 重启服务器 1.2 进入RAID界面 1.3 在RAID界面切换目录 2、创建RAID 2.1 移动到RAID卡 2.2 按F2&#xff0c;选择…

AspectJWeaver之Gadget分析

前言&#xff1a; 今天看了下ysoserial的AspectJWeaver方法&#xff0c;分析了下其是如何通过调用SimpleCache$StorableCachingMap来实现写文件&#xff0c;这里把分析的流程写下来&#xff1a; 首先我们要看下其所需要的jar包&#xff1a; <dependencies><dependen…

redis 从0到1完整学习 (十二):RedisObject 之 List 类型

文章目录 1. 引言2. redis 源码下载3. redisObject 管理 List 类型的数据结构3.1 redisObject 管理 List 类型3.2 List PUSH 源码 4. 参考 1. 引言 前情提要&#xff1a; 《redis 从0到1完整学习 &#xff08;一&#xff09;&#xff1a;安装&初识 redis》 《redis 从0到1…

SQL性能优化-索引

1.性能下降sql慢执行时间长等待时间长常见原因 1&#xff09;索引失效 索引分为单索、复合索引。 四种创建索引方式 create index index_name on user (name); create index index_name_2 on user(id,name,email); 2&#xff09;查询语句较烂 3&#xff09;关联查询太多join&a…

边缘计算网关在温室大棚智能控制系统应用,开启农业新篇章

项目需求 ●目前大棚主要通过人为手动控温度、控水、控光照、控风&#xff0c;希望通过物联网技术在保障产量的前提下&#xff0c;提高作业效率&#xff0c;降低大棚总和管理成本。 ●释放部分劳动力&#xff0c;让农户有精力管理更多大棚&#xff0c;进而增加农户收入。 ●…

WEB 3D技术 three.js通过光线投射 完成几何体与外界的事件交互

本文 我们来说 光线投射 光线投射技术是用于3维空间场景中的交互事件 我们先编写代码如下 import ./style.css import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";const scene new THRE…

使用 extract + TextMapAdapter 实现了自定义 traceId

前言 某些特定的场景&#xff0c;需要我们通过代码的方式实现自定义 traceId。 实现思路&#xff1a;通过 tracer.extract 能够构造出 SpanContext &#xff0c;将构造出来的 SpanContext 作为上层节点信息&#xff0c;通过 asChildOf(SpanContext) 能够构造出当前的 span。 …

MVC下的四种验证编程方式

ASP.NET MVC采用Model绑定为目标Action生成了相应的参数列表&#xff0c;但是在真正执行目标Action方法之前&#xff0c;还需要对绑定的参数实施验证以确保其有效性&#xff0c;我们将针对参数的验证成为Model绑定。总地来说&#xff0c;我们可以采用4种不同的编程模式来进行针…

第三百四十回

文章目录 1. 概念介绍2. 方法与信息2.1 获取方法2.2 详细信息 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何获取设备信息"相关的内容&#xff0c;本章回中将介绍如何获取App自身的信息.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本…