C++继承相关总结

文章目录

  • 前言
  • 1.继承的相关概念
    • 1.继承概念
    • 2.继承的相关语法
    • 3.基类和派生类对象赋值转换(赋值兼容规则)
  • 2.继承中的注意事项
    • 1.继承中的作用域
    • 2.派生类的默认成员函数
      • 1.构造函数与拷贝构造
      • 2.赋值重载与析构
    • 3.友元关系与静态成员变量
  • 3.多继承(菱形继承)
    • 1.虚拟继承
    • 2.虚拟继承的背后原理
  • 4.继承总结与反思
    • 1. 继承和组合

前言

本文主要是对C++三大特性之一的继承进行讲解。将会围绕继承的概念,继承的语法,继承方式,多继承中的菱形继承以及产生的问题进行介绍。

1.继承的相关概念

1.继承概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

2.继承的相关语法

在对继承语法进行介绍之前,我们先看看如下示例代码。

#include<iostream>
using namespace std;
class Person
{
public:
	void Print()
	{
		cout << _name <<" " << _age << endl;;
	}
private:
	string _name ="aaa";
     int _age=11;
};

class Student :public Person
{
   
private: 
	int _id;
};
int main()
{
	Student s;
	s.Print();
}

在这里插入图片描述

通过上述简单示例我们不难看出继承就是一种代码复用的手段。students类被称为派生类,也被称为子类,Preson被称为父类也叫做基类。这里继承方式是public公有继承。 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这点和我们现实世界比较像比如儿子继承父亲的遗产。这里继承被分为3种方式,分别是public共有继承,protected保护继承,private私有继承。

在这里插入图片描述

这里我们可以看出如下几点:1基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它.通俗来说不可见就是不能用。2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。3.基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

其实我们这里可以看出父类中的被private修饰的成员是用来防儿子的,protected修饰的成员是用来防外人的。


3.基类和派生类对象赋值转换(赋值兼容规则)

派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。但是要注意,基类对象不能赋值给派生类对象。也就是说父不能给给子,但是子可以给给父。可以抽象的理解为不能啃老。这里发生赋值兼容的时候并没有发生隐式类型转换,没有产生临时变量,这里是天然支持的。

在这里插入图片描述

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。也就说基类的指针的是可以通过强制类型转化赋值给一个派生类指针,但是不建议这么做,最好是基类指针指向的一个派生类对象。因为派生类的对象的空间的会比基类大,这样做可能会引发越界访问的问题。

在这里插入图片描述

2.继承中的注意事项

1.继承中的作用域

在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。注意在实际中在继承体系里面最好不要定义同名的成员。

在这里插入图片描述

这里Student中有_name的成员变量,但是父类也有一个_name的成员变量。如果不指定的话默认是Studnet对象在调用Print函数时默认是打印Student类中的成员变量_name,这里是根据就近原则来的。

在这里插入图片描述

这里父类和子类都有一个函数Print这里子类在调用Print函数会调用子类自己的Print函数,这里就会构成函数隐藏。这里不是函数重载,函数重载的前提条件是在同一作用域下的两个函数。子类和父类的函数名相同就会构成隐藏。

在这里插入图片描述

构成函数隐藏后如果想使用父类的成员函数必须指定类域,不然就会程序报错。

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

1.构造函数与拷贝构造

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

在这里插入图片描述

从上述代码中不难看出无论如何子类从父类那里继承得来的变量都会先调用父类构造函数先初始化。

同样的子类的拷贝构造函数必须调用父类的拷贝构造完成继承下来的父类的成员变量拷贝初始化。

在这里插入图片描述

我们之前讲了父类与子类的赋值兼容规则,因此s虽然是子类类型的引用,但是通过切片处理依然可以调用父类的拷贝构造进行初始化。

2.赋值重载与析构

派生类的operator=也必须要调用基类的operator=完成基类的复制,但是有个点要注意:因为父类与子类赋值重载的函数名是一样的,会构成隐藏,这样就得显示指定一下作用域。

在这里插入图片描述

析构函数就和之前的有所区别了,子类的析构函数不用我们显示写,子类对象在调用完析构函数后,编译器会自动调用父类的析构函数再去析构从父类那里继承下来的成员。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。因为对于子类对象来说,我们是先构造从父类那里继承下来的成员,之后在构造子类自己的成员。我们知道先构造的应该被后析构所以说这里是为了保证析构顺序。

3.友元关系与静态成员变量

在这里插入图片描述

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。抽象理解为:父亲的朋友不是儿子朋友。

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。静态成员变量对子类来说只有一份示例 对于父类来说也是只有一份实例 静态成员这份实例是被父类和子类共享的,静态成员变量是属于类的被所有对象共享。

在这里插入图片描述

在这里插入图片描述


补充一道很有意思的题目

如何设计一个不能被继承的类呢?其实我们把该类的构造函数或者析构函数设为私有即可。

在这里插入图片描述

那可能会有人问我怎么创建销毁对象,这里我们可以提供一个静态方法就行了。比如我以构造函数为例。

在这里插入图片描述

这样设计出了一个不能被继承的类。

3.多继承(菱形继承)

在这里插入图片描述

以上都是单继承,继承还可以多继承。单继承:一个子类只有一个直接父类时称这个继承关系为单继承。 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

多继承但是会引发一个特别的大的问题,就是菱形继承。菱形继承是一种特殊的多继承,即两个派生类继承同一个基类,同时两个派生类又作为基类继承给另一个派生类。

在这里插入图片描述

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。

代码示例

#include<iostream>
using namespace std;
class A
{
public:
	int _a;
};

class B :  public  A
{
public:
	int _b;
};
class C : public A
{
public:
	int _c;
};
class D :public B, public C
{
public:
	int _d;
};
int main()
{
	D d1;
	d1.B::_a = 1;
	d1.C::_a = 2;
	d1._b = 3;
	d1._c = 4;
	d1._d = 5;

}

菱形继承的对象成员模型图
在这里插入图片描述

这里就看出来了D类中有两份a,这样就会数据冗余造成空间资源的浪费。同时也会造成二义性,我们访问_a的时候到底是访问哪个_a呢?也许有人会说我们指定一下类域不就行了,但是空间资源的浪费怎么解决呢?而且正常来说我们肯定是想只有一份_a数据,就像我们只有只有唯一一个身份证号一样。为了解决菱形继承引发的问题,C++中引入了虚拟继承的概念。

1.虚拟继承

我们先来看看虚拟继承的语法在来探究一下虚拟继承背后的原理。

在这里插入图片描述

我们看到D的父类B,父类C通过关键字virtual采用虚拟继承的方式去继承A类,这样我们D类在继承BC的时候就不会有数据冗余和二义性的问题了。要注意的是虚拟继承不要在其他方去使用。

2.虚拟继承的背后原理

在这里插入图片描述

vs的监视窗口不能很好的观察,我将通过vs的内存窗口去观察一下这个D的对象模型。

在这里插入图片描述

从上图中我们可以看到D从b那里继承了_b外还有一个指针,这个指针被称为虚基指针,这个指针是指向一个虚基表这个表里存放相对于_a位置的偏移量,同样的对C来说也是如此。虚拟继承的原理是在派生类中添加一个虚基指针(vbp),指向一个虚基表(vbt),虚基表中存放了基类的偏移量。当派生类的对象要访问基类的成员时,就可以通过虚基指针和虚基表找到基类的地址,从而访问基类的成员。当D类的对象要访问A类的成员时,就可以通过虚基指针和虚基表找到A类的地址,从而访问A类的成员。

关于虚基类与虚基表的补充

上述例子中A是虚基类,因为它被B类和C类以虚拟继承的方式继承了。虚基类是指在菱形继承结构中,被多个派生类以虚拟继承的方式共同继承的基类。虚基表中存放的是虚基类相对于派生类对象的偏移量,虚基表不在虚基类或派生类里,而是在额外的空间中。虚拟继承确实会增加一些空间开销,因为每个虚拟继承的类都需要一个虚基指针和一个虚基表。但是,虚拟继承也可以避免菱形继承造成的数据冗余和二义性的问题,因为虚基类只会在最终的派生类中存在一份拷贝。

使用虚拟继承的派生类才有自己的虚基表。例如,对于A类,它是一个虚基类,但它并没有自己的虚基表,也没有虚基指针。对于B类和C类,它们是使用虚拟继承的派生类,所以它们有自己的虚基表和虚基指针。对于D类,它是一个多重继承的派生类,它也有自己的虚基表和虚基指针,但是它只继承了B和C的虚基指针,而不是A的。虚基指针除了指向虚基表外,还有一个作用是用于确定对象的类型。当一个对象被转换为一个虚拟继承的基类的指针时,它的虚基指针会被调整为指向该基类的虚基表。这样就可以通过虚基指针来判断对象的真实类型。D * d = new D();B * a = d;这个时候d的虚基指针会调整为指向B的虚基表。

补充一道面试题

class A
{
public:
	A(string s)
	{
		cout << s << endl;
	}

};
class B :virtual public A
{
public:
	B(string s1,string s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class C:virtual public A
{
public:
	C(string s1, string s2)
		:A(s1)
	{
		cout << s2 << endl;
	}

};
class D :public B, public C
{
public:
	D(string s1, string s2, string s3, string s4)
		:
		C(s1,s3),
		B(s1, s2),
		A(s1)
		
	{
		cout<<s4<<endl;
	}
};
int main()
{
	D d1("classA","classB","classC","classD");
}

这里打印结果是什么呢?实现是用菱形继承所以D构造的的时候对于s1 s2 s3 s4来说只会调用一次构造函数,也就是说s1会被A的构造函数调用打印,s2会被B的构造函数调用打印,s3会被C的构造函数调用打印,我们知道初始化列表的顺序是声明顺序。所以这里A先被继承之后再是BC被继承,所打印结果是classA clasB classC classD.

总结

在上述例子中B C虚拟继承A,B C中就会各自一个虚基指针指向各自的虚基表。D继承BC的时候会继承BC的虚基指针,也就是说D的虚基指针是BC虚基指针的组合,同样的D的虚基表也就是B C虚基表的组合。这样就解决了数据冗余问题和二义性。

4.继承总结与反思

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承否则在复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

1. 继承和组合

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。简单来说,就是车是交通工具这就是is-a的关系,车里有发动机这就是has-a的关系。

优先使用对象组合,而不是类继承 。继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系如果可以用继承,也可以用组合,首选用组合。

以上内容如有问题,欢迎指正!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

栈和队列OJ题合集(包含循环队列的两种实现)

目录 一:前言 二:有效的括号(括号匹配) 三:用队列实现栈 四:用栈实现队列 五:设计循环队列 一:前言 对栈和队列的基本性质和实现有问题的可以看上一期 链接&#xff1a;http://t.csdn.cn/YQMBA​​​​ 注意:本文用数据的大小来表示入栈入队的先后。 二:有效的括号(括号匹配…

fastp软件介绍

fastp软件介绍1、软件介绍2、重要参数解析2.1 全部参数2.2 使用示例2.3 重要参数详解&#xff08;1&#xff09;UMI去除&#xff08;2&#xff09;质量过滤&#xff08;3&#xff09;长度过滤&#xff08;4&#xff09;低复杂度过滤&#xff08;5&#xff09;adapter过滤&#…

《文章复现》考虑用户舒适度的冷热电多能互补综合能源系统优化调度

说明书 免费&#xff1a;https://download.csdn.net/download/qq_50594161/87625438 MATLAB代码&#xff1a;考虑用户舒适度的冷热电多能互补综合能源系统优化调度 关键词&#xff1a;用户舒适度 综合能源 PMV 优化调度 参考文档&#xff1a;《冷热电气多能互补的微能源网鲁…

什么是RabbitMQ?有什么用如何使用?一文回答

RabbitMQ RabbitMQ channel&#xff1a;操作MQ的工具exchange&#xff1a;交换机&#xff0c;路由消息到队列中queue&#xff1a;队列&#xff0c;缓存消息virtual host&#xff1a;虚拟主机&#xff0c;对queue&#xff0c;exchange等资源的逻辑分组 MQ模型 基本消息队列工作…

Java 8 - Lambda 表达式

1. 函数式接口 当一个接口中只有一个非 default 修饰的方法&#xff0c;这个接口就是一个函数式接口用 FunctionalInterface 标注 1&#xff09;只有一个抽象方法 FunctionalInterface public interface MyInterface {void print(int x); } 2&#xff09;只有一个抽象方法和…

射频接收机概述

接收机架构 射频接收机架构是指电子设备中用于接收无线电信号的部分。它通常由前置放大器、中频放大器、混频器、局部振荡器和带通滤波器等组成。以下是一个基本的射频接收机架构&#xff1a; 前置放大器&#xff1a;前置放大器的作用是放大接收天线接收到的微弱无线电信号&am…

程序员万万不能去的3种公司,越做越倒退,过来人的经验

俗话说“条条大路通罗马”&#xff0c;但是对于程序员来说&#xff0c;有些路千万别走&#xff0c;走得越久越难以抽身&#xff0c;甚至说毁掉你的职业生涯。 今天来跟大家讲一下&#xff0c;作为程序员&#xff0c;有些公司千万不要进去&#xff0c;你以为稀松平常&#xff0…

用Python发送电子邮件?这也太丝滑了吧(21)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 欢迎和猫妹一起&#xff0c;趣味学Python。 今日主题 猫爸赚钱养家&#xff0c;细想起来真的不容易啊&#xff01; 起早贪黑&#xff0c;都是6点早起做早饭&#xff0c;送…

Autodesk AutoCAD 2023(CAD设计软件)自动化工具介绍以及图文安装教程

Autodesk AutoCAD是一款功能强大的计算机辅助设计软件&#xff0c;主要用于2D和3D设计、制图和草图。它适用于多种行业&#xff0c;包括建筑、土木工程、机械工程、电气工程等等。 Autodesk AutoCAD具有2D和3D设计、多种工具和功能、可扩展性、与其他Autodesk软件集成和多平台…

记录一次解决Maven问题的坑

记录一次解决Maven问题的坑目录概述需求&#xff1a;设计思路实现思路分析1.一步步的解决问题比较方法2.后来感觉和这个没关系3.最后查询资料拓展实现性能参数测试&#xff1a;参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfec…

python get方法及常用的代码

1.首先&#xff0c;我们需要下载一个 Python的 pygame库。 2.接着&#xff0c;我们需要在 Pygame中去注册一个自己的账户。 3.登录成功后&#xff0c;我们就可以去下载 pygame中的文件了。 4.我们现在只需要将下载文件放入到 Pygame库中即可&#xff0c;这就完成了下载&#xf…

算法学习day43

算法学习day431. 力扣1049. 最后一块石头的重量 II1.1 分析1.2 代码2. 力扣494. 目标和2.1 分析2.2 代码3. 力扣474.一和零3.1 分析3.2 代码4.参考资料1. 力扣1049. 最后一块石头的重量 II 1.1 分析 动规五部曲&#xff1a; 1.确定dp数组以及下标的含义 dp[j]表示容量为j的背…

第⑦讲:Ceph集群RGW对象存储核心概念及部署使用

文章目录1.RadosGW对象存储核心概念1.1.什么是RadosGW对象存储1.2.RGW对象存储架构1.3.RGW对象存储的特点1.4.对象存储中Bucket的特性1.4.不同接口类型的对象存储访问对比2.在集群中部署RadosGW对象存储组件2.1.部署RGW组件2.2.集群中部署完RGW组件后观察集群的信息状态2.3.修改…

剑指offer JZ27 二叉树的镜像

Java JZ27 二叉树的镜像 文章目录Java JZ27 二叉树的镜像一、题目描述二、辅助栈三、递归法使用辅助栈和递归法解决剑指offer JZ27 二叉树的镜像的问题。 一、题目描述 操作给定的二叉树&#xff0c;将其变换为源二叉树的镜像。   数据范围&#xff1a;二叉树的节点数 0≤n≤…

--编写一个存储过程,输入一个日期,返回该日期与当下日期的时间差,如果该差是负的,则提示该日期已经过去XX天,不然提示距离该日期还有xx天

--创建存储过程&#xff0c;一个输入参数&#xff0c;一个输出参数 create or replace procedure sp_minus(i_date varchar2,o_minus out varchar2) is --声明一个变量&#xff0c;用来存放异常 v_errm varchar2(200); begin --判断输入格式 if length(i_date)<>8 th…

Redis主从复制

文章目录定义用途怎么使用案例演示三大命令&#xff1a;修改配置文件细节常见方式一主二仆薪火相传反客为主复制原理和工作流程主从复制的缺点定义 主从复制&#xff0c;master以写为主&#xff0c;slave以读为主&#xff0c;当master数据变化的时候&#xff0c;自动将新的数据…

十分钟搞懂Java限流及常见方案

目录限流基本概念QPS和连接数控制传输速率黑白名单分布式环境限流方案常用算法令牌桶算法漏桶算法滑动窗口常用的限流方案Nginx限流中间件限流限流组件合法性验证限流Guawa限流网关层限流从架构维度考虑限流设计限流基本概念 QPS和连接数控制 传输速率 黑白名单 分布式环境…

HTML5 <abbr> 标签 和 HTML5 <applet> 标签

标签定义及使用说明 <abbr> 标签用来表示一个缩写词或者首字母缩略词&#xff0c;如"WWW"或者"NATO"。 通过对缩写词语进行标记&#xff0c;您就能够为浏览器、拼写检查程序、翻译系统以及搜索引擎分度器提供有用的信息。 实例 被标记的缩写词如…

《程序员面试金典(第6版)》面试题 08.04. 幂集(回溯算法,位运算,C++)不断更新

题目描述 幂集。编写一种方法&#xff0c;返回某集合的所有子集。集合中不包含重复的元素。 说明&#xff1a;解集不能包含重复的子集。 示例: 输入&#xff1a; nums [1,2,3] 输出&#xff1a; [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ] 解题思路与代码 其实…

博客让谷歌或是百度收录

参考以下大佬的博客教程 Hexo框架(六)&#xff1a;SEO优化及站点被搜索引擎收录设置 | 你真是一个美好的人类 第一步 安装百度和 Google 的站点地图生成插件&#xff1a; npm install hexo-generator-baidu-sitemap --save npm install hexo-generator-sitemap --save 然后来…