C++---继承
继承
- 前言
- 继承的概念及定义
- 继承的概念
- 继承定义
- 继承关系和访问限定符
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元
- 继承与静态成员
- **多重继承**
- 多继承下的类作用域
- 菱形继承
- 虚继承
- 使用虚基类
- 支持向基类的常规类型转换
前言
在需要写Father类和Mother类的时候,需要给这两个类写一些属性,像 名字,性别,年龄,爱好,电话,家庭地址等
,这两个类中会有一些共同的属性,把这些公共的属性进行提取,封装成一个Person类,Father和Mother继承Person,就不需要在写共同的属性了。这就是本章要说的继承。
继承的概念及定义
继承的概念
继承机制是面向对象程序设计是代码可以复用的最重要的手段,它允许程序猿在保持原有类特性的基础上进行扩展,增加功能,这样产生的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构。层次结构的根部有一个基类,派生类就是直接或间接的从基类继承而来。基类负责定义在层次结构中所有类的共同拥有的成员,而每个派生类定义各自特有的成员。
class Person
{
public:void Print(){cout << "name>>" << _name << endl;cout << "age>>" << _age << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
protected:int _stuid = 123;
};int main()
{Student s;s.Print();return 0;
}
通过调试可以看出,派生类中有基类的成员,也有自己定义的特殊的成员。
继承定义
派生类 继承方式 基类
class Student : public Person
继承关系和访问限定符
继承基类的时候,访问限定符的不同,成员访问也会变化。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类的不可见 | 在派生类中不可见 |
-
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
-
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。。
-
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
-
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强 。
基类和派生类对象赋值转换
通常情况下,如果我们想把引用活指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。存在继承关系的类是也给重要的列外:我们可以将基类的指针或引用绑定到派生类对象上。 见上面代码将Student对象的地址赋值给Person*
。
之所以存在派生类向基类的类型转换是因为每个派生类对象都包含了一个基类的部分,而基类的引用或者指针可以绑定到该基类的部分上。
Student s;
Person* p1 = &s;
Person& p2 = s;
p1的过程中派生类会将父类的那一部分切出来拷贝过去,p2的过程则是子类中父类的部分的别名。只有派生类对象中的基类部分会被拷贝,移动或者赋值,它的派生类部分将被忽略掉。
s.Print();p1->_name = "B";p1->Print();p2._name = "C";p2.Print();
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或者引用的静态类型来推断该转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用 dynamic_cast
请求一个类型转换,该转换的安全检查将在运行时执行。
- 从派生类向基类的类型转换只对指针或者引用类型有效
- 基类向派生类不存在隐式类型转换
- 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行
自动类型转换只对指针或者引用有效,但是继承体系中的大多数类仍然(显示或隐式的)定义了拷贝控制成员。因此,我们通常能够将一个派生类对象拷贝,移动,或者赋值给一个基类对象。不过这种操作只处理派生类中的基类部分。
继承中的作用域
每个类定义自己的作用域,这个作用域内我们定义的成员,当存在继承关系时,派生类的作用域嵌套,在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析时,编译器将继续在外层的基类作用域中寻找改名字的定义。
和其他作用域一样,派生类也能重用定义在其直接基类或简介基类中的名字,此时定义在内层作用域(既派生类)的名字将隐藏定义在外层作用域(既基类)的名字。
class Person
{
public:void Print(){cout << "name>>" << _name << endl;cout << "age>>" << _age << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
public:void Print(){cout << "age>>" << _age << endl;cout << "Person.age>>" << Person::_age << endl;}protected:int _stuid = 123;int _age = 20;
};int main()
{Student s;s.Print();return 0;
}
输出结果是 20 18
派生类的成员将隐藏同名的基类成员。
如果想要访问基类中的成员,可以使用作用域运算符来使用隐藏的成员
class Person
{
public:void Print(){cout << "Person::Print()" << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
public:void Print(int i){cout << "Student::Print()" << endl;}
protected:int _stuid = 123;int _age = 20;
};
代码中的两个Print是什么关系呢?
隐藏
为什么不是重载呢?同名函数,参数不同,构成重载很合理。
但是重载有一个限定,同一个作用域。
派生类的默认成员函数
class Person
{
public:Person(const char* name = "A"):_name(name){}protected:string _name;int _age = 18;
};class Student : public Person
{
public:Student(const char* name = "B"):_id(0),_name(name){}
protected:int _id;
};
在父类中写了构造函数后,想在子类中给继承而来的name成员进行初始化,但是Student中的构造函数给name成员初始化的操作却报错了。
因为派生类的构造只能构造在派生类中新增的成员,要想调用基类的构造函数可以在派生类的初始化列表中加上 Person(name)
Student(const char* name = "B"):_id(0),Person(name)
如果不写 Person(name)
的话,派生类在执行构造函数的时候,会自动的去调用基类的构造函数(要有默认参数,不然会报错).
Student(const Student& s):Person(s) // 将基类 = 派生类,会有切片操作,将属于基类的部分进行切割。,_id(s._id){}
拷贝构造中也要调用父类Person才能完成。
如果在拷贝构造中不写Person(s),就会去调用默认拷贝构造函数。
Student& operator= (const Student& t){cout << "Student& operator= (const Student& t)" << endl;if (this != &t){Person::operator=(t);// 不写Person:: 会造成隐藏_id = t._id;}return *this;}
Person& operator= (const Person& n){cout << "Person& operator= (const Person& n)" << endl;if (this != &n){_name = n._name;}return *this;}
在基类和派生类中分别写一个赋值运算符,派生类也需要调用父类的赋值运算符。
在析构函数体执行完后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源。
class Person
{
public:Person(const char* name = "A"):_name(name){cout << "Person" << endl;}Person& operator= (const Person& n){cout << "Person& operator= (const Person& n)" << endl;if (this != &n){_name = n._name;}return *this;}~Person(){cout << "~Person" << endl;}protected:string _name;
};class Student : public Person
{
public:Student(const char* name = "B"):_id(0),Person(name){cout << "Student(const char* name = 'B')" << endl;}Student& operator= (const Student& t){cout << "Student& operator= (const Student& t)" << endl;if (this != &t){Person::operator=(t);_id = t._id;}return *this;}~Student(){Person::~Person();cout << "~Student" << endl;}Student(const Student& s):Person(s),_id(s._id){}protected:int _id;
};int main()
{Student s("张三");return 0;
}
后面由于多态的原因,析构函数的函数名被特殊处理了,统一处理成destructor。
在执行代码后
先给基类初始化,然后派生类初始化,在调用析构发现,基类的析构多调用了一次。
对象销毁的顺序正好与创建的顺序相反,创建对象的时候是先调用父类的构造函数,这里子类析构函数首先执行,然后是父类的析构函数,以此类推,沿着继承体系的反方向直至最后。
继承与友元
就像友元关系不能传递一样,有缘关系同样也不能继承,基类的友元在访问派生类成员时不具有特殊属性,类似的,派生类的友元也不能随意访问基类的成员。
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name = "S"
};class Student : public Person
{
protected:int _id = 1;
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._id << endl;
}int main()
{Person p;Student s;Display(p, s);return 0;
}
不能继承友元关系,每个类负责控制各自成员的访问权限。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系只存在该成员的唯一定义。不管有多少个派生类,静态成员只有一个实例。
class Student;
class Person
{
public:string _name = "S";static int num;
};int Person::num = 0;class Student : public Person
{
protected:int _id = 1;
};int main()
{Person p;Student s;cout << &s.num << endl;cout << &p.num << endl;return 0;
}
输出结果会发现,这两个地址是一样的。
静态成员遵循通用的访问控制规则。
多重继承
一个子类只有一个直接父类时称这个继承为单继承。
多重继承是指一个子类有两个或以上直接父类,多继承的派生类继承了所有父类的属性。从概念上看不是很难,但是多个基类相互交织就会产生一些特殊的问题。
构造一个派生类的对象将同时构造并初始化它的所有基类子对象。与从一个基类进行的派生一样,多继承的派生类的构造函数初始值也只能初始化它的直接基类。
多继承下的类作用域
在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。查找过程沿着继承体系自底向上进行,直到找到所需要的名字,派生类的名字将隐藏基类的同名成员。
在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到了,则对该名字的使用将具有二义性。
菱形继承
一个学生,它继承了A,也继承了B,A和B都继承了Person,前面也说过,派生类会继承基类的所有属性,如果A中存在age,B中也存在age,在Student中访问age就会导致二义性(不知道访问哪个),无法明确知道访问的是哪一个。
class Person
{
public:string _name;
};class A : public Person
{
public:string _id;
};class B : public Person
{
public:string _num;
};class Student : public A, public B
{
public:string _sex;
};int main()
{Student s;s._name = "张三";return 0;
}
当然,可以通过指定访问哪个父类的成员可以解决二义性的问题,但是数据冗余(浪费空间)的问题无法解决。
Student s;s.B::_name = "张三";s.A::_name = "李四";cout << s.B::_name << endl;cout << s.A::_name << endl;
当一个类拥有多个基类,有可能出现派生类从两个或更多基类中继承同名成员的情况,此时,不加前缀限定符直接使用该名字会引发二义性。
虚继承
在默认情况下,派生类中含有继承链上的每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。也就是上面的菱形继承造成的二义性和数据冗余问题。
C++给出了虚继承的机制来解决这两个问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
class Person
{
public:string _name;
};class A : virtual public Person
{
public:string _id;
};class B :virtual public Person
{
public:string _num;
};class Student : public A, public B
{
public:string _sex;
};int main()
{Student s;s._name = "张三";cout << s._name << endl;return 0;
}
#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 d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
在调试的时候,查看内存,可以发现,这些数据都是挨着放的。
这是通过前缀限定符解决二义性问题的,可以看出,数据冗余问题还存在。
下面看通过虚继承的方式。
在代码最后添上了 d._a = 0;
这样,就没有数据冗余和二义性了。但是BC类中多了两个东西。
把这两个地址查看一下发现,指向的都是0,但是这个地址的下一个位置存的都有数据。其实通过计算可以发现,1到6的偏移量刚好是20,3到6的偏移量刚好是12.所以BC中多出来的地址存的是距离A的偏移量(相对距离)。
存找基类偏移量的表叫虚机表
为什么要把偏移量给存下来呢?上面说的切片操作,基类以引用或者指针的方式,把派生类=基类。
使用虚基类
我们指定虚基类的方式是添加关键字 virtual
class B :virtual public Person
通过上面的代码,我们将Person 定义为 B的虚基类。
vitrual说明符表示一种愿望,即在后续的派生类当中共享虚基类的同一份实例。
支持向基类的常规类型转换
不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。
相关文章:

C++---继承
继承 前言继承的概念及定义继承的概念继承定义继承关系和访问限定符 基类和派生类对象赋值转换继承中的作用域派生类的默认成员函数继承与友元继承与静态成员**多重继承**多继承下的类作用域菱形继承虚继承使用虚基类 支持向基类的常规类型转换 前言 在需要写Father类和Mother…...

使用新版Maven-mvnd快速构建项目
目前我们项目的构建方式多数是 maven、gradle,但是 maven 相对 gradle 来说,构建速度较慢,特别是模块相对较多的时候,构建速度更加明显。但是我们将项目由 maven 替换为 gradle 相对来说会比较麻烦,成本较高。于是我们…...

【ICASSP 2023】ST-MVDNET++论文阅读分析与总结
主要是数据增强的提点方式。并不能带来idea启发,但对模型性能有帮助 Challenge: 少有作品应用一些全局数据增强,利用ST-MVDNet自训练的师生框架,集成了更常见的数据增强,如全局旋转、平移、缩放和翻转。 Contributi…...

MySQL 面试题——MySQL 基础
目录 1.什么是 MySQL?有什么优点?2.MySQL 中的 DDL 与 DML 是分别指什么?3.✨数据类型 varchar 与 char 有什么区别?4.数据类型 BLOB 与 TEXT 有什么区别?5.DATETIME 和 TIMESTAMP 的异同?6.✨MySQL 中 IN …...

JDK9特性——概述
文章目录 引言JDK9特性概述JDK9的改变JDK和JRE目录变化总结 引言 JAVA8 及之前,版本都是特性驱动的版本更新,有重大的特性产生,然后进行更新。 JAVA9开始,JDK开始以时间为驱动进行更新,以半年为周期,到时…...

征战开发板从无到有(三)
接上一篇,翘首已盼的PCB板子做好了,管脚约束信息都在PCB板上体现出来了,很满意,会不会成为爆款呢,嘿嘿,来,先看看PCB裸板美图 由于征战开发板电路功能兼容小梅哥ACX720,大家可以直…...

Linux设备树详细学习笔记
参考文献 参考视频 开发板及程序 原子mini 设备树官方文档 设备树的基本概念 DT:Device Tree //设备树 FDT: Flattened Device Tree //开放设备树,起源于OpenFirmware (所以后续会见到很多OF开头函数) dts: device tree source的缩写 //设备树源码 dtsi: device …...

【系统架构】系统架构设计基础知识
导读:本文整理关于系统架构设计基础知识来构建系统架构知识体系。完整和扎实的系统架构知识体系是作为架构设计的理论支撑,基于大量项目实践经验基础上,不断加深理论体系的理解,从而能够创造新解决系统相关问题。 目录 1、软件架…...

快递、外卖、网购自动定位及模糊检索收/发件地址功能实现
概述 目前快递、外卖、团购、网购等行业 :为了简化用户在收发件地址填写时的体验感,使用辅助定位及模糊地址检索来丰富用户的体验 本次demo分享给大家;让大家理解辅助定位及模糊地址检索的功能实现过程,以及开发出自己理想的作品…...
Springboot后端导入导出excel表
一、依赖添加 操作手册:Hutool — 🍬A set of tools that keep Java sweet. <!--hutool工具包--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.20</versio…...

通过stream流实现分页、模糊搜索、按列过滤功能
通过stream实现分页、模糊搜索、按列过滤功能 背景逻辑展示示例代码 背景 在有一些数据通过数据库查询出来后,需要经过一定的逻辑处理才进行前端展示,这时候需要在程序中进行相应的分页、模糊搜索、按列过滤了。这些功能通过普通的逻辑处理可能较为繁琐…...
webpack:系统的了解webpack一些核心概念
文章目录 webpack 如何处理应用程序?何为webpack模块chunk?入口(entry)输出(output)loader开发loader 插件(plugin)简介流程插件开发:Tapable类监听(watching)compiler 钩子compilation 钩子compiler和compilation创建自定义 插件 loader和pl…...

Unreal Engine Loop 流程
引擎LOOP 虚幻引擎的启动是怎么一个过程。 之前在分析热更新和加载流程过程中,做了一个图。记录一下!! 
linux万字图文学习进程信号
1. 信号概念 信号是进程之间事件异步通知的一种方式,属于软中断。 1.1 linux中我们常用Ctrlc来杀死一个前台进程 1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。2. S…...

DataX实现Mysql与ElasticSearch(ES)数据同步
文章目录 一、Linux环境要求二、准备工作2.1 Linux安装jdk2.2 linux安装python2.3 下载DataX: 三、DataX压缩包导入,解压缩四、编写同步Job五、执行Job六、定时更新6.1 创建定时任务6.2 提交定时任务6.3 查看定时任务 七、增量更新思路 一、Linux环境要求…...

第二章 进程与线程 十、调度算法1(先来先服务、短作业优先、最高响应比优先)
目录 一、先来先服务算法 1、算法思想 2、算法规则 3、用于作业/进程调度 4、是否可抢占? 5、优缺点 优点: 缺点: 6、是否会导致饥饿 7、例子 二、短作业优先算法 1、算法思想 2、算法规则 3、用于作业/进程调度 4、是否可抢占? 5、优缺…...

windows平台 git bash使用
打开所在需要git管理的目录,鼠标右键open Git BASH here 这样就直接进来,不需要windows dos窗口下麻烦的切路径,windows和linux 路径方向不一致 (\ /) 然后git init 建立本地仓库,接下来就是git相关的操作了. 图形化界面查看 打开所在需要git管理的目录,鼠标右键…...

Linux系统之安装uptime-kuma服务器监控面板
Linux系统之安装uptime-kuma服务器监控面板 一、uptime-kuma介绍1.1 uptime-kuma简介1.2 uptime-kuma特点 二、本次实践环境介绍2.1 环境规划2.2 本次实践介绍2.3 环境要求 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查系统是否安装Node.js 四、部署…...

计算机组成原理——基础入门总结(一)
本帖更新一些关于计算机组成原理的重点内容。由于博主考研时并不会考这门课,但是考虑到操作系统中又很多重要晦涩的概念涉及很多诸如内存、存储器、磁盘、cpu乃至各种寄存器的知识,此处挑选一些核心的内容总结复盘一遍——实现声明:本帖的内容…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...

定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...

【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准
城市路内停车管理常因行道树遮挡、高位设备盲区等问题,导致车牌识别率低、逃费率高,传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法,正成为破局关键。该设备安装于车位侧方0.5-0.7米高度,直接规避树枝遮…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
Python 高效图像帧提取与视频编码:实战指南
Python 高效图像帧提取与视频编码:实战指南 在音视频处理领域,图像帧提取与视频编码是基础但极具挑战性的任务。Python 结合强大的第三方库(如 OpenCV、FFmpeg、PyAV),可以高效处理视频流,实现快速帧提取、压缩编码等关键功能。本文将深入介绍如何优化这些流程,提高处理…...