继承【C++】
文章目录
- 继承的概念
- 继承的定义
- 继承方式和访问限定符
- 继承基类成员访问方式的变化
- 默认继承方式
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元
- 静态成员
- 菱形继承及菱形虚拟继承
- 继承的方式
- 菱形虚拟继承
- 菱形虚拟继承原理
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
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();return 0;
}
继承后,父类Person的成员,包括成员函数和成员变量,都会变成子类的一部分,也就是子类Student和Teacher复用了父类Person的成员。
继承的定义
Person是父类,也称作基类。Student是子类,也称作派生类
继承方式和访问限定符
访问限定符有以下三种:public访问、protected访问、private访问
继承的方式也有类似的三种:public继承、protected继承、private继承
继承基类成员访问方式的变化
基类当中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。
这里所说的不可见,从语法上限制访问(类里面和类外面都不能用)。
跟private访问限定符不一样,private访问限定符类外面不能使用,类里面可以使用
//基类
class Person
{
private:string name = "张三";
};
//派生类
class Student :public Person
{
public:void Print(){//在派生类当中访问基类的private成员cout << _name << endl;//error}
protected:int _stuid; // 学号
};
上述代码中 ,Student类继承了Person类,但是无法在Student类当中访问Person类当中的private成员_name。如果需要在派生类中能访问,就将private修改为protected
-
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
-
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public> protected> private。
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
-
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
默认继承方式
在使用继承的时候也可以不指定继承方式,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。
//基类
class Person
{
public:string _name = "张三"; //姓名
};
//派生类
class Student : Person //默认为private继承
{
protected:int _stuid; //学号
};
在关键字为class的派生类当中,所继承的基类成员_name的访问方式变为private。
//基类
class Person
{
public:string _name = "张三"; //姓名
};
//派生类
struct Student : Person //默认为public继承
{
protected:int _stuid; //学号
};
在关键字为struct的派生类当中,所继承的基类成员_name的访问方式仍为public。
基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
//基类
class Person
{
public:string _name; //姓名string _sex; //性别int _age; //年龄
};//派生类
class Student : public Person
{
protected:int _stuid; //学号
};int main()
{Student s;Person p = s; //派生类对象赋值给基类对象Person* ptr = &s;//派生类对象赋值给基类指针Person& ref = s; //派生类对象赋值给基类引用return 0;
}
派生类对象赋值给基类对象图示:
注意:基类对象不能赋值给派生类对象。
继承中的作用域
在继承体系中的基类和派生类都有独立的作用域。如果子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义
例如:
//父类
class Person
{
protected:int _num = 111;
};
//子类
class Student : public Person
{
public: void fun(){cout << _num << endl;}
protected:int _num = 999;
};
int main()
{Student s;s.fun();//访问子类的_numreturn 0;
}
如果需要访问父类当中的_num成员,使用作用域限定符进行指定访问。
//父类
class Person
{
protected:int _num = 111;
};
//子类
class Student : public Person
{
public: void fun(){cout << Person::_num << endl;}
protected:int _num = 999;
};
int main()
{Student s;s.fun();//访问子类的_numreturn 0;
}
下面的代码,调用成员函数fun时将调用子类当中的fun,如果需要调用父类当中的fun,则使用作用域限定符指定类域。
//父类
class Person
{
public:void fun(int x){cout << x << endl;}
};
//子类
class Student : public Person
{
public:void fun(double x){cout << x << endl;}
};
int main()
{Student s;s.fun(3.14); //直接调用子类当中的成员函数funs.Person::fun(20); //指定调用父类当中的成员函数funreturn 0;
}
上述代码,父类中的fun和子类中的fun不是构成函数重载,构成函数重载需要两个函数在同一作用域,而此时这两个fun函数并不在同一作用域。为了避免类似问题,实际在继承体系当中最好不要定义同名的成员。
隐藏: 父子类域中,成员函数的函数名相同就构成隐藏,不用管函数参数,只需要看函数名即可
派生类的默认成员函数
默认成员函数,即我们不写编译器会自动生成的函数,类当中的默认成员函数有以下六个:
//基类
class Person
{
public://构造函数Person(const string& 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;}
private:string _name;
};//派生类
class Student : public Person
{
public://默认构造函数//先父后子Student(const string name = "张三", int id = 0):Person(name)//匿名对象 ,调用基类的构造函数初始化基类的成员, _id(id){}//拷贝构造Student(const Student& s):Person(s), _id(s._id){}//赋值Student& operator= (const Student& s){if (this != &s){Person::operator=(s);//?_id = s._id;}return *this;}~Student(){//显示调用父类析构,无法保证先子后父// Person::~Person();//在子类析构函数完成,就调用父类析构,这样就保证了先子后父delete _ptr;}
private:int _id;int* _ptr = new int;
};
int main()
{Student s1;Student s2(s1);Student s3("李四", 1);s1 = s3;return 0;
}
-
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
-
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
-
派生类的operator=必须要调用基类的operator=完成基类的复制。
-
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
-
派生类对象初始化先调用基类构造再调派生类构造。
-
派生类对象析构清理先调用派生类析构再调基类的析构。
-
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
注意:
1、派生类和基类的赋值运算符重载函数,函数名相同,但是在不同作用域,构成隐藏,所以在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用。
2、由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();。因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。
3、在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用。
继承与友元
友元关系不能继承
即基类的友元可以访问基类的私有和保护成员,但是不能访问派生类的私有和保护成员。
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public://声明Display是Person的友元friend void Display(const Person& p, const Student& s);
protected:string _name; //姓名
};
class Student : public Person
{
protected:int _id; //学号
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl; //可以访问cout << s._id << endl; //err
}
int main()
{Person p;Student s;Display(p, s);return 0;
}
如果Display函数需要访问派生类Student的私有和保护成员,在派生类Student当中进行友元声明。
//有元关系不能继承
class Student;
class Person
{
public: friend void Display(const Person & p, const Student & s);
protected:string _name;
};class Student :public Person
{friend void Display(const Person& p, const Student& s);
protected :int _id;
};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;
}
静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
//基类
class Person
{
public:Person(){_count++;}
protected :string _name;
public:static int _count;
};int Person::_count = 0;
//派生类
class Student : public Person
{
protected :string _seminarCourse; //研究科目
};int main()
{Person p;Student s1;Student s2;cout << Person::_count << endl;return 0;
}
菱形继承及菱形虚拟继承
继承的方式
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承。
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承有数据冗余和二义性的问题。
//基类
class Person
{
public:string _name;
};
//派生类
class Student :public Person
{protected:int _num;
};
class Teacher :public Person
{protected:int _id;
};//菱形继承
class Assistant :public Student, public Teacher
{
protected:string _majorCourse; //主修课程
};int main()
{Assistant a;//a._name = "peter";//errreturn 0;
}
Assistant对象是多继承的Student和Teacher,而Student和Teacher当中都继承了Person,因此Student和Teacher当中都有_name成员,直接访问Assistant的_name成员,编译器不知道访问哪一个,会报错
解决方案:
//显示指定访问哪个父类的成员
a.Student::_name = "张三";
a.Teacher::_name = "李四";
上述解决方案可以解决二义性问题,但是不能解决数据冗余,在Assistant的对象在Person成员始终会存在两份。
菱形虚拟继承
为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承。
在Student和Teacher继承Person是使用虚拟继承,即可解决问题。
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:string _name; //姓名
};
class Student : virtual public Person //虚拟继承
{
protected:int _num; //学号
};
class Teacher : virtual public Person //虚拟继承
{
protected:int _id; //职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; //主修课程
};
int main()
{Assistant a;a._name = "peter";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;//解决了数据冗余cout << &a.Student::_name << endl;cout << &a.Teacher::_name << endl;return 0;
}
此时就可以直接访问Assistant对象的_name成员了,解决了二义性问题
菱形虚拟继承原理
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类对象当中各个成员在内存当中的分布情况如下:
菱形继承的数据冗余和二义性,本质上是D类对象当中含有两个_a成员。
如果使用菱形虚拟继承时,菱形继承当中D类对象的各个成员在内存当中的分布情况。
#include <iostream>
using namespace std;
class A
{
public:int _a;
};
class B : virtual public A
{
public:int _b;
};
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;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a
若是将D类对象赋值给B类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类A的成员,得到切片后该B类对象在内存中仍然保持这种分布情况。
D d;
B b = d; //切片行为
相关文章:

继承【C++】
文章目录 继承的概念继承的定义继承方式和访问限定符继承基类成员访问方式的变化 默认继承方式 基类和派生类对象赋值转换继承中的作用域派生类的默认成员函数继承与友元静态成员菱形继承及菱形虚拟继承继承的方式 菱形虚拟继承菱形虚拟继承原理 继承的概念 继承(inheritance)…...

ORB-SLAM3复现过程中遇到的问题及解决办法
在复现过程中遇到的问题的解决过程 1. 版本检查1.1 Opencv版本的检测1.2 Eigen版本的检测1.3 查看Python版本1.4 其他 2. 编译过程中遇到的问题及解决办法2.1 ./build.sh遇到的问题2.2 ./build_ros.sh遇到的问题 因为环境比较干净,所以遇到的问题相对少一些…...
vue开发桌面exe应用
vue开发桌面exe应用 Electron-vue 参考 Electron-vue搭建vue全家桶Element UI客户端(一) 如何使用Vue.js构建桌面应用程序...
C# 实现PictureBox从随机选择的文件夹内对图像进行随机播放
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System...

腾讯云国际代充-GPU服务器安装驱动教程NVIDIA Tesla
腾讯云国际站GPU 云服务器是基于 GPU 的快速、稳定、弹性的计算服务,主要应用于深度学习训练/推理、图形图像处理以及科学计算等场景。 GPU 云服务器提供和标准腾讯云国际 CVM 云服务器一致的方便快捷的管理方式。 GPU 云服务器通过其强大的快速处理海量数据的计算性…...

【python爬虫】9.带着小饼干登录(cookies)
文章目录 前言项目:发表博客评论post请求 cookies及其用法session及其用法存储cookies读取cookies复习 前言 第1-8关我们学习的是爬虫最为基础的知识,从第9关开始,我们正式打开爬虫的进阶之门,学习爬虫更多的精进知识。 在前面几…...

原神剑冢三层封印怎么解开 原神剑冢三层封印在哪里打
在原神游戏中原神探索剑冢封印并解开三层封印,玩家可以去蒙德城接取一个隐藏任务,这项任务需要玩家去解开剑冢三层封印,才能完成任务。然而,许多玩家可能还不知道如何解开这个封印,今天小编为大家整理了一份详细的攻略…...
Papers with Semi-supervised Learning for Medical Image Segmentation(SSL4MIS)
Papers_with_SSL4MIS CVPR2023 DateCategory标题TitleCodeBlog2023-06半监督医学图像分割用于半监督医学图像分割的伪标签引导对比学习Pseudo-Label Guided Contrastive Learning for Semi-Supervised Medical Image SegmentationLinkLink2023-06半监督图像分割SemiCVT&#…...

c#继承(new base)的使用
概述 C#中的继承是面向对象编程的重要概念之一,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和行为。 继承的主要目的是实现代码重用和层次化的组织。子类可以继承父类的字段、属性、方法和事…...

【办公自动化】使用Python批量处理Excel文件并转为csv文件
🤵♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞Ǵ…...

手机怎么剪视频?分享一些剪辑工具和注意事项
视频剪辑是一种将多个视频片段进行剪切、合并和编辑的技术,它可以帮助我们制作出精彩的视频作品。如今,随着智能手机的普及,我们可以随时随地使用手机进行视频剪辑。本文将为大家介绍一些手机剪辑工具和注意事项,帮助大家更好地进…...

面试题汇总
文章目录 一. 腾讯二. 华为三. 快手1. Long 的长度和范围,为什么要减 1 (Java基础)2. 线程池配置无界队列了之后,拒绝策略怎么搞,什么时候用到无界队列 (JUC并发) 四. 美团五. 阿里六. 百度七. 字节八. 大疆1. 为什么创建进程开销比线程大? …...
LLVM系列:1.设计思想和LLVM IR简介
文章目录 背景介绍内容简介LLVM库的集合以及模块化设计LLVM优化器的模块化设计LLVM代码生成器的模块化设计LLVM IRLLVM编译流程LLVM IR的设计思想LLVM的形式LLVM IR的结构组成指令格式和变量示例参考文献:背景介绍 LLVM项目于2000年创立于伊利诺斯州大学,原本是一个为了静态…...
mysql中间件Atlas
Mysql 的 proxy 中间件有比较多的工具,例如,mysql-proxy(官方提供), atlas , cobar, mycat, tddl, tinnydbrouter等等。 而Atlas是由 Qihoo 360公司Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。它在MySQL官方推出的MySQL-Pro…...

ChatGPT 实现动态地图可视化展示
地图可视化分析有许多优点和好处: 1.直观理解:地图可视化使得复杂的数据更易于理解。通过地图可视化,人们可以直观地看到地理位置、地区之间的关系以及空间分布的模式。 2.提高决策效率:地图可视化可以帮助决策者快速理解和解释数据,从而提高决策效率。 3.高效的数据整…...

Vue.js安装步骤和注意事项
安装完node.js后开始安装和部署Vue在检查webpack的下载版本时出现错误出现错误的原因是之前下载时未指定对应的版本号导致版本不兼容先卸载掉之前下载的版本 cnpm uninstall webpack-cli -g cnpm install webpack-cli4.9.2 -g 最后检查版本是否对应...

IDEA中Run/Debug Configurations添加VM options和Program arguments
1. 现象描述 我在我的IDEA当中打开配置模板后,发现没有VM options和Program arguments,也就是虚拟机选项和程序实参这两项,导致我不能配置系统属性参数和命令行参数!!!!!࿰…...
信息技术03--初/高中--简答题(73道知识简答)
文章目录 第一章 初中简答 1-231.1、请简要回答TCP/IP中传输层的功能以及两种主要协议1.2、请简要回答”数字化学习与创新“主要体现在哪些方面?1.3、人工智能给人类社会带来了巨大的变化也带来了一些问题,请列举出三个涉及个人隐私问题的场景并进行简要…...

Matlab之统计一维数组直方图 bin 计数函数histcounts
一、语法 [N,edges] histcounts(X) [N,edges] histcounts(X,nbins) [N,edges] histcounts(X,edges) 解释: 1.1 [N,edges] histcounts(X) 将 X 的值划分为多个 bin,并返回每个 bin 中的计数以及 bin 边界。histcounts 函数使用自动分 bin 算法&am…...
TDengine函数大全-时间和日期函数
以下内容来自 TDengine 官方文档 及 GitHub 内容 。 以下所有示例基于 TDengine 3.1.0.3 TDengine函数大全 1.数学函数 2.字符串函数 3.转换函数 4.时间和日期函数 5.聚合函数 6.选择函数 7.时序数据库特有函数 8.系统函数 时间和日期函数 TDengine函数大全NOWTIMEDIFFTIMETRU…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...

如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...
Leetcode33( 搜索旋转排序数组)
题目表述 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...
区块链技术概述
区块链技术是一种去中心化、分布式账本技术,通过密码学、共识机制和智能合约等核心组件,实现数据不可篡改、透明可追溯的系统。 一、核心技术 1. 去中心化 特点:数据存储在网络中的多个节点(计算机),而非…...

恶补电源:1.电桥
一、元器件的选择 搜索并选择电桥,再multisim中选择FWB,就有各种型号的电桥: 电桥是用来干嘛的呢? 它是一个由四个二极管搭成的“桥梁”形状的电路,用来把交流电(AC)变成直流电(DC)。…...