C++相关概念和易错语法(20)(赋值兼容转换、多继承、继承与组合)

news2024/9/22 15:35:41

1.赋值兼容转换

赋值兼容转换有一点易混,先看一下下面的代码,想想a、b、c对象里面存的什么,顺便结合以前的知识,对继承加深理解。


#include <iostream>
using namespace std;

class A
{
protected:
	A(int a)
		:_a(a)
	{}
	int _a;
};

class B : public A
{
public:
	B(int a, int b)
		:A(a)
		,_b(b)
	{}

	int _b;
};

int main()
{
	B b(1, 2);
	A a = b;
	A c(0);

	return 0;
}

结果如下,其中A c(0)操作不合语法,报错

(1)为什么A c(0)报错?

A的构造函数是protected,对外和private没有任何区别,都是无法访问的,因此在main函数里创建对象无论如何都是不会成功的,因为调不到private构造函数,也不会生成一个默认构造(因为已经有了一个构造函数)

(2)为什么B b(1, 2)操作可行?

B以public继承A的protected函数意味着B能访问A的protected函数,该函数在B中的访问限制为protected,所以在B中是可以调用A的构造函数的,能完成变量的初始化。这里有点难理解,属于继承中比较绕的部分了

(3)A a = b是什么操作?

这涉及一个新的概念:赋值兼容转换。这是子类给父类赋值的一种操作。在这里b是子类对象,a是父类对象。子类可给父类赋值,相当于将子类中的父类继承给它的那部分数据赋值给它。我们还可以使用父类指针或引用,指针就是指向子类的父类部分,当修改父类指针的指向内容时,子类的内容也被修改了。引用也是指向子类的父类部分,相当于给子类的父类部分取别名。

赋值兼容转换类似于将子类切出一部分给父类,所以这个过程又叫切割或切片

(4)区分赋值兼容转换和隐式类型转换

最本质的区别在于两者的底层操作不同。对于赋值兼容转换来说,无论是赋值、指针、引用,都不会在中间生成临时变量,赋值就是截断后将子类数据拷贝给父类,指针和引用都是直接让父类指向子类对应部分的。而变量隐式类型转换会先用一个临时变量存储类型提升、截断后的数据,再将这个数据赋值回去,这个临时变量的有无是区分两者的关键。

对于赋值兼容转换来说,没有临时变量意味着当父类引用时不会出现权限缩小的情况,即我们可以通过父类引用修改子类的值。

而对于其他变量来说,只要左右数据类型不同就会发生隐式类型转换,中间会产生临时变量,这个变量是常属性的,会导致必须加const才能引用

(5)赋值兼容转换注意事项

a.赋值兼容转换是一个单独的语法规则,和隐式类型转换没有任何关系,前面已经很充分的分析了

b.赋值兼容转换只能在public继承方式下可用

只要继承不是public,无论访问限定符是什么,都不行

c.赋值兼容转换可以跨级

但必须保证子赋值给父,中间继承方式只能是public

2.拷贝构造

看一下下面的代码,尝试理解继承下的拷贝构造



#include <iostream>
using namespace std;

class A
{
public:
	A() = default;
	A(const A& a)
		:_a(new int[10])
	{
		for (int count = 0; count < 10; count++)
		{
			_a[count] = a._a[count];
		}
	}

	int* _a = new int[10] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
};

class B : public A
{
public:
	B() = default;

	B(const B& b)
		:A(b)
		,_b(new int[10])
	{
		for (int count = 0; count < 10; count++)
		{
			_b[count] = b._b[count];
		}
	}

	int* _b = new int[10];
};

int main()
{
	B b1;
	B b2 = b1;
	b1._a[0] = 1, b2._a[0] = 2;
	b1._b[0] = 3, b2._b[0] = 4;

	return 0;
}

继承下的六大成员函数都和普通类的成员函数大差不差,只要不显式写,都是默认调用父类的对应的默认成员函数,所以我们需要注意的就是当遇到深拷贝时要会显式地去写拷贝构造

其中特别注意子类的拷贝构造利用了赋值兼容转换,需要着重理解

3.多继承

(1)多继承理解

C++支持多继承,即一个子类继承多个父类,享有多个父类的成员函数和成员变量。在语法上只需要用逗号隔开,分别写继承方式和继承的父类,在特性上和单继承基本上没区别,前面理解了这里就非常简单。

(2)菱形继承

这是在谈到多继承时不得不面对的问题——菱形继承。我们刚刚理解多继承觉得没啥问题,不就是单继承的翻版吗?但下面这种情况就有问题了。

我们会发现,BCD都没有问题,但E就有问题了,E通过两条继承关系链继承了两次A,意味着E里面有两份A的成员变量,这是单继承永远碰不到的情况,这叫数据冗余。

在数据冗余的前提下,E想要初始化或者访问A继承下来的成员变量时,也不知道到底要访问哪一份,这叫做二义性。

(3)二义性

观察下面的代码,看看如何消除二义性。

#include <iostream>
using namespace std;

class A
{
public: 
	int _a;
};

class B : public A
{};

class C : public B
{};

class D : public A
{};

class E : public C, public D
{};

int main()
{
	E e;
	e.D::_a = 1, e.C::_a = 2;
	cout << e.D::_a << " " << e.B::_a << endl;
	return 0;
}

执行结果

我们发现,只有E会产生二义性,而BC和D分别对应的A是唯一的,由于E同时继承了BCD,我们只需要在BCD中访问_a就可以避免歧义,即确定类域

(4)数据冗余和虚拟继承

二义性解决了,但数据冗余仍然存在。事实上,我们发现当B和D出现的时候,数据冗余就已经无法避免了

所以源头是在B和D身上,我们是否可以使用一种办法,让B和D中的A实际上只有一份呢,这就要使用到virtual,即虚拟继承。

我们先看看在刚才代码的基础上给B和D使用虚拟继承后会发生什么?


#include <iostream>
using namespace std;

class A
{
public: 
	int _a;
};

class B : virtual public A
{};

class C : public B
{};

class D : virtual public A
{};

class E : public C, public D
{};

int main()
{
	E e;
	e.D::_a = 1, e.C::_a = 2;
	cout << e.D::_a << " " << e.B::_a << endl;
	return 0;
}

结果是

可以看到,当使用虚拟继承后,BD共享一个A,即整个继承体系中只有一份A,这就避免了数据冗余的情况。

在实际情况中,我们应当慎重使用多继承,一定要提防菱形继承,尽量不要使用,就算我们有菱形虚拟继承来解决这个问题。在很多语言中都没有多继承这个概念了,就是因为菱形继承带来的很多冗杂的语法处理和理解难度。

4.继承与组合

(1)继承和组合的区分和选择问题

继承上面已经讲了很多,不再重复。组合就是在一个类里使用另一个类作为它的成员变量,这种写法之前我们见过很多次了,在很多语法讲解上都使用了组合。我们现在需要思考:组合和继承的区别是什么?在两者之间怎样选择?

组合和继承的本质都是复用。继承语法中父子的关系通常是:子是父,子是父属性的一种延伸;

而组合通常是:A的组成中含有B,A的组成中含有C。继承是is关系,组合是has关系。

在选择过程中,如果两者表现出强is关系,那就使用继承,如植物和水果,动物与人。如果表现出强has关系,就使用组合,如汽车和汽油,自行车和轮胎。如果呈现两者均可的关系(如stack含有vector,stack就是vector),那么就优先使用组合。

(2)黑箱复用和白箱复用

继承作为面向对象编程三大特性之一,为什么反而推荐优先使用组合呢?这需要涉及黑箱白箱(黑盒白盒)的概念。

继承本质上是一种白箱复用,而组合是黑箱复用。

黑箱是不可见的,就像当我们使用组合时,被使用的类的细节我们是不知道的,但我们会用,如STL很少有人能讲清楚里面的细节。当使用黑箱复用时,我们关注的是这个类的功能,有什么接口,这是非常容易学习的。不仅如此,黑箱复用还将封装的特性完美展示了出来,我们只能调用该类向外展示的函数接口,其余细节完全不知道,因此如果这个类出现了毛病,有人去维护,它们只需要修bug就行了,只需要保证对外提供的接口是安全的,功能是正常的就可以了。

白箱复用是可见的,就像当我们使用继承时,父类除private以外其余的成员我们都可以直接调用。我们书写的代码是嵌在父类上的,这种模式下封装的特性就明显被弱化了。父类中的protected成员对外不开放但对子开放,就会导致当父类出现毛病时,有人去修改时,如果动了protected成员函数,那么子类的代码会立马失效,而一般来讲,protected成员更倾向于类功能的底层,函数更多,父类的书写者不可能去考虑子类调用的情况来适配,这对于书写子类功能的人来说无疑是一场灾难。这也是高内聚(函数功能集中),低耦合(类与类之间的关系不紧密)的反面教材。

白盒测试和黑盒测试也遵循上面的规则。黑盒测试注重对功能,接口的测试;而白盒测试注重于观察底层代码细节,从源码入手测试。白盒测试相比较黑盒测试更轻松。

综上,我们可以充分理解为什么推荐组合而不是继承。总的来说继承是以牺牲一定封装特性换来的,而随着我们深入学习,我们会意识到,封装才是编程思想的老大,任何破坏封装特性的行为都要深思熟虑。当然,如果确实存在强is关系时,继承依然可靠。

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

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

相关文章

PostgreSQL 中如何解决因频繁的小事务导致的性能下降?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 PostgreSQL 中解决因频繁小事务导致性能下降的方法 PostgreSQL 中解决因频繁小事务导致性能下降的方法…

使用机器学习 最近邻算法(Nearest Neighbors)进行点云分析 (scikit-learn Open3D numpy)

使用 NearestNeighbors 进行点云分析 在数据分析和机器学习领域&#xff0c;最近邻算法&#xff08;Nearest Neighbors&#xff09;是一种常用的非参数方法。它广泛应用于分类、回归和聚类分析等任务。下面将介绍如何使用 scikit-learn 库中的 NearestNeighbors 类来进行点云数…

C++ | Leetcode C++题解之第233题数字1的个数

题目&#xff1a; 题解&#xff1a; class Solution { public:int countDigitOne(int n) {// mulk 表示 10^k// 在下面的代码中&#xff0c;可以发现 k 并没有被直接使用到&#xff08;都是使用 10^k&#xff09;// 但为了让代码看起来更加直观&#xff0c;这里保留了 klong l…

优化器算法

优化器算法 梯度下降算法 首先引用动手学深度学习中对梯度下降算法的直观理解与推导。说明了不断的迭代可能会使得f(x)的值不断下降&#xff0c;从直观上解释了梯度下降的可能性。 将损失函数在x点处一阶泰勒展开。 f ( x ϵ ) f ( x ) ϵ f ′ ( x ) O ( ϵ 2 ) . f(x\eps…

在InternStudio上创建一台GPU服务器

填写配置 创建完成 ssh连接&#xff0c;并测试常用指令 查看开发机信息 查看gpu信息 创建conda环境 跑个test

可重入锁深入学习(有码)

【摘要】 ​今天&#xff0c;梳理下java中的常用锁&#xff0c;但在搞清楚这些锁之前&#xff0c;先理解下 “临界区”。临界区在同步的程序设计中&#xff0c;临界区段活称为关键区块&#xff0c;指的是一个访问共享资源&#xff08;例如&#xff1a;共享设备或是共享存储器&a…

9. Python的魔法函数

Python中的魔法函数 在Python中魔法函数是在为类赋能&#xff0c;使得类能够有更多操作。通过重写类中的魔法函数&#xff0c;可以完成很多具体的任务 1. __str__ 通过str魔法函数&#xff0c;可以设置对类的实例的 print() 内容 2. __len__ 通过len魔法函数&#xff0c;可…

tessy 集成测试:小白入门指导手册

目录 1,创建集成测试模块且分析源文件 2,设置测试环境 3,TIE界面设置相关函数 4,SCE界面增加用例 5,编辑数据 6,用例所对应的测试函数序列 7,添加 work task 函数 8,为测试场景添加函数 9,为函数赋值 10,编辑时间序列的数值 11,执行用例 12,其他注意事项…

计算机毕设:服装购物管理系统(Java+Springboot+MySQL+Tomcat),完整源代码+数据库+毕设文档+部署说明

本文关键字&#xff1a;Java编程&#xff1b;Springboot框架&#xff1b;毕业设计&#xff1b;毕设项目&#xff1b;编程实战&#xff1b;医护人员管理系统&#xff1b;项目源代码&#xff1b;程序数据库&#xff1b;毕设文档&#xff1b;项目部署说明&#xff1b; 一、项目说…

Java中JUC包详解

文章目录 J.U.C.包LockReadWriteLockLockSupportAQSReentrantLock对比synchronized加锁原理释放锁原理 CountDownLatchCyclicBarrierSemaphore J.U.C.包 java.util.concurrent&#xff0c;简称 J.U.C.。是Java并发工具包&#xff0c;提供了在多线程编程中常用的工具类和框架&a…

实战检验:Orange Pi AIpro AI开发板的性能测试与使用体验

文章目录 前言Orange Pi AIpro 简介Orange Pi AIpro 体验将Linux镜像烧录到TF卡YOLO识别视频中物体肺部CT识别 Orange Pi AIpro 总结 前言 Orange Pi AIpro&#xff0c;作为首款基于昇腾技术的AI开发板&#xff0c;它集成了高性能图形处理器&#xff0c;配备8GB/16GB LPDDR4X内…

MySQL复合查询(重点)

前面我们讲解的mysql表的查询都是对一张表进行查询&#xff0c;在实际开发中这远远不够。 基本查询回顾 查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还要满足他们的姓名首字母为大写的J mysql> select * from emp where (sal>500 or jobMANAGER) and ename l…

强化学习:bellman方程求解state value例题

最近在学习强化学习相关知识&#xff0c;强烈推荐西湖大学赵世钰老师的课程&#xff0c;讲解的非常清晰流畅&#xff0c;一路学习下来令人身心大爽&#xff0c;感受数学抽丝剥茧&#xff0c;化繁为简的神奇魅力。 bellman方程还是比较容易理解的&#xff1a;当前状态下的state …

嵌入式linux系统中GDB调试器详解

前言 GDB全称GNU symbolic debugger,它是诞生于GNU开源组织的(同时诞生的还有 GCC、Emacs 等)UNIX及UNIX-like下的调试工具,是Linux下最常用的程序调试器,GDB 支持调试多种编程语言编写的程序,包括C、C++、Go、Objective-C、OpenCL、Ada 等。但是在实际应用中,GDB 更常…

linux_进程周边知识——理解冯诺依曼体系结构

前言&#xff1a; 本篇内容是为了让友友们较好地理解进程的概念&#xff0c; 而在真正了解进行概念之前&#xff0c; 要先了解一下冯诺依曼体系结构。 所以博主会先对冯诺伊曼体系结构进行解释&#xff0c; 然后再讲解进程的概念。 ps&#xff1a; 本篇内容适合了解一些linux指…

github中下载zip后,本地仓库如何与github上的项目相关联

有时候网速问题&#xff0c;git clone 太慢&#xff0c;就直接下载zip文件&#xff0c;然后再进行关联 1、下载zip 2、解压&#xff0c;把文件夹名称中-main去掉 3、进行关联 cd <repo> git init git add . git remote add origin https://github.com/<user>/&l…

springboot在线教育平台-计算机毕业设计源码68562

摘要 在数字化时代&#xff0c;随着信息技术的飞速发展&#xff0c;在线教育已成为教育领域的重要趋势。为了满足广大学习者对于灵活、高效学习方式的需求&#xff0c;基于Spring Boot的在线教育平台应运而生。Spring Boot以其快速开发、简便部署以及良好的可扩展性&#xff0c…

第一个基于FISCOBCOS的前后端项目(发行转账)(已开源)

本文旨在介绍一个简单的基于fiscobcos的前后端网站应用。Springbootjs前后端不分离。 所使用到的合约也是一个最基本的。首先您需要知道的是完整项目分为三部分&#xff0c;1是区块链平台webase搭建&#xff08;此项目使用节点前置webase-front即可&#xff09;&#xff0c;2是…

帕金森病患者在日常饮食中需要注意哪些特殊的营养需求?

帕金森病患者的特殊营养需求 帕金森病患者在日常饮食中需要特别注意以下几个方面的营养需求&#xff1a; 蛋白质摄入&#xff1a;由于帕金森病药物可能与蛋白质竞争同一种转运蛋白进入大脑&#xff0c;因此建议将蛋白质的摄入量分散在一天中的多餐中&#xff0c;避免集中在单一…

【python学习】多线程编程的背景、定义、特点、优缺点、使用场景和示例以及和单线程的区别

引言 随着计算机技术的发展&#xff0c;多核处理器已经成为了主流,为了充分利用多核处理器带来的并行计算能力&#xff0c;提高程序的执行效率和响应速度&#xff0c;多线程编程变得尤为重要 Python作为一种高级编程语言&#xff0c;提供了多线程编程的支持&#xff0c;允许开发…