掘根宝典之C++多重继承,二义性,虚基类

多重继承派生类

除去一个类从一个基类派生,C++还支持一个派生类同时继承多个基类

MI:有多个直接基类的类

1.多重继承派生类的定义

如果已经定义了多个基类,那么定义多重继承的派生类的形式为:

class 派生类名:访问标号1 基类名1,访问标号2 基类名2,... { //类体成员列表
};

举个例子

class A { };
class B : public A { }; //A-B
class C : public A { }; //A-C
class D : public B,public C { }; //B-D,C-D

这里的D类就是一个MI. 

2.多重继承派生类的构造函数

多重继承派生类的构造函数形式与单一继承时的构造函数形式基本相同,只是在派生类的构造函数初始化列表中调用多个基类构造函数。

一般形式为:

派生类名(形式参数列表):基类名1(基类1构造函数实参列表),基类名2(基类2构造函数实参列表),...,成员对象名1(子对象1属类构造函数实参列表),...,派生类初始化列表
{派生类初始化函数体
}



其调用顺序是:
(1)调用基类构造函数,各个基类按定义时的次序先后调用;
(2)调用成员对象构造函数,各个子对象按声明时的次序先后调用;
(3)执行派生类初始化列表;
(4)执行派生类初始化函数体;

例:多重继承举例

#include<iostream> 
using namespace std; class Base1 {private:int b1;public:Base1() {b1=0;cout<<"默认构造Base1:"<<"b1="<<b1<<endl;}Base1(int i) {b1=i;cout<<"构造Base1:"<<"b1="<<b1<<endl;}
};class Base2 {private:int b2;public:Base2() {b2=0;cout<<"默认构造Base2:"<<"b2="<<b2<<endl;}Base2(int j) {b2=j;cout<<"构造Base2:"<<"b2="<<b2<<endl;}
};class Base3 {public:Base3() {cout<<"默认构造Base3:"<<endl;}
};class Derive : public Base1,public Base2,public Base3 {private:Base1 memberBase1;Base2 memberBase2;Base3 memberBase3;public:Derive() {cout<<"默认构造函数Derive."<<endl; }Derive(int a,int b,int c,int d): Base1(a),Base2(b),memberBase1(c),memberBase2(d) {cout<<"构造Derive."<<endl;}
};int main()
{cout<<endl<<"创建派生类对象obj1:"<<endl;Derive obj1;cout<<endl<<"创建派生类对象(1,2,3,4):"<<endl;Derive obj2(1,2,3,4);return 0;
}



运行结果:

创建派生类对象obj1:
默认构造Base1:b1=0//基类默认构造函数,下面2个也是
默认构造Base2:b2=0
默认构造Base3:
默认构造Base1:b1=0//成员对象的构造函数,下面2个也是
默认构造Base2:b2=0
默认构造Base3:
默认构造函数Derive,//派生类默认构造函数创建派生类对象(1.2.3.4):
构造Base1:b1=1   //基类构造函数,下面2个也是
构造Base2:b2=2
默认构造Base3:
构造Base1:b1=3  //成员对象的构造函数,下面2个也是
构造Base2:b2=4
默认构造Base3:
构造Derive.     //派生类构造函数

二义性问题及名字支配规则

1.二义性问题

多重继承时,多个基类可能出现同名的成员。在派生类中如果使用一个表达式的含义能解释为可以访问多个基类的成员,则这种对基类成员的访问就是不确定的,称这种访问具有二义性。C++要求派生类对基类成员的访问必须是无二义性的

例如:

class A {public:void fun() { cout<<"a.fun"<<endl; }
};
class B {public:void fun() { cout<<"b.fun"<<endl; }void gun() { cout<<"b.gun"<<endl; }
};
class C:public A,public B {public:void gun() { cout<<"c.gun"<<endl; } //重写gun()void hun() { fun(); } //出现二义性
};
int main()
{C c,*p=&c;return 0;
}


使用成员名限定可以消除二义性,例如:

//成员名限定消除二义性

c.A::fun();
c.B::fun();
p->A::fun();
p->B::fun();



基本形式为:

对象名.基类名::成员名
对象指针名->基类名::成员名


2.名字支配规则

C++对于在不同的作用域声明的名字,可见性原则是:如果存在两个或多个具有包含关系的作用域,外层声明了一个名字,而内层没有再次声明相同的名字,那么外层名字在内层可见;如果在内层声明了相同的名字,则外层名字在内层不可见,这时称内层名字隐藏(或覆盖)了外层名字,这种现象称为隐藏规则

在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用域是不同的:基类在外层,派生类在内层

如果派生类声明了一个和基类成员同名的新成员,派生的新成员就覆盖了基类同名成员,直接使用成员名只能访问到派生类的成员

如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数不同,从基类继承的同名函数的所有重载形式也都会被覆盖

如果要访问被覆盖的成员,就需要使用基类名和作用域限定运算符来限定

派生类D中的名字N覆盖基类B中同名的名字N,称为名字支配规则。如果一个名字支配另一个名字,则二者之间不存在二义性,当选择该名字时,使用支配者的名字,如:

c.gun(); //使用C::gun



如果要使用被支配者的名字,则应使用成员名限定,例如:

c.B::gun(); //使用B::gun



虚基类

C++引入虚基类的目的是为了解决多继承时可能出现的冲突问题。当一个类通过多个路径继承了同一个基类时,如果不使用虚基类,那么在派生类中就会存在多个基类子对象,这可能会导致数据重复和二义性的问题。

通过使用虚基类,可以确保在派生类中只有一个基类子对象,从而避免数据重复和二义性的问题。虚基类的成员在派生类中只有一个副本,不会重复出现。

另外,虚基类还可以实现多态性,能够让派生类对象按照基类的指针或引用进行使用,从而提高代码的灵活性和可扩展性。

1.虚基类的定义

虚基类是在派生类定义时,指定继承方式时声明的。声明虚基类的一般形式为:

class 派生类名: virtual 访问标签 虚基类名,... { //类体成员列表
};

还有一种形式是

class 派生类名: 访问标签 virtual 虚基类名,... { // 成员列表
};



需要注意,为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承(我们下面会举例子)

例:虚基类举例

#include<iostream>
using namespace std; class A { //声明为基类Apublic:A(int n) { //A类的构造函数 nv=n;cout<<"Member of A"<<endl;} void fun() {cout<<"fun of A"<<endl;}private:int nv;
};class B1: virtual public A { //声明A为虚基类 public:B1(int a):A(a) { //B1类的构造函数 cout<<"Member of B1"<<endl;}private:int nv1; 
};class B2: virtual public A { //声明A为虚基类public:B2(int a):A(a) { //B2类的构造函数cout<<"Member of B2"<<endl; } private:int nv2;
};class C: public B1,public B2 {public://派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用C(int a):A(a),B1(a),B2(a) {cout<<"Member of C"<<endl;}void fund() {cout<<"fun of C"<<endl;}private:int nvd;
};int main()
{C c1(1);c1.fund();c1.fun(); //不会产生二义性return 0; 
}

 现在C对象只将包含A对象的一个副本。从更本质的说,继承的B1和B2对象共享一个A对象,而不是各种引入自己的A对象副本。这样子调用A类方法就不会有二义性了(不知道调用B!继承的还是B2继承的A类方法)。

如果我们不引入虚基类,再看看这个例子

#include<iostream>
using namespace std;class A { //声明为基类A
public:A(int n) { //A类的构造函数 nv = n;cout << "Member of A" << endl;}void fun() {cout << "fun of A" << endl;}
private:int nv;
};class B1 : virtual public A { //声明A为虚基类 
public:B1(int a) :A(a) { //B1类的构造函数 cout << "Member of B1" << endl;}
private:int nv1;
};class B2 :  public A {         //注意这里没有声明A为虚基类
public:B2(int a) :A(a) { //B2类的构造函数cout << "Member of B2" << endl;}
private:int nv2;
};class C : public B1, public B2 {
public://派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用C(int a) :A(a), B1(a), B2(a) {cout << "Member of C" << endl;}void fund() {cout << "fun of C" << endl;}
private:int nvd;
};int main()
{C c1(1);c1.fund();c1.fun(); //产生了二义性return 0;
}

我们将B2后面的virtual去掉,使A类失去虚基类的性质。发现上面这个程序出现了二义性,编译器不知道调用B1继承的A类方法还是B2继承的A类方法。 


2.虚基类的初始化

如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生和间接派生)中,都要通过构造函数的初始化表对虚基类进行初始化。例如:

class A { public: A(int) { } }; //定义构造函数有参数的基类
class B: virtual public A {public:B(int a):A(a) { } //对基类A初始化
};
class C: virtual public A {public:C(int a):A(a) { } //对基类A初始化
};
class D: public B,public C {public:D(int a):A(a),B(a),C(a) { }
};


在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化

关于虚基类的说明:

(1)一个类可以在一个类族中即被用作虚基类,也被用作非虚基类

(2)派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的默认构造函数

(3)在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行多重继承应用举例

#include<iostream>
using namespace std; enum Color { //颜色枚举类型 Red,Yellow,Green,White
};class Circle { //圆类Circle的定义private:float radius;public:Circle(float r) {radius=r;cout<<"Circle initialized!"<<endl;} ~Circle() {cout<<"Circle destroyed!"<<endl;}float Area() {return 3.1415926*radius*radius;}
};class Table { //桌子类Table的定义private:float height;public:Table(float h) {height=h;cout<<"Table initialized!"<<endl;}~Table() {cout<<"Table destroyed!"<<endl;}float Height() {return height;}
};class RoundTable: public Table,public Circle { //圆桌类的定义private:Color color;public:RoundTable(float h,float r,Color c);int GetColor() {return color;}~RoundTable() {cout<<"RoundTable destroyed!"<<endl;}
};RoundTable::RoundTable(float h,float r,Color c):Table(h),Circle(r) { //圆桌构造函数的定义color=c;cout<<"RoundTable initialized!"<<endl; 
}int main()
{RoundTable cir_table(15.0,2.0,Yellow);cout<<"The table properties are:"<<endl;cout<<"Height="<<cir_table.Height()<<endl; //调用Table类的成员函数cout<<"Area="<<cir_table.Area()<<endl; //调用circle类的成员函数cout<<"Color="<<cir_table.GetColor()<<endl; //调用RoundTable 类的成员函数return 0;
}



运行结果:

Table initialized!
Circle initialized!
RoundTable initialized!
The table properties are:
Height=15
Area=12.5664
Color=1
RoundTable destroyed!
Circle destroyed!
Table destroyed!

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

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

相关文章

【开源】SpringBoot框架开发独居老人物资配送系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询社区4.2 新增物资4.3 查询物资4.4 查询物资配送4.5 新增物资配送 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的独居老人物资配送系统&#xff0c;包含了社区档案、…

win7自带截图工具保存失效解决办法

今日发现一台远航技术的win7中自带的截图工具使用时正常&#xff0c;保存图片时没有弹出保存位置的对话窗口&#xff0c;无法正常保存图片。解决方案如下&#xff1a; 1、进入注册表编辑器。开始-搜索程序和文件-输入 regedit 按下回车键&#xff0c;打开注册表&#xff1b; 2、…

多模态基础---BERT

1. BERT简介 BERT用于将一个输入的句子转换为word_embedding&#xff0c;本质上是多个Transformer的Encoder堆叠在一起。 其中单个Transformer Encoder结构如下&#xff1a; BERT-Base采用了12个Transformer Encoder。 BERT-large采用了24个Transformer Encoder。 2. BERT的…

360小工具

有时候不希望打开360安全卫士&#xff0c;但又需要使用它的小工具 小工具目录统一基于360安装目录&#xff0c;一般情况在&#xff1a;C:\Program Files (x86)\360\360Safe 备份还原小工具 C:\Program Files (x86)\360\360Safe\Utils\winrebackup\360WinREBackup64.exe 360LS…

静态时序分析:SDC约束命令set_clock_transition详解

相关阅读 静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html?spm1001.2014.3001.5482 在静态时序分析&#xff1a;SDC约束命令create_clock详解一文的最后&#xff0c;我们谈到了针对理想(ideal)时钟&#xff0c;可以使用set_clock_transition命令直…

MQTT的应用场景和发展方向

随着物联网&#xff08;IoT&#xff09;技术的发展&#xff0c;MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;作为一种轻量级的通信协议&#xff0c;正在被广泛应用于各种物联网场景中。本文将介绍MQTT的应用场景以及未来的发展方向。 一、MQTT的应用场景…

在Linux系统中,yum 和 apt-get 都是用于管理软件包和更新系统的命令行工具,它们的区别是什么?

一、 yum yum 是在基于RPM的Linux发行版&#xff08;如CentOS、RHEL及其衍生版&#xff09;中使用的包管理器。它通过网络连接到软件仓库并自动处理依赖关系来安装、更新和卸载软件包。主要命令包括&#xff1a; 1、更新软件包列表 yum update 2、安装软件包 yum install …

c++堆排序解释

堆排序是一种基于二叉堆数据结构的排序算法。它是利用了堆的性质&#xff0c;即父节点的值总是大于或等于&#xff08;或小于或等于&#xff09;其子节点的值。 堆排序的基本思想是首先将待排序的数组构建成一个最大堆&#xff08;或最小堆&#xff09;&#xff0c;然后将堆的…

数据结构与算法:双向链表

朋友们大家好啊&#xff0c;在上节完成单链表的讲解后&#xff0c;我们本篇文章来对带头循环双向链表进行讲解 双向链表 双向链表、头节点和循环的介绍构建双向链表节点的构建初始化双向循环链表&#xff08;空链表&#xff09;销毁双向链表 链表的打印双向链表头尾的插与删尾插…

Medical Boundary Diffusion Modelfor Skin Lesion Segmentation

皮肤病灶分割的医学边界扩散模型 摘要 由于多尺度边界关注和特征增强模块的进步&#xff0c;皮肤镜图像中的皮肤病变分割最近取得了成功。然而&#xff0c;现有的方法依赖于端到端学习范式&#xff0c;直接输入图像和输出分割图&#xff0c;经常与极其困难的边界作斗争&#…

B端系统升级方案模板:针对美观性和体验性升级(总体方案)

大家好&#xff0c;我是大美B端工场&#xff0c;专注于前端开发和UI设计&#xff0c;有需求可以私信。本篇从全局分享如何升级B端系统&#xff0c;搞B端系统升级的有个整体思维&#xff0c;不是说美化几个图标&#xff0c;修改几个页面就能解决的&#xff0c;这个方案模板&…

laravel_进程门面_再次介绍

文章目录 单次调用进程池调用伪造和断言断言和判断的区别 前面我已经简单介绍了一点process facades&#xff0c;这里再进行一个补充的介绍。 可以也看看前面的介绍。 单次调用 use Illuminate\Support\Facades\Process;$result Process::run(ls -la);return $result->ou…

c++希尔排序解释

希尔排序是一种排序算法&#xff0c;它是插入排序的改进版本。它通过将序列分成多个子序列&#xff0c;对每个子序列进行插入排序&#xff0c;然后逐步减小子序列的长度&#xff0c;直到整个序列有序。 希尔排序的基本思想是&#xff1a;选择一个增量序列&#xff0c;根据增量…

【天枢系列 01】Linux行数统计:命令对决,谁才是王者?

文章目录 01 统计行数命令1.1 wc 命令1.2 grep 命令1.3 sed 命令1.4 awk 命令1.5 Perl 命令 02 Linux的wc命令详细用法2.1 基本语法2.2 主要选项2.3 附加选项2.4 示例用法2.5 注意事项【重要&#xff01;】 03 Linux的grep命令详细用法3.1 基本语法3.2 主要选项3.3 示例用法3.4…

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

文章目录 一、起因二、代码1. 定义exchange和queue2. RabbitTemplate3. EnhancedCorrelationData4. 发送消息 环境如下 VersionSpringBoot3.2.1spring-amqp3.1.1RabbitMq3-management 一、起因 老版本的spring-amqp在CorrelationData上设置ConfirmCallback。但是今天却突然发…

非常详细!操作系统【IO核心子系统】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;OS从基础到进阶 &#x1f3c6;&#x1f3c6;本文完整PDF源文件请翻阅至文章底部下载。&#x1f3c6;&#x1f3c6; 1 I/O核心子系统——概述1.1 核心子系统要完成的功能1.1.1 假脱机技术1…

Legba:一款功能强大的多协议凭证爆破与密码喷射枚举工具

关于Legba Legba是一款功能强大的多协议凭证爆破与密码喷射枚举工具。该工具的目的就是在消耗更少资源的同时实现更好的性能和稳定性&#xff0c;可以帮助广大研究人员执行多种协议场景下的凭证爆破&#xff0c;以及密码喷射和枚举任务。 支持的协议和功能 AMQP&#xff08;…

Python 异常处理及程序调试

Python 是一门功能强大而又易于学习的编程语言&#xff0c;它提供了丰富的工具和库来帮助开发者编写高效、稳定的程序。然而&#xff0c;在编写复杂的应用程序时&#xff0c;错误和异常是难以避免的。本文将介绍 Python 中的异常处理机制以及程序调试技巧&#xff0c;帮助读者提…

JavaScript中什么是事件委托

JavaScript 中的事件委托&#xff08;Event delegation&#xff09;是一种重要的编程技术&#xff0c;它能够优化网页中的事件处理&#xff0c;提高程序的性能和可维护性。本文将详细介绍事件委托的概念、工作原理&#xff0c;并提供示例代码来说明其实际应用。 事件委托是基于…

PointMixer论文阅读笔记

MLP-mixer是最近很流行的一种网络结构&#xff0c;比起Transformer和CNN的节构笨重&#xff0c;MLP-mixer不仅节构简单&#xff0c;而且在图像识别方面表现优异。但是MLP-mixer在点云识别方面表现欠佳&#xff0c;PointMixer就是在保留了MLP-mixer优点的同时&#xff0c;还可以…