【C++】继承基础知识一遍过

目录

一,概念

二,继承定义

1. 继承格式 

2. 访问限定符与继承方式的关系

3. 继承父类成员访问方式的变化

小结: 

三. 父类与子类对象赋值转化

四,继承作用域

1.特点 

2. 测试题

五,派生类不一样的默认成员函数

1.构造函数

2.拷贝构造

3.赋值符号重载

4.析构函数

5. 小结

六,友元与继承

七,继承与静态成员

2.思考:如何制作一个无法被继承的类

八,菱形继承与虚拟菱形继承

1. 菱形继承的问题

2. 虚拟继承解决方案

3. 虚拟继承底层细节 

九,继承与组合

总结:

结语


一,概念

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

基于原有类的一般叫做基类(base_class),我感觉叫他为—— 父类 ,这更容易理解。
运行下面代码
#include <iostream>
#include <string>using namespace std;class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18;
};class Student : public Person
{
protected:int _stuid; // 学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};int main()
{Student s;Teacher t;s.Print();t.Print();t.Print();return 0;
}

继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用Print可以看到成员函数的复用

二,继承定义

1. 继承格式 

下面我们看到Person是父类,也称作基类。 Student是子类,也称作派生类  

2. 访问限定符与继承方式的关系

 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的写出继承方式。

3. 继承父类成员访问方式的变化

小结: 

1. 父类private成员在派生类中无论以什么方式继承都是不可见的。这里的 不可见是指父类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
2. 父类private成员在派生类中虽然被继承了,但父类私有是不可见的(因为权限问题),如果父类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。 可以看出保护成员限定符是因继承才出现的

 3. 子类访问权限 =   修饰符与继承方式的最小权限 ,如:修饰符是private,继承方式是public最终权限是private。 权限从大到小是:public > protected > private

4.在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。所以基本上,常用的处理 继承方式:public 处理成员变量,函数: 用public / protected

三. 父类与子类对象赋值转化

子类对象 可以赋值给 父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫 切片或者 切割。寓意把派生类中父类那部分切来赋值过去。
class Person
{
protected:string _name; // 姓名string _sex;int _age; // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};void Test()
{Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.父类对象不能赋值给子类对象// sobj = pobj;// 3.父类的指针可以通过强制类型转换赋值给子类的指针pp = &sobj;Student * ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_No = 10;// 父类利用指针强转给子类指针pp = &pobj;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_No = 10;   
}

我们可以发现:

1. 父类对象不能赋值给子类对象

2. 父类对象地址强转为子类地址,最终在子类访问其新成员时会发生越界报错。

四,继承作用域

1.特点 

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

2. 测试题

class A
{
public:void fun(int i){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" << i << endl;}
};void Test()
{B b;b.fun();
};
// 不定向选择:
// A: 子父func构成重载
// B: 子父func构成隐藏
// C: 程序报错
// D:以上都不对

答案:BC 。B,首先是子父类拥有同名成员函数,构成隐藏。C,因为子类对父类同名函数的隐藏,导致无法找到匹配fun函数,因此报错。

五,派生类不一样的默认成员函数

 这是本次的实验代码,经过这次的学习,我们来补充这个派生类。

class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:// 构造函数
protected:int _num; //学号
};void Test()
{Student a;int x = 1;
}int main()
{Test();return 0;
}

1.构造函数

 我们先不写构造函数,我们查看默认构造函数会发生什么事?

 很明显:默认构造函数,父类会调用其自己的构造函数;子类也是调用其自己的构造函数,没有,则编译器自动生成构造函数。(总之:各自构造各自的)且构造顺序为先父类——>子类。

    // 构造函数Student(const char* name ,const int num = 0):Person(name),_num(num){}

2.拷贝构造

 思路:父类数据,通过父类的隐式转化。

    // 拷贝构造Student(const Student& s1):_num(s1._num), Person(s1)    // 通过子类向父类的强转化{cout << "Student(const Student& )" << endl;}

3.赋值符号重载

 思路:调用父类赋值符号重载,再给子类成员变量赋值。

    // 赋值符号重载Student& operator=(const Student& s1){cout << "operator=" << endl;if (this != &s1){Person::operator=(s1);_num = s1._num;}	return *this;}

 补充:如果子类成员变量不需要深拷贝,其实不需要写拷贝,赋值符号重载函数,因为父类有这两个函数。

4.析构函数

 这个没啥好说的,父子类各自调用各自的析构函数。

析构顺序:子类先析构,当子类析构完成后再调用父类析构。

5. 小结

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator= 必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

六,友元与继承

如果大家对友元陌生了,可以看小博主这篇文章回忆:详解C++类和对象(下篇)——用代码实践功能_花果山~~程序猿的博客-CSDN博客

结论:派生类不能继承父类的友元关系,换句话说,友元函数或者友元类无法直接获取派生类的私有或者保护成员变量及函数。  

示例代码: 

class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
void main()
{Person p;Student s;Display(p, s);
}

友元关系无法继承,所以在派生类中如果需要友元关系,需要重新声明友元关系。 

七,继承与静态成员

静态成员变量与函数相关基础知识详解C++类和对象(下篇)——用代码实践功能_花果山~~程序猿的博客-CSDN博客

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

示例代码:

class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;cout << " Person接口 人数 :" << Person::_count << endl;Student::_count = 666; // 在Student类,访问Person类中静态成员变量,并且设置该静态成员变量cout << " Person接口 人数 :" << Person::_count << endl;cout << " Student接口 人数 :" << Student::_count << endl;cout << " Graduate接口 人数 :" << Graduate::_count << endl;
}

2.思考:如何制作一个无法被继承的类

 思路:对构造与析构函数其中一个私有化。

class  Person
{
public:~Person()	{}
private:Person(){}int _age = -1;char* _name;
};class Student : public Person
{
private:int _score;
};void TestPerson()
{Student a;Person  a1; // 当然,父类自己也实例不了对象了
}

诺,这样子类就调不动父类。但,同时父类自己也实例不了对象,你真的,哭死我了。

解决方案:创建一个静态成员函数,让类外就可以实例化对象,这样就绕开了创建对象时系统调用构造函数这条路。

    // 单纯的成员函数也不行,因为对象都没有你告诉咋调函数。static Person create_Person(){return Person();}

八,菱形继承与虚拟菱形继承


1. 菱形继承的问题

从下面的对象成员模型构造,可以看出菱形继承有 数据冗余 二义性 的问题。 Assistant 的对象中 Person 成员会有两份。这会
导致内存浪费!!!

2. 虚拟继承解决方案

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 Student Teacher 的继承 Person 时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用。

实验代码:

class A
{
public:int _a;
};// class B : public A
class B : virtual public A
{
public:int _b;
};// class C : public A
class C : virtual public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;cout << &(d.B::_a) << endl;d.C::_a = 2;cout << &(d.C::_a) << endl;d._b = 3;d._c = 4;d._d = 5;return 0;
}

虚拟菱形继承,让一个类通过虚拟继承从多个基类继承时,只会保留一个共同的基类子对象,避免了出现多个相同的基类子对象。

诺,数据冗余:

3. 虚拟继承底层细节 

 

这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?
这里是通过了 B C 的两个指针,指 向的一张表。这 两个指针 叫虚基表指针,这两个表叫 虚基表 。虚基表中存的偏移量。通过偏移量 可以找到下面的 A  

 这里会提到内存大小端知识,可以参见本文【C语言】整,浮点型数据存储,大小端。细节拉满!!_小端浮点数组_花果山~~程序猿的博客-CSDN博客

 原理精简图:

 补充:如果多个相同虚拟菱形继承类的对象,其访问的偏移表是同一套。如:D是一个虚拟菱形继承的类,则D   d1, d2, d3....., d1,d2,d3都是访问同一套偏移表。从这里我们也可以发现,虚拟菱形继承为了解决数据冗余和二义性的问题,需要访问偏移表,但毫无疑问,这会造成性能损失。

九,继承与组合

继承是一种关系,其中一个类(称为子类或派生类)可以继承另一个类(称为父类或基类)的属性和方法。

组合是另一种关系,其中一个类(称为容器类)包含另一个类的对象(称为成员类)。容器类通过创建成员类的对象来使用成员类的属性和方法。

(就像之前我们所学的STL中vector类这样的容器,里面放着string类)

总结:

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

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

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

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

相关文章

用深度强化学习来玩Chrome小恐龙快跑

目录 实机演示 代码实现 实机演示 用深度强化学习来玩Chrome小恐龙快跑 代码实现 import os import cv2 from pygame import RLEACCEL from pygame.image import load from pygame.sprite import Sprite, Group, collide_mask from pygame import Rect, init, time, display,…

centos7手动配置jdk1.8环境与maven环境

安装jdk1.8 链接&#xff1a;https://pan.baidu.com/s/1_P7jHzH4Lk2jcPWWD7pi4w 提取码&#xff1a;6kkm winscp软件上传压缩包到Linux中 解压 # 解压到/usr/local/java目录下 tar -zxvf jdk-8u381-linux-x64.tar.gz -C /usr/local/java配置环境变量 vi /etc/profile # 最后…

听觉刺激期间的神经血管耦合:ERPs和fNIRS血流动力学

导读 强度依赖性振幅变化(IDAP)已在事件相关电位(ERPs)中进行了广泛的研究&#xff0c;并与多种精神疾病相关联。本研究旨在探讨功能近红外光谱(fNIRS)在IDAP范式中的应用&#xff0c;该范式与ERPs相关&#xff0c;可以指示神经血管耦合的存在。两个实验分别有33和31名参与者。…

【C#】C#调用进程打开一个exe程序

文章目录 一、过程二、效果总结 一、过程 新建WinForm程序&#xff0c;并写入代码&#xff0c;明确要调用的程序的绝对路径&#xff08;或相对路径&#xff09;下的exe文件。 调用代码&#xff1a; 这里我调用的另一个程序的路径是&#xff1a; F:\WindowsFormsApplication2…

MPP 与 SMP 的区别,终于有人讲明白了【文末送书】

文章目录 导读01 SMP1. SMP 的典型特征2. SMP的优缺点 02 分布式MPP计算架构1. MPP 架构核心原理2. MPP 典型特征3. MPP优缺点 写作末尾 导读 当今数据计算领域主要的应用程序和模型可大致分为在线事务处理&#xff08;On-line Transaction Processing &#xff0c;OLTP&#…

如何为虚拟机添加磁盘,扩充原有分区的磁盘空间

如何为虚拟机添加磁盘&#xff0c;扩充原有分区的磁盘空间 关机新增磁盘 虚拟机关机的状态下&#xff0c;在 VMware 当中新增一块磁盘&#xff0c;选中左边要添加磁盘的虚拟机镜像&#xff0c;然后鼠标右键点击设置。 选中磁盘点击添加 点击下一步&#xff0c;悬着SCSI这个…

vue3 封装千分位分隔符自定义指令

toLocaleString作用&#xff1a;在没有指定区域的基本使用时&#xff0c;返回使用默认的语言环境和默认选项格式化的字符串。可点击进入MDN查看 // 千分位分隔符指令 import { Directive, DirectiveBinding } from vueconst thousandSeparator: Directive {mounted(el: any, …

好玩的js特效

记录一些好玩的js特效 1、鱼跳跃特效 引入jquery:https://code.jquery.com/jquery-3.7.1.min.js 源码如下&#xff1a; <!--引入jquery--> <script src"https://code.jquery.com/jquery-3.7.1.min.js"></script> <!--引入跳跃源码--> <s…

深入理解 JVM 之——字节码指令与执行引擎

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 类文件结构 Write Once&#xff0c;Run Anywhere 对于 C 语言从程序到运行需要经过编译的过程&#xff0c;只有经历了编译后&#xff0c;我们所编写的代码才能够翻译为机器可以直接运行的二进制代码&#x…

230. 二叉搜索树中第K小的元素

230. 二叉搜索树中第K小的元素 C代码&#xff1a;二叉树 int kthSmallest(struct TreeNode* root, int k){// struct TreeNode** stack malloc(sizeof(struct TreeNode*) * 10000); // root 是结构体的地址struct TreeNode* stack[10000];int stkTop 0;while (root ! NULL …

mysql 查询优化 、索引失效

查询优化 物理查询优化 通过索引和表连接方式等技术来进行优化&#xff0c;这里重点需要掌握索引的使用 逻辑查询优化 通过SQL 等价变换 提升查询效率&#xff0c;直白一点就是说&#xff0c;换一种查询写法执行效率可能更高 索引失效 计算、函数、类型转换&#xff08;自动或…

FasterNet(PConv)paper笔记(CVPR2023)

论文&#xff1a;Run, Don’t Walk: Chasing Higher FLOPS for Faster Neural Networks 先熟悉两个概念&#xff1a;FLOPS和FLOPs&#xff08;s一个大写一个小写&#xff09; FLOPS: FLoating point Operations Per Second的缩写&#xff0c;即每秒浮点运算次数&#xff0c;或…

【计算机网络】 确认应答机制与超时重传

文章目录 ACK机制——确认应答机制超时重传 ACK机制——确认应答机制 当我们客户端发送了一个数据&#xff0c;seq是1100&#xff0c;那么服务端在收到时就会回一个ack101的ACK包&#xff0c;代表101之前的包我都收到了&#xff0c;下面请你从101继续发送。然后客户端就会发送1…

Alins - 化繁为简、极致优雅的WebUI框架

最近造了个js框架 Alins&#xff0c;分享一下&#xff1a; &#x1f680; Alins: 最纯粹优雅的WebUI框架 English | 文档 | 演练场 | 更新日志 | 反馈错误/缺漏 | Gitee | 留言板 0 简介 0.1 前言 Alins是一款极致纯粹、简洁、优雅的Web UI框架。秉持0-API、Less is More 的…

慕尼黑主题活动!亚马逊云科技生成式AI全新解决方案,引领未来移动出行领域

IAA作为世界五大车展之一&#xff0c;一直对全球汽车产业的发展起着关键作用&#xff01;2023年9月5日在慕尼黑开幕的IAA MOBILITY 2023以“体验联动智慧出行”为主题&#xff0c;紧跟移动出行领域的前沿变化&#xff0c;将汇集整车企业、开发者、供应商、科技公司、服务提供商…

【小沐学Unity3d】3ds Max 多维子材质编辑(Multi/Sub-object)

文章目录 1、简介2、精简材质编辑器2.1 先创建多维子材质&#xff0c;后指定它2.2 先指定标准材质&#xff0c;后自动创建多维子材质 3、Slate材质编辑器3.1 编辑器简介3.2 编辑器使用 结语 1、简介 多维子材质&#xff08;Multi/Sub-object&#xff09;是为一个模形&#xff0…

tab切换,左右加箭头,点击箭头实现tab切换

和正常tab切换一样原理&#xff0c;点击箭头多了步计算 <template><div><div class"tab-container"><p>{{projectName}}</p><div class"banner"><div v-for"(tab, index) in tabs" :key"index&quo…

物联网世界的无线电报之MQTT详解

文章目录 1. 前言1.1. 物联网与MQTT的关系1.2. MQTT的重要性及应用场景 2. MQTT基础2.1. MQTT的定义与起源2.2. MQTT的工作原理2.3. MQTT的协议格式2.4. 用java造个轮子 3. 深入理解MQTT3.1. MQTT的主要组件3.1.1. Publisher&#xff08;发布者&#xff09;3.1.2. Subscriber&a…

群晖NAS:通过Docker 部署宝塔面板【注册表:cyberbolt/baota】

群晖NAS&#xff1a;通过 Docker 部署宝塔面板【注册表&#xff1a;pch18/baota】 由于 docker 源地址被墙&#xff0c;在面板里面查询不到注册表&#xff0c;使用 ssh 命令行拉取 1、打开 SSH&#xff0c;链接后打开命令行 这里不赘述&#xff0c;具体自行百度 2、下载 镜像…

笔试记录-扔鸡蛋问题

写目录 一个鸡蛋两个鸡蛋K个鸡蛋 今天面试官问了我这个扔鸡蛋问题&#xff0c;以前学过&#xff0c;但是面试的时候想不起来了&#xff0c;应该是直接寄了&#xff0c;接下来总结一下这个问题的动态规划做法. 问题&#xff1a;有一个N层高的楼&#xff0c;现在给你若干个鸡蛋&a…