【C++】详细讲解继承(上)

 C++面向对象的三大特性:封装,继承,多态。现在我们就介绍一下继承。

1.继承的概念及定义

1.1 继承的概念

        继承机制是⾯向对象程序设计使代码可以 复⽤ 的最重要的⼿段。我们前面接触到的都是 函数 层次的复用,遇到过的 层次的复用有模板,而继承是 类层次 的一种新的复用
        继承允许我们在 保持原有类特性的基础上进⾏扩展 ,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类(或子类)。

 假如现在我们模拟校园环境,设计老师(Teacher)和学生(Student)两个类。老师和学生都有姓名、电话、地址、年龄等成员变量,都有身份认证相关成员函数。

class Student //学生
{
public:void identity() //身份认证{//...}protected:string _name; //姓名size_t _age;  //年龄string _add;  //住址string _tel;  //电话
};
class Teacher  //老师
{
public:void identity() //身份认证{//...}protected:string _name; //姓名size_t _age;  //年龄string _add;  //住址string _tel;  //电话
};

学生特有的变量是学号和学习相关的成员函数。

class Student //学生
{
public:void identity() //身份认证{//...}void study() //学习{//...}protected:string _name; //姓名size_t _age;  //年龄string _add;  //住址string _tel;  //电话string _stuid;//学号 
};

老师特有的变量是职称和教书相关的成员函数。

class Teacher  //老师
{
public:void identity() //身份认证{//...}void teaching() //教书{//...}
protected:string _name; //姓名size_t _age;  //年龄string _add;  //住址string _tel;  //电话string _title;//职称
};

但是我们会发现这样设计的两个类重复的地方特别多,显得很冗余。

那我们把公共的信息提取出来,放在一个Same类里面,Student和Teacher这两个类复用这个类,就不用重复定义了。

class Same
{
public:void identity() //身份认证{//...}
protected:string _name; //姓名size_t _age;  //年龄string _add;  //住址string _tel;  //电话
};

复用这个Same类,就是继承它,怎么继承?写法如下。

//学生
class Student : public Same
{
public:void study() //学习{//...}protected:string _stuid;//学号 
};
//老师
class Teacher : public Same 
{
public:void teaching() //教书{//...}protected:string _title;//职称
};

这就是继承的意义。接下来我们细说一下继承。

1.2 继承的定义

1.2.1 定义的格式

前面定义的Same类就是一个父类,也称作基类;Student类是子类,也称作派生类。

 继承方式有三个:public继承、private继承、protected继承。

1.2.2 继承基类成员访问⽅式的变化

由上面的表我们可以观察到:

  • 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
  • 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。(这里也体现出private和protected的区别)
  • 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有(private)成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式取权限小的:public > protected > private
如果我们不写继承方式,使⽤关键字 class 时默认的继承⽅式是 private ,使⽤ struct 时默认的继承⽅式是 public ,不过最好显⽰的写出继承⽅式。

在实际运⽤中 ⼀般使⽤都是public继承 ,⼏乎很少使⽤protetced/private继承,也不提倡使⽤
protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实
际中扩展维护性不强。

1.3 类模板的继承

之前我们用适配器模式写过栈和队列【C++】栈和队列的模拟实现(适配器模式)

这里我们还可以用 继承来实现栈和队列,以栈stack为例。
template<class T>
class stack : public std::vector<T> //继承vector
{};

然后再在stack里面实现相关接口。

template<class T>
class stack : public std::vector<T> //继承vector
{
public:void push(const T& x){vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}
};

(注意:基类是类模板时,需要指定⼀下类域, 否则编译报错:error C3861: “push_back”: 找不到标识符   相关的错误。)

这里stack的实现就是用了继承来实现。这个代码还可以进行改进,如下。

#define CONTAINER std::vector //宏
template<class T>
class stack : public CONTAINER<T> //继承
{
public:void push(const T& x){CONTAINER<T>::push_back(x);}void pop(){CONTAINER<T>::pop_back();}const T& top(){return CONTAINER<T>::back();}bool empty(){return CONTAINER<T>::empty();}
};

宏替换,就可以改变stack的底层逻辑,可以换成list,deque。

2.父类和子类对象赋值兼容转换

public继承 的派⽣类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引⽤ 。这⾥有个形象的说法叫 切⽚ 或者 切割 。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

比如说现在有 Student类的指针ptr1Same类指针ptr2,ptr1赋值给ptr2,就是派生类(子类)对象赋值给基类(父类),ptr2只会指向ptr1中基类有的部分。

引用同理。

Student st;//子类对象
Same sa = st; //子类对象赋值给父类对象
Same* psa = &st;//子类对象赋值给父类指针
Same& rsa = sa; //子类对象赋值给父类引用

(这里所有的赋值都不会产生临时变量,因为子类直接做了切片给父类)

父类(基类)对象不能赋值给子类(派生类)。

Student st;//子类对象
Same sa; //父类对象
sa = st;//子类赋值给父类(可以,做切片)
st = sa;//父类赋值给子类(不可以)
子类的 指针或者引⽤可以 通过强制类型转换赋值给父类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。(等以后细说)

3.继承中的作用域

3.1 隐藏规则

下面的类基类和派生类有一个同名的成员变量是_name。

class Same //基类
{ 
protected:string _name = "123";size_t _age; 
};
class Teacher : public Same //派生类
{
public:void Print(){cout << _name << endl;}
protected:string _name = "456";
};

那我们在派生类中访问_name的时候到底访问的是哪个?

int main()
{Teacher t;t.Print();return 0;
}

 结果是显示派生类(子类)里的_name。

如果我们就想访问基类(父类)里的_name,可以直接用 基类::基类成员 显⽰访问,如下。

class Teacher : public Same
{
public:void Print(){cout << Same::_name << endl; //指定作用域访问}
protected:string _name = "456";
};

  • 在继承体系中基类和派⽣类都有独⽴的作⽤域
  • 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  • 注意在实际中在继承体系⾥⾯最好不要定义同名的成员

3.2 相关练习

class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" << i << endl;}
};int main()
{B b;b.fun(10);b.fun();return 0;
};

答案: B.隐藏

为什么不是重载?因为函数构成重载的要求是两个函数在同一作用域。而父类和子类有独立的作用域。

 

答案: A.编译报错 

如果是下面这样调用,没有传参

int main()
{B b;b.fun();return 0;
};

先看子类里的fun,子类里的fun是需要传参的,匹配不成功,会不会继续去父类里找?不会,子类把父类同名函数隐藏了,直接报错。

如果传参,像下面这样,就会调用子类里的fun函数。

int main()
{B b;b.fun(10);return 0;
};

如果就是想要父类里的fun函数,直接指定定义域调用。

int main()
{B b;b.fun(10);b.A::fun(); //指定return 0;
};

 

4.派生类的默认成员函数 

4.1 四个常见的默认成员函数

4.1.1 默认构造

我们不写,编译器默认生成的构造函数的行为:

1.对内置类型->是否初始化是不确定的。

2.对自定义类型->调用默认构造

3.继承父类成员看作一个整体对象,要求调用父类的默认构造

  • 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员
  • 如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
  • 派⽣类对象初始化调⽤基类构造派⽣类构造。

 比如说Same类是基类,我们显示写它的默认构造,Student类为派生类,不显示写。

class Same //基类
{
public:Same(const char* name = "peter"): _name(name){cout << "Same()" << endl;}
protected:string _name;
};
class Student : public Same  //派生类
{
public://没有显示写默认构造,编译器自己生成
protected:int _num;string _add;
};

按照规则,_num是内置类型,可能初始化也可能不初始化;_add是自定义类型,调用string自己的初始化;继承还要调用父类的默认构造,所以_name应该被初始化为peter。

 假如我们不显示写基类的默认构造,就必须在派生类显示调用。

class Same //基类
{
public:Same(const char* name) //此时基类没有默认构造: _name(name){cout << "Same()" << endl;}
protected:string _name;
};
class Student : public Same
{
public:Student(const char* string, int num, const char* add):Same(string) //在初始化列表阶段显示调用, _num(num),_add(add){}
protected:int _num;string _add;
};

然后我们传参,就可以了。

int main()
{Student st("张三", 0, "Chain");return 0;
}

4.1.2 拷贝构造

1.对内置类型 -> 值拷贝

2.对自定义类型 -> 调用自己的拷贝构造函数

3.对于继承成员看作一个整体对象,要求调用父类的拷贝构造

  • 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
class Same //基类
{
public:Same(const char* name = "peter") //默认构造: _name(name) {cout << "Same()" << endl;}Same(const Same& p) //拷贝构造: _name(p._name){cout << "Same(const Same& p)" << endl;}protected:string _name;
};
class Student : public Same //派生类
{
public:Student(const char* string, int num, const char* add):Same(string) //在初始化列表阶段显示调用, _num(num),_add(add){}//拷贝构造一般不用自己写
protected:int _num;string _add;
};

一般情况下,派生类(子类)默认生成的拷贝构造就够用了,不用自己写,如果有需要深拷贝的资源,才需要自己写。

int main()
{Student st1("张三", 0, "Chain");Student st2(st1);return 0;
}

_num是内置类型,进行值拷贝;_add是自定义类型string,调用string自己的拷贝构造;_name是父类继承成员,调用父类Same的拷贝构造。

如果我们要在派生类(子类)中显示地写拷贝构造,写法如下。

Student(const Student& s) //显示地写子类的拷贝构造:Same(s)//父类的拷贝构造,_add(s._add),_num(s._num)
{//假设里面是深拷贝资源的拷贝逻辑
}

Same是父类,Same后面的括号里应该传父类的对象,但是我们没有父类的对象,只有子类的对象s,为什么可以直接传s过去?

这里用到的就是前面说过的  子类和父类对象赋值兼容转换  。我们要拷贝父类的那一部分,就要把父类的那一部分拿出来,我们把子类对象s传给父类Same,Same的拷贝构造函数是引用传参

这里引用的就是子类对象中切出来的父类的那一部分

4.1.3 赋值运算符重载

  • 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。
  • 需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。

我们先实现一个基类(父类)的赋值重载。

class Same //基类
{
public:Same(const char* name = "peter") //默认构造: _name(name){cout << "Same()" << endl;}Same(const Same& p) //拷贝构造: _name(p._name){cout << "Same(const Same& p)" << endl;}Same& operator=(const Same& p) //赋值重载{cout << "Same& operator=(const Same& p)" << endl;if (this != &p)_name = p._name;return *this;}protected:string _name;
};
class Student : public Same //派生类
{
public:Student(const char* string, int num, const char* add):Same(string) //在初始化列表阶段显示调用, _num(num),_add(add){}//没有实现operator=protected:int _num;string _add;
};

operator=和拷贝构造差不多,我们不在派生类(子类)中显示地写赋值重载时,编译器自动生成的就够用。

int main()
{Student st1("张三", 0, "Chain");Student st2("李四", 1, "LA");st1 = st2;return 0;
}

也是内置类型值拷贝,自定义类型调用自己的operator=,继承父类成员看作整体,调用父类的operator=。

 如果有需要深拷贝的资源,才需要自己实现。自己实现的话,写法如下。

Student& operator=(const Student& s)
{if (this != &s){Same::operator=(s);//显示调用基类的赋值重载_num = s._num;_add = s._add;//深拷贝逻辑}return *this;
}

显示调用父类(基类)的operator=时,要指定类域,因为同名函数子类的会把父类的隐藏,屏蔽基类对同名成员的直接访问,如果不指定类域,会造成栈溢出。

4.1.4 析构

  • 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。
class Same //基类
{
public:Same(const char* name = "peter") //默认构造: _name(name){cout << "Same()" << endl;}Same(const Same& p) //拷贝构造: _name(p._name){cout << "Same(const Same& p)" << endl;}Same& operator=(const Same& p) //赋值重载{cout << "Same& operator=(const Same& p)" << endl;if (this != &p)_name = p._name;return *this;}~Same() //析构{cout << "~Same()" << endl;}
protected:string _name;
};

派生类(子类)默认生成的析构函数就够了,如果有需要资源释放的时候才需要自己实现。 

  • 编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系

所以我们如果需要在子类(派生类)自己写析构函数时,不可以像下面这样。 

~Student()
{~Same(); //错误的写法//资源释放逻辑...
}

既然构成隐藏,调用就需要指定类域调用。

~Student()
{Same::~Same(); //正确的写法//资源释放逻辑...
}

但是我们会发现,下面的代码明明只有两个对象,却调用了4次析构函数。

而调用析构次数太多会出问题。

  • 对于析构函数,在子类(派生类)显示写时,里面不需要显示调用父类(基类)的析构可以认为这是一个规定,为了保证析构顺序是先子后父,子类析构函数之后,会自动调用父类的析构。
~Student()
{//资源释放逻辑...//自动调用父类析构
}

 

 

4.2 实现一个不能被继承的类

⽅法1:将基类的构造函数 私有 ,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个 final 关键字,final修改基类,派⽣类就不能继承了。
class Same final //基类加final后不可被继承
{
public://成员函数
protected:string _name;
};

本次分享见到这里,我们下篇见~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/893473.shtml

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

相关文章

AIGC专栏18——EasyAnimateV5.1版本详解 应用Qwen2 VL作为文本编码器,支持轨迹控制与相机镜头控制

AIGC专栏18——EasyAnimateV5.1版本详解 应用Qwen2 VL作为文本编码器&#xff0c;支持轨迹控制与相机镜头控制 学习前言相关地址汇总源码下载地址HF测试链接MS测试链接 测试效果Image to VideoText to Video轨迹控制镜头控制 EasyAnimate详解技术储备Qwen2 VLStable Diffusion …

1905电影网中国地区电影数据分析(一) - 数据采集、清洗与存储

文章目录 前言一、数据采集步骤及python库使用版本1. python库使用版本2. 数据采集步骤 二、数据采集网页分析1. 分析采集的字段和URL1.1 分析要爬取的数据字段1.2 分析每部电影的URL1.2 分析每页的URL 2. 字段元素标签定位 三、数据采集代码实现1. 爬取1905电影网分类信息2. 爬…

【25】Word:林涵-科普文章❗

目录 题目​ NO1.2.3 NO4.5.6 NO7.8 NO9.10 NO11.12 不连续选择&#xff1a;按住ctrl按键&#xff0c;不连续选择连续选择&#xff1a;按住shift按键&#xff0c;选择第一个&#xff0c;选择最后一个。中间部分全部被选择 题目 NO1.2.3 布局→纸张方向&#xff1a;横向…

P6周:VGG-16算法-Pytorch实现人脸识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 我的环境 语言环境&#xff1a;Python 3.8.12 编译器&#xff1a;jupyter notebook 深度学习环境&#xff1a;torch 1.12.0cu113 一、前期准备 1.设置GPU im…

【Rust自学】14.4. 发布crate到crates.io

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 14.4.1. 创建并设置crates.io账号 在发布任何 crate 之前&#xff0c;你需要在 crates.io并…

数据结构——实验八·学生管理系统

嗨~~欢迎来到Tubishu的博客&#x1f338;如果你也是一名在校大学生&#xff0c;正在寻找各种编程资源&#xff0c;那么你就来对地方啦&#x1f31f; Tubishu是一名计算机本科生&#xff0c;会不定期整理和分享学习中的优质资源&#xff0c;希望能为你的编程之路添砖加瓦⭐&…

IBM湖仓一体与向量数据库:访问MinIO控制台(Accessing the MinIO console)

_1、从密钥中复制S3凭证并保存 &#xff08;Copy the S3 credentials from the secret and save it &#xff09; oc extract secret/ibm-lh-config-secret -n ${PROJECT_CPD_INST_OPERANDS} --to- --keysenv.properties | grep -E "LH_S3_ACCESS_KEY|LH_S3_SECRET_KEY&q…

Ragas-RAG能力评测

Ragas是一个框架&#xff0c;它可以帮助你从不同的方面评估你的问答&#xff08;QA&#xff09;流程。它为你提供了一些指标来评估你的问答系统的不同方面&#xff0c;具体包括&#xff1a; 评估检索&#xff08;context&#xff09;的指标&#xff1a;提供了上下文相关性&…

基于ESP32-IDF驱动GPIO输出控制LED

基于ESP32-IDF驱动GPIO输出控制LED 文章目录 基于ESP32-IDF驱动GPIO输出控制LED一、点亮LED3.1 LED电路3.2 配置GPIO函数gpio_config()原型和头文件3.3 设置GPIO引脚电平状态函数gpio_set_level()原型和头文件3.4 代码实现并编译烧录 一、点亮LED 3.1 LED电路 可以看到&#x…

使用ffmpeg提高mp4压缩比,减小文件体积【windows+ffmpeg+batch脚本】

文章目录 关于前情提要FFmpeg是什么使用脚本运行FFmpeg首先&#xff0c;下载ffmpeg.exe然后在视频相同位置写一个bat脚本运行压缩脚本 关于 个人博客&#xff0c;里面偶尔更新&#xff0c;最近比较忙。发一些总结的帖子和思考。 江湖有缘相见&#x1f91d;。如果读者想和我交…

Vue3初学之Element Plus Dialog对话框,Message组件,MessageBox组件

Dialog的使用&#xff1a; 控制弹窗的显示和隐藏 <template><div><el-button click"dialogVisible true">打开弹窗</el-button><el-dialogv-model"dialogVisible"title"提示"width"30%":before-close&qu…

自然语言处理(NLP)-总览图学习

文章目录 自然语言处理&#xff08;NLP&#xff09;-总览图学习1.一张总览图的学习1. 语音学&#xff08;Phonology&#xff09;2. 形态学&#xff08;Morphology&#xff09;3. 句法学&#xff08;Syntax&#xff09;4. 语义学&#xff08;Semantics&#xff09;5. 推理&#…

机器学习 vs 深度学习

目录 一、机器学习 1、实现原理 2、实施方法 二、深度学习 1、与机器学习的联系与区别 2、神经网络的历史发展 3、神经网络的基本概念 一、机器学习 1、实现原理 训练&#xff08;归纳&#xff09;和预测&#xff08;演绎&#xff09; 归纳: 从具体案例中抽象一般规律…

谈谈RTMP|RTSP播放器视频view垂直|水平反转和旋转设计

技术背景 我们在做RTMP|RTSP播放器的时候&#xff0c;有这样的技术诉求&#xff0c;有的摄像头出来的数据是有角度偏差的&#xff0c;比如“装倒了”&#xff0c;或者&#xff0c;图像存在上下或者左右反转&#xff0c;这时候&#xff0c;就需要播放器能做响应的处理&#xff…

论文阅读--Qwen22.5技术报告

Qwen2 1 引言 所有模型都是在超过7 trillion token&#xff08;7万亿&#xff09;的高质量、大规模数据集上预训练的 2 Tokenizer & Model 2.1 Tokenizer 沿用Qwen&#xff08;Bai等人&#xff0c;2023a&#xff09;的做法&#xff0c;我们采用了基于字节级字节对编码…

FPGA中场战事

2023年10月3日,英特尔宣布由桑德拉里维拉(Sandra Rivera)担任“分拆”后独立运营的可编程事业部首席执行官。 从数据中心和人工智能(DCAI)部门总经理,转身为执掌该业务的CEO,对她取得像AMD掌门人苏姿丰博士类似的成功,无疑抱以厚望。 十年前,英特尔花费167亿美元真金白银…

【jmeter】下载及使用教程【mac】

1.安装java 打开 Java 官方下载网站https://www.oracle.com/java/technologies/downloads/选择您想要下载的 Java 版本&#xff0c;下载以 .dmg 结尾的安装包&#xff0c;注意 JMeter 需要 Java 8下载后打开安装包点击“安装”按钮即可 2.下载jmeter 打开 Apache JMeter 官方…

postman请求参数化

postman界面介绍 一、使用环境变量(Environment Variables)进行参数化 1、在请求中使用环境变量 在请求的url、请求头(Headers)、请求体(Body)等部分都可以使用环境变量。 URL 部分示例 点击 Postman 界面右上角的 “眼睛” 图标(Environment Quick Look)打开环境管理…

2024年博客之星年度评选|第一步——创作影响力评审入围Top300名单 | 博客之星陪跑指南

2024年博客之星年度评选&#xff5c;第一步——创作影响力评审入围Top300名单 | 博客之星陪跑指南 2024年博客之星年度评选正在如火如荼地进行中&#xff01;作为博客圈最具影响力的评选活动之一&#xff0c;今年的评选吸引了众多优秀博主的参与。现在&#xff0c;距离Top300入…

阻燃高温尼龙行业:市场潜力巨大,引领材料科学新变革

在当今快速发展的工业和材料科学领域&#xff0c;阻燃高温尼龙作为一种兼具卓越防火性能和高温稳定性的新型材料&#xff0c;正逐步成为多个领域的首选材料。随着全球对安全性能要求的提高和技术的不断创新&#xff0c;阻燃高温尼龙市场呈现出快速增长的态势&#xff0c;展现出…