【C++技能树】继承概念与解析
Halo,这里是Ppeua。平时主要更新C++,数据结构算法,Linux与ROS…感兴趣就关注我bua!
继承
- 0. 继承概念
- 0.1 继承访问限定符
- 1. 基类和派生类对象赋值兼容转换
- 2. 继承中的作用域
- 3. 派生类中的默认成员函数
- 4.友元
- 5.继承中的静态成员
- 6.菱形继承
- 7.菱形虚拟继承
- 总结
- 总结

0. 继承概念
设想一个场景,你需要设计学生、老师、教授…的类,除了每个身份中独有的信息,例如:学号,工号,教授身份号,但是他们都有一个共同的属性,就是人.所以我们可以先设计一个类:人.
每设计一个新的类都可以复用人这个类,增加了代码的复用性.这就是C++中的新特性:继承
我们之前接触的函数重载是函数层面的复用,继承则是类层面的复用
class Person{
public:void print(){cout<<age<<" "<<name<<endl;}int age=0;string name="Peter";string address;int tel;
};
class Student:public Person
{
public:int _stuid;
};
我们可以通过调用来看看其结构模型.
int main()
{Person p1;cout<<p1.name<<endl;Student s1;cout << s1._stuid;
}
子类中可以共享父类中的变量,父类不可以访问子类的变量.
0.1 继承访问限定符
在类中有访问限定符,同样的,在继承方式上也有访问限定符.
派生类(子类)可以通过以下方式来继承基类(父类):
在这里新出现了一个权限符号,protected.它与private是类似的:
protected修饰的变量在类外与private类似,不能被访问.
但是在派生类中可以访问protected修饰的变量,而不能访问private修饰的变量
所以权限的大小的关系为:
public>protected>private
所以,在权限的继承中有一个最小原则.
- 以public来继承,可以继承的变量为:public,protected
- 以protected来继承.可以继承的变量为:protected
- 以private来继承,无可以继承的变量
通常情况下,我们一般用public来继承,protected/private的继承方式实用性不高
1. 基类和派生类对象赋值兼容转换
派生类可以转换为基类,而基类并不能转换为派生类.
例如上面Person与Student的例子:
可以实现
Person p1;
Student s1;
p1=s1;
不能实现
s1=p1;
这其实很好理解.父类中的属性往往比子类中的成员多,子类中的成员可以通过切割多余的成员转换到父类中.
通过这样的方式,这中间不涉及强制类型转换.我们可以通过以下这个例子来看.
我们知道,强制类型转换会产生一个临时变量.例如:
int a=10;
double b=a;
这当中会产生一个临时变量double a,来赋值给b.
Person &p1=s1;
如果产生了临时变量,这个赋值是不可以的.但通过编译器验证,我们发现这样是可以的.侧面的说明这并不是引用.
但对于p1的成员进行修改,s1也会同样被修改.
Student s1;
Person p1 = s1;
p1.name = "H";
Before:
After:
虽然这种限制(子类可以转父类,父类不可转子类)可以通过指针直接访问内存的方法解除
原来指向的是student对象,现在强制转换为student指针是可以的.
Student s1;
Person *p1 = &s1;
Student* sp1 = (Student*)p1;
sp1->age = 10;
原来指向的是person对象,现在强制转换为student指针则会发生越界
Student s1;
Person pp1;
Person *pp1 = &s1;
Student* sp1 = (Student*)pp1;
sp1->age = 10;
2. 继承中的作用域
一个{}是一个作用域,所以在基类和派生类中,都有自己的作用域.
所以当在派生类中定义与基类相同的名的变量的时候就会构成隐藏:隐藏父类的相关变量
当在派生类中定义与基类相同的函数时(只需要名字相同),就会构成重定义(隐藏):重定义父类相关函数
如果需要访问父类被隐藏的属性,需要在前加类域限定符才能访问
#include<iostream>
using namespace std;
class Person {
public:void print(){cout <<"Person:" << age << endl;}int age = 0;
};
class Student :public Person
{
public:void print(int i){cout <<"Student:" << age << endl;}int age=10;};
int main()
{Student s1;cout << s1.age; //10s1.print(1);// student:10cout << s1.Person::age;//0s1.Person::print();//person:0return 0;
}
3. 派生类中的默认成员函数
总的来说,派生类中的所有涉及父类的行为都要从父类当中去寻找相关方法论:
-
派生类初始化的时候会先调用父类的初始化函数,在调用自己的.若父类没有默认构造函数则需要在派生类中的初始化列表中调用父类构造函数传入参数.(**为什么需要在初始化列表中调用不在函数体里调用呢?**自定义类型成员(且该类没有默认构造函数时)在初始化列表中调用相关文章:初始化列表
class Person{ public:Person(int sage):age(sage){}void print(){cout<<age<<" "<<name<<endl;}int age=0;string name="Peter";string address;int tel; private:int s=0; }; class Student:public Person { public:Student(int _age):Person(_age){}int _stuid; };
-
拷贝构造与赋值运算符重载需要通过显式调用父类中的方法来完成.
class person { public:person(const char *name="peter"):_name(name){cout<<"person()";}person(const person&p1):_name(p1._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;}string _name;}; class student:public person { public:student(const char*name="zhangsan",int id=0):person(name),_id(id){cout<<"student()"<<endl;}student(const student&s1):person(s1),_id(s1._id){}student& operator=(const student& s1){if(this!=&s1){//出现隐藏,想要调用父类的=person::operator=(s1);_id=s1._id;}return *this;}void print(){cout<<_id<<" "<<_name<<endl;} private:int _id; };
-
析构函数不需要显式调用父类(也不能),编译器会自己调用完派生类的析构函数,在调用基类的析构函数
其实这也很好理解.从函数栈帧方面:先创建父类再创建子类,自然先析构子类再析构父类.
从内存保护方面:在子类中有可能调用了父类的成员对象,如果先消除父类,会导致子类中出现野指针的情况
4.友元
父类的友元不能访问子类的成员变量。(父亲的朋友不是孩子的朋友)
class B;
class A{
friend void print(const A& a1,const B& b1);
private:int a=10;};
class B:public A{
private:int b=100;
};
void print(const A& a1,const B&b1)
{cout<<a1.a<<endl; //rightcout<<b1.a<<endl; //rightcout<<b1.b<<endl; //error
}
在上面的例子中可以看到:print函数可以访问A的private,而不能访问B中的private
5.继承中的静态成员
静态成员只会存在一份.在父类当中,子类中可以继承静态成员.但是继承的是访问权,只能访问不能修改
且其是存在类当中,也就是无论几个对象,访问的都是同一个静态成员
class B;
class A{
friend void print(const A& a1,const B& b1);
public:static int count;
private:int a=10;
};
int A::count=10;
class B:public A{
public:void print(){cout<<count<<" "<<endl;}private:int b=100;};void print(const A& a1,const B&b1)
{cout<<a1.count<<endl;cout<<b1.count<<endl;cout<<A::count;
}int main()
{A a1;B b1;print(a1, b1);//10A::count++;print(a1, b1);//11}
6.菱形继承
在c++中,多继承的结构模型是这样的,使用不当时会导致出现菱形继承的情况.导致内存中会重复出现一些变量.也会导致二义性
例如,在person中有一个表示年龄的age,在student与teacher中各有表示年龄的age,当professor继承student与teacher时,就会有两个age.这在现实环境中显然是不合理的
#include<iostream>
using namespace std;
class Person {
public:int age = 10;
};class Student :public Person
{
public:int stuid = 1;};
class Teacher :public Person
{int teaid = 2;
};
class Professor:public Student,public Teacher
{int profeid = 3;
};
int main()
{Professor p1;p1.Student::age = 100;p1.Teacher::age = 200;}
其在内存中的模型为:
可以看到此时出现数据冗余二义性.
C++解决这个问题的方法则是:菱形虚拟继承
7.菱形虚拟继承
一个新的关键字:virtual,在之后用到很多,但每个地方的含义都不大相同.
在继承方面,我们用virtual来修饰基类.也就是在上方结构模型的腰部
class Person {
public:int age = 10;
};class Student :virtual public Person
{
public:int stuid = 1;};
class Teacher :virtual public Person
{int teaid = 2;
};
class Professor:public Student,public Teacher
{int profeid = 3;
};
int main()
{Professor p1;p1.Student::age = 100;p1.Teacher::age = 200;cout<<sizeof(p1);
}
此时的内存模型为
结构模型为:
我们发现,重复出现的变量age修饰完只出现了一个.
观察内存模型,我们发现原来存age的地方,变成了一个指针.而age被放在了整个对象的最后一个位置.
打开内存模型,我们发现,这个指针指向了一块内存空间.叫虚基表.其中第一个位置为:0(其存放的为虚表偏移量) 第二个位置存着该指针相较于age的偏移量
为什么要这样设计呢:
-
解决了数据冗余
-
相同的对象可以调用同一个虚基表
-
存放偏移量让切割成为了可能
这里可以这样理解,当我创建了一个teacher的对象,将professor传入,则完成了切割,数据从teacher的指针开始访问,若我此时想要访问a,我直接读取偏移量即可.
总结
继承使C++底层变得复杂了起来,在日常使用中,需要避免出现菱形继承的问题.
更推荐使用组合:在一个类中调用另一个封装完的类,此时被调用的类的细节对调用类来说是不可见的.
相较于继承,更推荐使用组合的方式.高内聚低耦合一直是我们设计程序的原则
型,我们发现原来存age的地方,变成了一个指针.而age被放在了整个对象的最后一个位置.
[外链图片转存中…(img-WM0k8lWU-1692871122130)]
打开内存模型,我们发现,这个指针指向了一块内存空间.叫虚基表.其中第一个位置为:0(其存放的为虚表偏移量) 第二个位置存着该指针相较于age的偏移量
为什么要这样设计呢:
-
解决了数据冗余
-
相同的对象可以调用同一个虚基表
-
存放偏移量让切割成为了可能
这里可以这样理解,当我创建了一个teacher的对象,将professor传入,则完成了切割,数据从teacher的指针开始访问,若我此时想要访问a,我直接读取偏移量即可.
总结
继承使C++底层变得复杂了起来,在日常使用中,需要避免出现菱形继承的问题.
更推荐使用组合:在一个类中调用另一个封装完的类,此时被调用的类的细节对调用类来说是不可见的.
相较于继承,更推荐使用组合的方式.高内聚低耦合一直是我们设计程序的原则
相关文章:优先使用对象组合,而不是类继承
相关文章:

【C++技能树】继承概念与解析
Halo,这里是Ppeua。平时主要更新C,数据结构算法,Linux与ROS…感兴趣就关注我bua! 继承 0. 继承概念0.1 继承访问限定符 1. 基类和派生类对象赋值兼容转换2. 继承中的作用域3. 派生类中的默认成员函数4.友元5.继承中的静态成员6.菱…...

计算机网络 第二节
目录 一,计算机网络的分类 1.按照覆盖范围分 2.按照所属用途分 二,计算机网络逻辑组成部分 1.核心部分 (通信子网) 1.1电路交换 1.2 分组交换 两种方式的特点 重点 2.边缘部分 (资源子网) 进程通信的方…...

无涯教程-机器学习 - 矩阵图函数
相关性是有关两个变量之间变化的指示,在前面的章节中,无涯教程讨论了Pearson的相关系数以及相关的重要性,可以绘制相关矩阵以显示哪个变量相对于另一个变量具有较高或较低的相关性。 在以下示例中,Python脚本将为Pima印度糖尿病数…...
Redis 高可用与集群
Redis 高可用与集群 虽然 Redis 可以实现单机的数据持久化,但无论是 RDB 也好或者 AOF 也好,都解决 不了单点宕机问题,即一旦单台 redis 服务器本身出现系统故障、硬件故障等问题后, 就会直接造成数据的丢失,因此需要…...
修改文件名后Git仓上面并没有修改
场景: 我在本地将文件夹名称由Group → group ,执行git push 后,远程分支上的文件名称并没有修改。 原因: 是我绕过了git 直接使用了系统的重命名操作。 在 Git 中,对于已经存在的文件或文件夹进行大小写重命名是一个敏感的操作…...

Linux 信号
目录 基本概念信号的分类可靠信号与不可靠信号实时信号与非实时信号 常见信号与默认行为进程对信号的处理signal()函数sigaction()函数 向进程发送信号kill()函数raise() alarm()和pause()函数alarm()函数pause()函数 信号集初始化信号集测试信号是否在信号集中 获取信号的描述…...

深入探讨梯度下降:优化机器学习的关键步骤(二)
文章目录 🍀引言🍀eta参数的调节🍀sklearn中的梯度下降 🍀引言 承接上篇,这篇主要有两个重点,一个是eta参数的调解;一个是在sklearn中实现梯度下降 在梯度下降算法中,学习率…...
高频算法面试题
合并两个有序数组 const merge (nums1, nums2) > {let p1 0;let p2 0;const result [];let cur;while (p1 < nums1.length || p2 < nums2.length) {if (p1 nums1.length) {cur nums2[p2];} else if (p2 nums2.length) {cur nums1[p1];} else if (nums1[p1] &…...

Hive-启动与操作(2)
🥇🥇【大数据学习记录篇】-持续更新中~🥇🥇 个人主页:beixi 本文章收录于专栏(点击传送):【大数据学习】 💓💓持续更新中,感谢各位前辈朋友们支持…...

css transition 指南
css transition 指南 在本文中,我们将深入了解 CSS transition,以及如何使用它们来创建丰富、精美的动画。 基本原理 我们创建动画时通常需要一些动画相关的 CSS。 下面是一个按钮在悬停时移动但没有动画的示例: <button class"…...
LeetCode 面试题 02.05. 链表求和
文章目录 一、题目二、C# 题解 一、题目 给定两个用链表表示的整数,每个节点包含一个数位。 这些数位是反向存放的,也就是个位排在链表首部。 编写函数对这两个整数求和,并用链表形式返回结果。 点击此处跳转题目。 示例: 输入&a…...

一米脸书营销软件
功能优势 JOIN ADVANTAGE HOME PAGE MARKETING 公共主页营销 可同时对多个账户公共主页评论,点赞等 可批量邀请多个好友对Facebook公共主页进行评论点赞等,也可批量登录小号对自己公共主页进行点赞。 GROUP MARKETING 小组营销 可批量针对不同账户进行…...

vue 根据数值判断颜色
1.首先style样式给两种颜色 用:class 三元运算符判断出一种颜色 第一步:在style里边设置两种颜色 .green{color: green; } .orange{color: orangered; }在取数据的标签 里边 判断一种颜色 :class"item.quote.current >0 ?orange: green"<van-gri…...
Hugging Face 实战系列 总目录
PyTorch 深度学习 开发环境搭建 全教程 Transformer:《Attention is all you need》 Hugging Face简介 1、Hugging Face实战-系列教程1:Tokenizer分词器(Transformer工具包/自然语言处理) Hungging Face实战-系列教程1:Tokenize…...

国标视频云服务EasyGBS国标视频平台迁移服务器后无法启动的问题解决方法
国标视频云服务EasyGBS支持设备/平台通过国标GB28181协议注册接入,并能实现视频的实时监控直播、录像、检索与回看、语音对讲、云存储、告警、平台级联等功能。平台部署简单、可拓展性强,支持将接入的视频流进行全终端、全平台分发,分发的视频…...
HTML <th> 标签
实例 普通的 HTML 表格,包含两行两列: <table border="1"><tr><th>Company</th><th>Address</th></tr><tr><td>Apple, Inc.</td><td>1 Infinite Loop Cupertino, CA 95014</td></tr…...
HTTP/1.1协议中的响应报文
2023年8月30日,周三下午 目录 概述响应报文示例详述 概述 HTTP/1.1协议的响应报文由以下几个部分组成: 状态行(Status Line)响应头部(Response Headers)空行(Blank Line)响应体&a…...
TDengine函数大全-选择函数
以下内容来自 TDengine 官方文档 及 GitHub 内容 。 以下所有示例基于 TDengine 3.1.0.3 TDengine函数大全 1.数学函数 2.字符串函数 3.转换函数 4.时间和日期函数 5.聚合函数 6.选择函数 7.时序数据库特有函数 8.系统函数 选择函数 TDengine函数大全BOTTOMFIRSTINTERPLASTLAS…...
非关系型数据库Redis的安装
一、关系型数据库与非关系型数据库的区别:---------面试高频率问题 1、首先了解一下 什么是关系型数据库? 关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织。 优点: 易于维护:都是使用…...
oracle 创建数据库
查询表空间的命令 select t1.name,t2.name from v$tablespace t1,v$datafile t2 where t1.ts# t2.ts#; CREATE TABLESPACE ORM_342_BETA DATAFILE /app/oracle/oradata/sysware/ORM_342_BETA.DBF size 800M --存储地址 初始大小800M autoextend on nex…...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...

Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...