【C++】继承

【C++】继承

1.继承的概念

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

2.继承定义

Person是父类,也称作基类。Student是子类,也称作派生类。

继承关系和访问限定符:

继承基类成员访问方式

总结:

保护和私有作为访问限定符作用一样,继承中不同
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
protected在派生类中可以访问,不能在类外面被访问。可以看出保护成员限定符是因继承才出现的。
#include<iostream>usingnamespace std;classPerson{//public:protected:voidPrint(){ cout <<"name:"<< _name << endl; cout <<"age:"<< _age << endl;}protected: string _name ="peter";// 姓名int _age =18;// 年龄};classStudent:protectedPerson{public:Student():_stuid(520304){}protected:int _stuid;// 学号};classTeacher:protectedPerson{public:Teacher():_jobid(521024){}protected:int _jobid;// 工号};
总结表格发现基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,最好显示的写出继承方式。
5.成员变量会继承下去,因为每个对象的成员变量都是独一无二的。成员函数就没必要继承下去·,定义在公共部分,使用时调用即可。
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
7.派生类在继承基类时,可以不指定继承方式,默认为private
  • 测试代码
#include<iostream>usingnamespace std;classPerson{public:voidPrint(){ cout <<"name:"<< _name << endl; cout <<"age:"<< _age << endl;}protected: string _name ="peter";// 姓名int _age =18;// 年龄};classStudent:publicPerson{public:Student():_stuid(520304){}protected:int _stuid;// 学号};classTeacher:publicPerson{public:Teacher():_jobid(521024){}protected:int _jobid;// 工号};intmain(){ Student s; Teacher t; s.Print(); t.Print();return0;}

3.基类和派生类对象赋值转换

1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。有个形象说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

2.基类对象不能赋值给派生类对象:因为基类和派生类的内存布局不同,基类对象缺少派生类中新增的成员变量和方法。这种赋值会导致信息丢失或未定义行为。
3.与变量赋值存在隐式类型转换和显示类型转换不同,派生类对象可以赋值给基类的引用或指针,这种赋值没有中间量,因为派生类对象本身就是基类对象的扩展。这种赋值是隐式的,编译器会自动处理类型转换。
4(了解).基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,后续多态会深入学习。

4.继承中的作用域

在继承体系中基类和派生类都有独立的作用域

子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问

在这里插入图片描述


这里_num是同名成员,构成隐藏。

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。成员变量也可以隐藏

在这里插入图片描述


B中的fun和A中的fun不是构成重载,因为不是在同一作用域

注意在实际中在继承体系里面最好不要定义同名的成员
5.域的本质是在编译器工作时指导编译器去“查找”的规则,编译阶段检查语法。后续代码段的“查找是”链接阶段,需要区分“查找”
6.使用同名变量时遵循就近原则子类域–父类域–全局域
7.静态成员函数可以被继承:因为它们是类接口的一部分,独立于对象存在。静态成员函数不依赖于具体的对象实例,它们可以独立于对象存在。这使得它们可以被子类继承,而不需要与对象的状态绑定。这意味着静态成员函数可以在没有创建类的对象的情况下被调用。
8.基类对象不包含静态变量:因为静态变量是类级别的,独立于对象存在。静态变量在类加载时初始化,并且在类的生命周期内存在,而不是在对象创建时初始化。静态变量是类的成员,而不是对象的成员。分配内存,并且只有一份,它们在类的所有实例之间共享。

5.派生类的默认成员

“默认”的意思就是指我们不写,编译器会帮我们自动生成一个。在派生类中这几个成员函数是生成方式如下:

1.初始化把父类看成一个整体,子类初始化自己的,父类的由其在初始化列表自动调用父类默认构造完成,若父类没有默认构造时会通过初始化列表调用基类 的构造函数,就像匿名对象一样进行初始化,否则父类无默认构造函数将报错。
初始化按声明顺序,继承相当于声明在派生类之前,先初始化


该段代码父类没有默认构造函数,通过初始化列表显示调用基类构造函数Person(name)。
初始化列表的作用:
1.子类的构造函数可以通过初始化列表显式调用父类的构造函数。如果子类构造函数的初始化列表中没有显式调用父类的构造函数,编译器会尝试调用父类的默认构造函数。
2.如果父类没有默认构造函数,而子类构造函数的初始化列表中也没有显式调用父类的其他构造函数,编译器将报错,因为无法找到合适的父类构造函数。

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

在这里插入图片描述
派生类的operator=必须要调用基类的operator=完成基类的复制。赋值注意父类和子类的调用顺序,只在公有中使用,保护和私有中权限会发生变化
规定父先构造,子先析构,为什么?
因为子类有可能析构后还用到父类,父类不可能用到子类。

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

在这里插入图片描述
  • 整体代码
classPerson{public://构造/*Person(const char*name="mint") :_name(name) { cout << "Person()" << endl; }*/Person(constchar* name):_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=()"<< endl;//检查自赋值if(this!=&p){ _name = p._name;}return*this;}//析构~Person(){ cout <<"~Person()"<< endl;delete _pstr;}protected: string _name; string* _pstr =newstring("10240304");};classStudent:publicPerson{public://派生类构造函数,初始化顺序先父后子Student(constchar* name ="小鹅",int id =0):Person(name),_id(id){ cout <<"Student()"<< endl;}Student(const Student& s):Person(s),_id(s._id){ cout <<" Student(const Student& s)"<< endl;} Student&operator=(const Student&s){if(this!=&s){ Person::operator=(s); _id = s._id;}return*this;}~Student(){// 由于多态的原因(具体后面讲),析构函数的函数名被// 特殊处理了,统一处理成destructor// 显示调用父类析构,无法保证先子后父// 所以子类析构函数完成就,自定调用父类析构,这样就保证了先子后父P//Person::~Person(); cout <<*_pstr << endl;delete _ptr;}protected:int _id;int* _ptr =newint;};intmain(){//Person p; Student s1; Student s2(s1); Student s3("喵喵",24); s1 = s3;return0;}

调用顺序如下

6.继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员


若派生类中不加友元声明将报错

了解前向声明的概念:

作用:
1.声明但不定义:告诉编译器某个类或函数的存在,但不需要其完整的实现细节。
2.减少头文件包含:避免通过 #include 引入其他头文件,从而减少编译时的依赖关系。
核心目的:通过减少不必要的头文件包含,降低模块间的依赖关系。
适用场景:仅需类型声明(如指针、引用、函数参数)时优先使用前向声明。

注意事项:在需要完整类型信息时必须包含头文件。

7.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例,所有父类对象共享同一个static成员实例
静态成员属于父类和派生类,在派生类中不会单独拷贝一份,可以理解为继承的是使用权

可以利用这样的特性来统计人的个数,去计算总共创建了多少个对象


Person::_count 被设置为0,因为 Student 类继承自 Person 类,并且 _count 是 protected 访问权限,这意味着 Student 类可以访问和修改 _count。
所以最后输出结果为0

8.菱形继承与虚拟继承(重点)

继承方式

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承问题

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

通过显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。

在这里插入图片描述
二义性可理解为当一个人在生活中有两个不同身份时,但其基本特征不变,例如年龄,身份证号码等。二义性问题出在当分饰两个不同角色时,基本特征有两份,但实际上是一样的

虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。虚拟继承在“腰部”的位置去使用,即在继承方式前加一个virtual修饰。如上图在Student和Teacher的继承Person时使用虚拟继承

classPerson{public: string _name;// 姓名int _age;};classStudent:virtualpublicPerson{protected:int _num;//学号};classTeacher:virtualpublicPerson{protected:int _id;// 职工编号};classAssistant:publicStudent,publicTeacher{protected: string _majorCourse;// 主修课程};

使用后派生类共享_age同一块空间,都可以进行修改。
没使用虚拟继承问题

虚拟继承节省空间问题


非虚拟对象中B和C包含一个父类成员指针大小(副本),加上自身,各自都为8字节,D对象自身为4字节,总共为20字节。

在虚继承中,B 和 C 都虚继承自 A,确保 A 只被实例化一次。所以内存大小为每个对象的指针大小+虚基表指针大小。
每个虚继承的派生类都包含一个虚基表指针。这些指针指向各自的虚基表。不排除编译器的优化,优化模式下会合并虚基表以减少内存开销,不过我们并不考虑
这里B和C都包含一个虚基表指针,32位系统:通常占用4字节;64位系统:通常占用8字节。这里32位系统下,所以占用空间大小4*4+4+4


当存储成员所占空间较大时,虚基表节省空间的作用就愈发明显,因为指针大小固定不变,替换掉了冗余空间。

虚拟继承解决数据冗余和二义性的原理

为了研究虚拟继承原理,给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

  • 菱形继承


内存地址严格按照多继承顺序存储,可以看出A和B中都存有A值的地址。
  • 虚拟继承


内存第地址左高右低,这里是小端存储,输入时从左往右输入,先输入内存中右边数据(小端)
对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
虚继承把冗余数据单独拿出来,具体放哪由编译器决定,但多了一点东西就是虚基表指针

为什么要内存中要存虚基表指针,而不直接存父类成员的地址?

1.例如在基类和派生类对象赋值转换中,会发生切片,就不能像普通场景一样直接在内存地址中找到父类成员变量,而是需要通过偏移量去寻找。
2.如果一个父类有很多成员,那么其派生类将其地址一个个存储显得冗余,通过虚基表指针指向其地址即可

虚基表指针

作用:

虚基表指针用于动态解析基类成员的地址,特别是在存在虚继承的情况下。这是因为虚基类的地址在编译时无法确定,需要在运行时通过虚基表指针来查找。

为什么需要?

动态解析:在虚继承中,基类的地址在编译时无法确定,因为基类可能被多个派生类共享。虚基表指针允许在运行时动态确定基类的地址。
避免重复实例化:虚继承确保基类只被实例化一次,无论有多少派生类继承自它。虚基表指针帮助管理这种共享关系。
支持多态:虚基表指针支持多态行为,允许通过基类指针调用派生类的成员函数。

9.继承和组合

多继承可以认为是C++的缺陷之一,很多后来的OO(面向对象)语言都没有多继承,如Java。


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

Read more

宇树科技Go2机器人强化学习(RL)开发实操指南

宇树科技Go2机器人强化学习(RL)开发实操指南

在Go2机器人的RL开发中,环境配置、模型训练、效果验证与策略部署的实操步骤是核心环节。本文基于宇树科技官方文档及开源资源,以Isaac Gym和Isaac Lab两大主流仿真平台为核心,提供从环境搭建到实物部署的全流程操作步骤,覆盖关键命令与参数配置,帮助开发者快速落地RL开发。 一、基础准备:硬件与系统要求 在开始操作前,需确保硬件与系统满足RL开发的基础需求,避免后续因配置不足导致训练中断或性能瓶颈。 类别具体要求说明显卡NVIDIA RTX系列(显存≥8GB)需支持CUDA加速,Isaac Gym/Isaac Lab均依赖GPU进行仿真与训练操作系统Ubuntu 18.04/20.04/22.04推荐20.04版本,兼容性最佳,避免使用Windows系统(部分依赖不支持)显卡驱动525版本及以上需与CUDA版本匹配(如CUDA 11.3对应驱动≥465.19.01,CUDA 11.8对应驱动≥520.61.05)软件依赖Conda(

By Ne0inhk

一、FPGA到底是什么???(一篇文章让你明明白白)

一句话概括 FPGA(现场可编程门阵列) 是一块可以通过编程来“变成”特定功能数字电路的芯片。它不像CPU或GPU那样有固定的硬件结构,而是可以根据你的需求,被配置成处理器、通信接口、控制器,甚至是整个片上系统。 一个生动的比喻:乐高积木 vs. 成品玩具 * CPU(中央处理器):就像一个工厂里生产好的玩具机器人。它的功能是固定的,你只能通过软件(比如按不同的按钮)来指挥它做预设好的动作(走路、跳舞),但你无法改变它的机械结构。 * ASIC(专用集成电路):就像一个为某个特定任务(比如只会翻跟头)而专门设计和铸造的金属模型。性能极好,成本低(量产时),但一旦制造出来,功能就永远无法改变。 * FPGA:就像一盒万能乐高积木。它提供了大量基本的逻辑单元(逻辑门、触发器)、连线和接口模块。你可以通过“编程”(相当于按照图纸搭建乐高)将这些基本模块连接起来,构建出你想要的任何数字系统——可以今天搭成一个CPU,明天拆了重新搭成一个音乐播放器。 “现场可编程”

By Ne0inhk
基于FPGA的CLAHE自适应限制对比度直方图均衡算法硬件verilog实现

基于FPGA的CLAHE自适应限制对比度直方图均衡算法硬件verilog实现

基于FPGA的CLAHE自适应限制对比度直方图均衡算法硬件verilog实现 摘要:本文详细阐述了基于 FPGA 的 CLAHE(自适应限制对比度直方图均衡)算法的硬件verilog实现方案。CLAHE是一种强大的图像增强算法,广泛应用于医学影像、红外成像、低照度增强等领域。本文将从算法原理出发,深入讲解各模块的RTL架构设计,包括坐标计数器、直方图统计、CDF计算、双线性插值映射以及乒乓RAM管理等核心模块的实现细节。 项目开源地址:https://github.com/Passionate0424/CLAHE_verilog 开源不易,辛苦各位看官点点star!! 一、CLAHE算法基本原理 1.1 算法背景 CLAHE(Contrast Limited Adaptive Histogram Equalization,对比度受限的自适应直方图均衡)是对传统自适应直方图均衡(AHE)的改进。AHE通过将图像划分为多个子区域(称为 “Tiles”),对每个Tile独立进行直方图均衡化,从而适应图像的局部特性。然而,AHE在噪声较大的平坦区域(如天空、

By Ne0inhk

Telegram搜索机器人推荐——查找海量资源,提升信息检索效率

大家好,本文首发于 ZEEKLOG 博客,主要面向需要在 Telegram 中高效检索资源的同学。我结合自己的实测体验,总结了几款实用的搜索机器人与完整操作流程,帮助大家解决“怎么快速找到频道、群组、文件”的痛点。如果你也在为信息筛选耗时头疼,建议耐心读完并亲手试试,收获会很大。觉得有帮助别忘了给个点赞、收藏和关注支持一下 🙂 📚 本文目录 * 使用准备 * 什么是Telegram搜索机器人? * Telegram搜索机器人的核心功能 * 推荐的Telegram搜索机器人 * 如何使用Telegram搜索机器人? * Telegram搜索机器人的应用场景 * 总结 在信息爆炸的时代,如何高效获取自己想要的资源?Telegram搜索机器人为你带来全新解决方案,无需翻找频道、群组,只需输入关键词,即可一键查找海量内容。无论是影视剧、电子书、图片还是优质群组,Telegram搜索机器人都能帮你轻松找到。推荐搜索机器人:@soso、@smss、@jisou 使用准备 1. 能访问外网,不会魔法的同学请参考:这里 2. 安装 Telegram

By Ne0inhk