当前位置: 首页 > backend >正文

C++笔记-继承(下)(包含派生类的默认成员函数,菱形继承等)

一.派生类的默认成员函数

1.14个常见默认成员函数

 默认成员函数,默认的意思就是指我们不写,编译器会自动为我们生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表显示调用。

2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

3.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5.派生类对象初始化先调用基类构造再调派生类构造。
6.派生类对象析构清理先调用派生类析构再调基类的析构。
7.因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

首先先写一个父类Person,便于下面的讲解。

在写子类的4个默认函数时要把父类成员当成一个整体

首先将其中的构造函数:

正如第一条所言,我们在写子类的构造函数时,在初始化列表部分要显示调用父类的构造函数,这里显式调用的格式就如上图所示,如果父类还有其他的成员变量,那么一并都写入父类的构造函数种。

接下来呢时拷贝构造函数,这类有人可能有疑惑:为什么直接传s就可以呢?
这个就涉及到我们上一篇讲的基类和派生类之间的转化,有印象的朋友就能记起来子类对象可以赋值给父类的引用或指针,本质就是“切片”,而我们上面写的Person类中的拷贝构造函数正是引用,所以这里直接传子类对象即可。

接着是=符号重载,这里面要注意的是我们要指定作用域来调用Person类中的=符号重载,至于原因也和上一篇讲的知识有关:最后讲的隐藏规则。

这里如果不指定作用域,那么子类的=符号重载会将父类的=符号重载给隐藏,这里会一直调用自己,一直递归,最终导致栈溢出。

而最后的析构函数呢情况比较复杂,就如上图所示,按理说应该没问题的,就和上面的构造函数和拷贝构造函数一样,那么这里为何为报错呢?

原因就正如第七条所言,因为都是destructor,所以子类的析构函数把父类的析构函数给隐藏了,所以这里就找不到父类的析构函数。

所以要利用父类的析构函数就要指定作用域,但是析构函数比较特殊,我们在实际应用中不会去显示调用父类的析构函数,原因就如第六条所言,我们要调用子类的析构函数,再调用父类的析构函数,而这里我们如果在子类析构函数中上去就直接调用父类的析构函数,那么就会使顺序颠倒。

并且因为第四条所言,那么就会导致父类的析构函数被调用两次:

很明显,是不能这么用的,这肯定会出问题的。所以在实际运用中我们是不需要主动去调用父类的析构函数,这也是析构函数和前面三个默认函数的区别。

4.2实现一个不能被继承的类

不能被继承的方式我讲解两种:c++98和c++11两种不同的方式。

c++98的方法就是将父类的构造函数放在private下,这种方式为什么可以呢?

就如上面讲的构造函数,子类构造函数要显示调用父类的构造函数,而父类的构造函数不能访问,通过这种机制就导致父类无法被继承。

当我们创建对象时就会出现这样的报错。

而c++11的方法相较于c++98简单了许多,直接在类名后面加一个关键字final,这样这个类就无法被继承了,就如上图所示。

二.继承和友元

友元关系不能被继承,也就是说基类友元不能访问派生类私有和保护成员。

我们以上面的例子为例,在讲解之前,注意我上面写的前置声明,因为Display函数同时用到了两个类,而student类还没有实现,所以就在前面加上一个前置声明,来告诉编译器我下面有一个类叫student。

通过上面的例子可以看出,Display并不能访问子类的保护成员。这就像你父亲的朋友不是你的朋友一样,是一个道理,所以是无法访问的。

而要想访问呢就在子类中进行友元声明即可访问。

三.继承与静态成员

基类定义了startic静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。

可以看出如果是非静态成员,父类和子类_name地址是不一样的,说明派生类继承下来了,父类和派生类对象各有一份。

而静态成员通过检验可以看出,父类和子类_count的地址是一样的,说明父类和子类共用同一份静态变量。

四.多继承及菱形继承问题

4.1继承模型

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

上面就是单继承的示意图,下面就是多继承的示意图,也是菱形继承的示意图。

严格的多继承第二张图去掉上面的Person,这样就成了一个标准的多继承。

单继承和多继承比较简单,主要来讲菱形继承的问题:

1.数据冗余

以第二幅图为例,如果Person中有一个_name的成员变量,那么student中会有一份name,teacher中也会有一份,这就导致Assistant在同时继承student和teacher时,就会有两份name,这就是数据冗余,其实也就是造成空间的浪费。

我们都知道没必要存两份name,但是菱形继承就会导致这个问题。

而数据冗余又会引出二义性的问题。

2.二义性

此时创建一个Assistant对象,访问name就会报错,因为编译器不知道到底要访问student中的name还是teacher中的name,这就是二义性。

而要解决这种问题有两种办法:

第一种办法就是指定作用域,指定你要访问哪个作用域的name,可以解决问题。

4.2虚继承

很多人说c++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是c++的缺陷之一,后来的一些编程语言都没有多继承,如java。

而虚继承就是上面的第二种解决方式:

此时我们不指定作用域也不会报错,因为虚继承,顾名思义就是看似继承了,其实没有继承,到最后Assistant中只有一份name。

使用虚继承后构造函数就要引用Person类的构造函数,因为是虚继承,student和teacher类中的构造函数都会调用Person类的构造函数,你传参过去编译器不知道到底用谁的name。

并且显式调用Person类的构造函数时,其实编译器并不会走student和teacher类中Person类构造函数那一行,也就是说写了之后只会走Assistant类中显式调用的Person类构造函数,不走其他两个的,即使显式调用了。

这也就是虚继承很燃的地方,比较绕,这也是因为多继承引出的问题而填的坑。

我们通过调试也可以发现,当修改了name之后,所有的name都会发生改变,也说明了name只有一份,看似student和teacher中有name,其实根本就没有把name放入进去。

虚继承呢大家也不必深究,因为实际操作中我们也避免生成菱形继承,并且虚继承也会造成性能损失,所以了解一下即可。

最后我们再看一道题:

问:p1,p2和p3的关系?

A.p1==p2==p3   B.p1<p2<p3   C.p1==p3!=p2   D.p1!=p2!=p3

大家可以思考一下这个问题。

答案呢选c,这个题呢涉及到了多继承中的指针偏移问题:

通过这个图就可以清晰观察到创建对象时指针的位置,p3==p1完全是巧合,正好都指向这段空间的起始位置,而p2就发生了指针偏移,经过切片后,base2基类的地址在中间的,因为前面是base1基类的地址,所以没有指向起始位置。

大家思考一下这个是菱形继承吗?

答案是菱形继承,在多继承的情况下只要有公共的基类,就是菱形继承,这个怎么体现呢?

B继承了A,C继承了B,那么C中就有一份A,D继承了A,那么D中也有一份A,所以是菱形继承。

再思考一下,如果要加虚继承那么加在哪两个位置呢?

答案加在BD两处,因为BD两处是直接导致有两份A的地方,所以是BD两处加。

5.继承和组合

1.public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
2.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
3.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
4.优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

组合的大概形式就如上图所示。

这也是个概念性的知识,大家看上面的解释,知道基本形式怎么用就行。

以上就是c++继承(下)的内容。

http://www.xdnf.cn/news/3388.html

相关文章:

  • AJAX 实例
  • vscode 的空格和 tab 设置 与 Rime 自建词库
  • AI大模型基础设施:主流的几款开源AI大语言模型的本地部署成本
  • 企业内训|智能驾驶与智能座舱技术——某汽车厂商
  • Ubuntu18 登录界面死循环 Ubuntu进不了桌面
  • 初学Vue之记事本案例
  • 【Linux】VSCode用法
  • 【嵌入式———通用定时器基本操作——实验需求2:案列:测量PWM的频率/周期】
  • 用手机相册教我数组概念——照片分类术[特殊字符][特殊字符]
  • 构建现代分布式云架构的三大支柱:服务化、Service Mesh 与 Serverless
  • 第十一届蓝桥杯 2020 C/C++组 门牌制作
  • vue 常见ui库对比(element、ant、antV等)
  • 兰亭妙微:数据驱动的 B 端设计:如何用 UI 提升企业级产品体验?
  • 【Qt】网络
  • ZYNQB笔记(十六):AXI DMA 环路测试
  • FreeSWITCH 发送 sip message 的 lua 程序
  • 深挖Java基础之:变量与类型
  • 总结C++中的STL
  • 分布式事务,事务失效,TC事务协调者
  • 图数据库榜单网站
  • 算法每日一题 | 入门-顺序结构-字母转换
  • X²+1素数问题
  • DirectX12(D3D12)基础教程七 深度模板视图\剔除\谓词
  • 【数据结构与算法】跳表实现详解
  • Windows结合WSL之ext4.vhdx不断增大问题
  • 第九节:文件操作
  • C++漫游指南——字符串篇与内存分配篇
  • ganesha-DBUS
  • 人形机器人的 “灵动密码”:动作捕捉与 AI 如何为其注入活力
  • BOSS的收入 - 华为OD机试(A卷,Java题解)