【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.mzph.cn/news/73807.shtml

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

相关文章

基于深度学习的三维重建从入门实战教程 原理讲解 源码解析 实操教程课件下载

传统的重建方法是使用光度一致性等来计算稠密的三维信息。虽然这些方法在理想的Lambertian场景下,精度已经很高。 但传统的局限性,例如弱纹理,高反光和重复纹理等,使得重建困难或重建的结果不完整。 基于学习的方法可以引入比如镜面先验和反射先验等全局语义信息,使匹配…

Ribbon负载均衡+Nacos服务搭建

Ribbon负载均衡 流程 首先通过RibbonLoadBalanceerClient获取服务名&#xff0c;并传给DynamicServerListLoadBalancer——>通过EureKa-server获取服务名对应服务列表(也就是被注册到EureKa中的服务&#xff0c;可能包括不同端口的)&#xff0c;然后我们会根据IRule中的服务…

【python自动化】playwright长截图切换标签页JS注入实战

前言 当前教程使用的playwright版本为1.37.0,selenium版本为3.141.0 官方文档&#xff1a;https://playwright.dev/python/docs/screenshots 本教程目录如下 文章目录 前言playwright各类截图源码阅读ElementHandle类下的截图Page类下的截图Locator类下的截图 Playwright快速…

官方项目《内容示例》中Common UI部分笔记:Common UI 分场景使用教程

文章目录 前言0. 通用设置0.1 开启插件0.2 设置Viewport 1. 分场景教程1. 1 在仅使用鼠标控制的场景下Common Activatable StackCommon Activatable Widget 1.2 当焦点落到一个按钮时显示默认确认&#xff08;Click/Accept&#xff09;按键图标Common Input Action DataBaseInp…

【Mysql】数据库第二讲(数据库中数据类型的介绍)

数据类型 1.数据类型分类2.数值类型介绍2.1tinyint类型2.2bit类型介绍2.3小数类型介绍2.3.1 float2.3.2decimal 3.字符串类型介绍3.1char3.2varchar面试&#xff1a;char和varchar的区别 4.日期和时间类型5.enum和set 1.数据类型分类 2.数值类型介绍 2.1tinyint类型 数值越界测…

项目(智慧教室)第三部分,人机交互在stm32上的实现

一。使用软件 1.stm32cubemx中针对汉字提供的软件 2.对数据进行处理 2.上面点击ok--》这里选择确定 3.这里选择保存即可由字符库&#xff0c;但是需要占用内存太大&#xff0c;需35M&#xff0c;但是stm32只有几百k&#xff0c;所以需要自己删减。 生成中文字符&#xff08;用…

QTday3(QT实现文件对话框保存操作、实现键盘触发事件【WASD控制小球的移动】)

1.实现文件对话框保存操作 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }void Widget::on_fontBtn_clicked() {//调用QFo…

dll文件反编译源代码 C#反编译 dotpeek反编译dll文件后export

目录 背景下载安装dotpeek导入dll文件export导出文件参考 背景 项目合作的时候&#xff0c;使用前人的或者其他部门dll文件直接在机台运行&#xff0c;会出现很多问题&#xff0c;逻辑&#xff0c;效率等等&#xff0c;此时我们可以选择对他们的代码进行反编译和重构&#xff…

redisson分布式锁

RLock官网解释 基于Redis的Java分布式可重入锁对象&#xff0c;实现了锁接口。 如果获得锁的Redisson实例崩溃&#xff0c;那么这种锁可能永远挂起在获得状态。为了避免这种情况&#xff0c;Redisson维护了锁看门狗&#xff0c;它在锁持有者Redisson实例活着的时候延长锁过期时…

算法笔记:哈夫曼树、哈夫曼编码

1 字符的机内表示 2 前缀编码 字符只放在叶结点中字符编码可以有不同的长度由于字符只放在叶结点中&#xff0c;所以每个字符的编码都不可能是其他字符编码的前缀前缀编码可被惟一解码 3 哈夫曼树 哈夫曼树是一棵最小代价的二叉树&#xff0c;在这棵树上&#xff0c;所有的字…

SpotBugs(是FindBugs的继任者)安装、使用

SpotBugs介绍 SpotBugs和FindBugs的关系 SpotBugs是FindBugs的继任者&#xff0c;从SpotBugs停止的地方继续。 备注&#xff1a;FindBugs项目已经停止了&#xff0c;从2015年发布3.0.1版本以后再没有新的版本。 SpotBugs通过静态分析寻找java代码中的bug&#xff0c;通过发现…

lv3 嵌入式开发-9 linux TFTP服务器搭建及使用

目录 1 TFTP服务器的介绍 2 TFTP文件传输的特点 3 TFTP服务器的适用场景 4 配置介绍 4.1 配置步骤 4.2 使用 5 常见错误 1 TFTP服务器的介绍 TFTP&#xff08;Trivial File Transfer Protocol&#xff09;即简单文件传输协议 是TCP/IP协议族中的一个用来在客户机与服务器…

手机也可以搭建个人博客?安卓Termux+Hexo搭建属于你自己的博客网站【cpolar实现公网访问】

文章目录 前言 1.安装 Hexo2.安装cpolar3.远程访问4.固定公网地址 前言 Hexo 是一个用 Nodejs 编写的快速、简洁且高效的博客框架。Hexo 使用 Markdown 解析文章&#xff0c;在几秒内&#xff0c;即可利用靓丽的主题生成静态网页。 下面介绍在Termux中安装个人hexo博客并结合…

mysql 安全加固

PS&#xff1a;之前在做安全测试的时候&#xff0c;报告mysql有安全漏洞&#xff0c;于是研究了下如何修复&#xff0c;于是记录下来分享给大家 1.1修改mysql 存放位置 修复 1.停服务 service mysqld stop2.迁位置 2.1 新建迁移目录 mkdir /home/database2.2 迁移数据文件…

【MySQL】MySQL8.0安装教程

下载 MySQL官网下载安装包 安装 1、双击安装程序开始安装 2、选择安装类型 选Server only&#xff08;只安装mysql&#xff09;&#xff0c;然后点击“next”。 3、检测需要的安装&#xff0c; 直接点击Execute开始安装 4、点击next 5、点击next 6、next 7、密码验证方式&a…

macos13 arm芯片(m2) 搭建hbase docker容器 并用flink通过自定义richSinkFunction写入数据到hbase

搭建hbase docker容器 下载镜像 https://hub.docker.com/r/satoshiyamamoto/hbase/tags 点击run 使用镜像新建容器 填写容器名和 容器与宿主机的端口映射 测试 通过宿主机访问容器内的hbase webUI http://localhost:60010/master-status

I2C与I3C的对比

I2C与I3C的对比 电气特性 I2C 1.半双工 2.串行数据线(SDA)和串行时钟线(SCL) 3.数据线漏极开路&#xff0c;即I2C接口接上拉电阻 4.I2C总线运行速度&#xff1a;**标准模式100kbit/s&#xff0c;快速模式400kbit/s&#xff0c;快速模式plus 1Mbit/s&#xff0c;**高速模式…

深入探讨梯度下降:优化机器学习的关键步骤(三)

文章目录 &#x1f340;引言&#x1f340;随机、批量梯度下降的差异&#x1f340;随机梯度下降的实现&#x1f340;随机梯度下降的调试 &#x1f340;引言 随机梯度下降是一种优化方法&#xff0c;主要作用是提高迭代速度&#xff0c;避免陷入庞大计算量的泥沼。在每次更新时&a…

[uniapp]踩坑日记 unexpected character > 1或‘=’>1 报错

在红色报错文档里下滑&#xff0c;找到Show more 根据提示看是缺少标签&#xff0c;如果不是缺少标签&#xff0c;看看view标签内容是否含有<、>、>、<号,把以上符合都进行以<号为例做{{“<”}}处理

Ubuntu编译运行socket.io

本篇文章记录一下自己在ubuntu上编译运行socket.io的过程&#xff0c;客户端选用的是socket.io的c的库&#xff0c;编译起来倒不难&#xff0c;但是说到运行的话&#xff0c;对我来说确实是花了点功夫。毕竟程序要能运行起来才能更方便地去熟悉代码&#xff0c;因此今天我就记录…