C++?继承!!!

一、引言

        代码的复用对于代码的质量以及程序员的代码设计上都是非常重要的,C++中的许多特性都体现了这一点,从函数复用、模板的引入到今天我们将一起学习的:继承

二、什么是继承?

        1、继承的概念

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

        注:接下来我们将多次用到下面的继承场景,即人、老师与学生三者之间的关系:
        

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "zhao"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
protected:int _stuid; //学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};

        2、继承的定义

                (1).定义继承的格式

                注:括号内为对于其前元素的说明

class Student(派生类/子类) : public(继承方式) Person
{
public://子类的成员函数列表//子类的成员变量列表
};
                (2).继承关系与访问限定符

                继承方式有三种,分别对应了三种访问限定符,它们分别是:
                

                不同的继承方式与不同的访问限定符组合会产生九种不同的效果:
                                 可以发现,基类的私有成员在派生类中一定不可见(在类外与类内都不可访问,可以认为没有这个成员),对于其他情况来说,一个成员的权限是访问限定符与继承方式中权限更小的那一个,所以在可能用到继承的类中,我们更倾向于使用保护作为基类的访问限定符

三、父子类之间的对象赋值转换

           父子类之间支持子类对象、指针和引用向父类进行赋值转换,这时会发生切片赋值,也就是会将子类中属于父类成员的那一部分赋值给父类对象,同时切掉独属于子类的那一部分,对于引用和指针也是类似的,引用赋值时,父类的引用类型是子类中属于父类对象的别名;指针赋值时,父类的指针类型直接指向了子类中属于父类对象的那一部分

        需要注意的是,父类对象不能,转换赋值给子类对象,这点是很好理解的,因为父类对象中不包含子类对象的内容

        上面几点可以通过下面的代码及其运行结果说明,为了方便演示,我将上面提供过的类中的protected成员换成了public成员:
        代码:
        

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}string _name = "zhao"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
public:int _stuid; //学号
};class Teacher : public Person
{
public:int _jobid; // 工号
};
int main()
{Student s;s._age = 10;s._name = "mei";Teacher t;Person p = s;Person* pp = &s;Person& rp = s;p.Print();pp->Print();rp.Print();
}

        运行结果:
        

四、继承中的作用域

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

        2、子类和父类中如果有同名成员,那么子类的成员将会屏蔽掉父类中与它同名的成员,使其不能直接访问,这种情况叫做隐藏,也叫做重定义

        下面的代码和运行结果可以说明2中的问题:
        代码:
        

class Person
{
protected:string _name = "zhao"; // 姓名int _num = 1010; // 身份证号
};class Student : public Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "num:" << _num << endl;}
protected:int _num = 10; //学号
};
int main()
{Student s;s.Print();
}

        运行结果:
        

        3、需要注意的是,函数要构成隐藏,只需要函数名相同就构成隐藏

        下面的代码及运行结果可以说明3中的问题:
        代码:
        

class Person
{void Print(int x = 10){cout << "Person" << endl;}
protected:string _name = "zhao"; // 姓名int _num = 1010; // 身份证号
};class Student : public Person
{
public:void Print(){cout << "Student" << endl;}
protected:int _num = 10; //学号
};
int main()
{Student s;s.Print();
}

        运行结果:
        

        4、注意在实际应用中最好不要在继承体系中定义同名成员,很容易混淆

五、派生类中的默认成员函数

        每一个类中都有6个默认成员函数,“默认”是指我们不写,编译器会自动生成的函数,在这里对普通类中的默认成员函数不多做赘述,如果想要了解关于默认成员函数的详细内容可以跳转到以下链接:
        C++?类和对象(中)!!!-CSDN博客https://blog.csdn.net/2501_90507065/article/details/147402717?spm=1001.2014.3001.5501        接下来我们将主要讨论一个继承体系中派生类的默认成员函数都有哪些特点

        1、构造函数

        构造函数用于初始化类中的成员变量,对于派生类的构造函数,在初始化列表部分会自动调用基类的默认构造函数,如果基类没有提供默认构造函数,我们需要主动调用它的构造函数,函数的调用类似于定义一个基类匿名对象,这是很合理的一种写法,显然,我们不可以在初始化列表部分初始化基类的成员,但是可以进行函数体内赋值,两者并不冲突

        2、拷贝构造函数

        派生类的拷贝构造函数必须调用基类的拷贝构造函数完成对基类成员的拷贝构造,对于拷贝构造函数传入的派生类对象引用,我们可以直接以类似定义基类匿名对象的形式将派生类对象的引用传入,这匹配了上文讲过的引用类型的切片赋值,与构造函数相同,上面的限制都是在初始化列表,对于函数体内赋值我们不做限制

        3、operator=赋值运算符重载

        派生类的operator=函数体中必须调用基类的operator=完成对于基类成员的赋值,需要注意的是,由于派生类与基类中的operator=函数名一致,这时候会造成函数名相同时派生类对于基类同名成员的隐藏,这时候我们需要指定类域,否则会在派生类的operator=中调用自己,形成死递归,最终造成程序崩溃

        4、析构函数

        由于继承体系的特殊性,派生类中一定会先定义基类部分,这就意味着初始化列表中一定会先初始化基类部分,再初始化其他部分,那么在析构函数中我们就必须保证先释放其它部分再释放基类部分,很明显这是确定的,所以编译器接管了这一部分任务,我们需要在析构函数中完成对于其它部分的释放,在析构函数的末尾,编译器会自动调用基类的析构函数完成对于基类的析构

        需要注意的是,由于多态部分的一些情况,析构函数需要构成重写,一个条件是函数名必须相同(以后会讨论到),所以编译器对这个部分做了特殊处理,析构函数名被统一处理成了destrutor,所以基类与派生类的析构函数构成了隐藏,与operator=类似,如果要在派生类的析构函数中调用基类的析构函数需要指定类域,但是这种情况一般不会出现,在这里只是做一个特殊说明

        5、注意

        还有两个默认成员函数分别是取地址运算符重载和const修饰的取地址运算符重载,但是这两个函数编译器自动生成的完全够用,所以在这里不做过多赘述

        了解了上面几个默认成员函数的相关定义以及它们分别的注意点,下面利用Person类与Student类的做演示:

        

//基类
class Person
{
public://构造函数Person(int x)//只是为了演示构造函数有传参的情况{}//拷贝构造函数Person(Person& rp){_name = rp._name;_code = rp._code;}//赋值运算符重载Person& operator=(Person& rp){_name = rp._name;_code = rp._code;return *this;}//析构函数~Person()//没有空间释放{}
protected:string _name = "zhao"; // 姓名int _code = 1010; // 身份证号
};//派生类
class Student : public Person
{
public://构造函数Student(int x):Person(x){}//其余成员给缺省值//拷贝构造函数Student(Student& rs):Person(rs)//调用基类的拷贝构造,引用类型进行切片赋值{_num = rs._num;}//赋值运算符重载Student& operator=(Student& rs){Person::operator=(rs);_num = rs._num;return *this;}~Student(){_num = 0;}
protected:int _num = 10; //学号
};

六、继承与友元

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

        所以对于如下情况,Display函数并不能访问stuNum

 class Student;class Person{public:friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名
};class Student : public Person{protected:int _stuNum; // 学号
};
void Display(const Person& p, const Student& s){cout << p._name << endl;cout << s._stuNum << endl;}void main(){Person p;Student s;Display(p, s);}

七、静态成员变量与友元

        基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例

        下面的代码及其运行结果可以说明这个问题:
        代码:
        

class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum; // 学号
};
class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};
void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_count << endl;Student::_count = 0;cout << " 人数 :" << Person::_count << endl;
}
int main()
{TestPerson();return 0;
}

        运行结果:

        

八、复杂的菱形继承及菱形虚拟继承

        1、引入

                (1).单继承

                一个子类只有一个父类的继承体系称为单继承,如下图所示:

                

                (2).多继承

                一个子类继承了多个父类的情况称为多继承,如下图所示:
                

                (3).菱形继承

                菱形继承是一种特殊的多继承,可以理解为一个多继承归根结底有一个(多个)
类被重复继承,最常见的情况如下图所示:
                

                很明显,菱形继承是存在一些问题的,就是:在Assistant类对象中,Person类的成员被保存了两份,这导致了Person类的数据早成冗余,同时Assistant的Person属性会出现分歧,这两个问题被称为菱形继承的数据冗余及数据二义性,下图是Assistant类对象模型,可以很明显的感受到菱形继承的两个问题:

                

        2、菱形继承产生问题的解决与虚拟继承

                (1).探究如何解决菱形继承产生的问题

                首先,对于二义性的问题是比较好解决的,我们可以对于两个Person类对象成员按域给值,可以暂时性的解决程序跑不通的问题,如下所示:
                

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 ; // 主修课程
};void Test (){// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;a._name = "peter";// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";a.Teacher::_name = "yyy";}
                (2).虚拟继承

                上面的方法只是暂时性的解决了菱形继承产生的问题,但是并没有从根本上解决这个问题,这时候我们引入了虚拟继承,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用,这是由于虚拟继承改变了类对象模型,同时需要编译器做大量处理,如下就是经过虚拟继承处理后的代码:

                

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 ; // 主修课程
};void Test (){Assistant a ;a._name = "peter";}

        3、虚拟继承为什么可以解决数据冗余和二义性的问题

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

class A{
public:int _a;};// class B : public Aclass B : virtual public A{public:int _b;};// class C : public Aclass 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对象模型(未进行虚拟继承处理的):
                

        下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下 面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A :

        

        在这里需要注意的是D类对象中的B、C都要分别去找属于它们自己的A成员,否则在平常的情况不会有问题,但是在跨类的切片赋值时会出问题

九、继承学习的总结与反思

        1、尽量不使用多继承,不使用菱形继承,菱形继承改变了类对象模型,性能上存在问题

        2、继承与组合

                (1).继承是is-a的关系,描述的是A是B

                (2).组合是has-a的关系,描述的是A拥有B

                (3).能使用组合不使用继承

                (4).继承是一种白箱复用,将基类细节暴露了出来,同时提高了类之间的耦合度

                (5).组合是一种黑箱复用,对象内部更多的是封装的,类之间耦合度低,组合类之间没有很强的关系

十、结语

        这就是本期关于继承的所有内容了,期待各位于晏、亦菲和我一起学习、进步!
            ·        

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

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

相关文章

Android设置界面层级为最上层实现

Android设置界面层级为最上层实现 文章目录 Android设置界面层级为最上层实现一、前言二、Android设置界面层级为最上层实现1、主要代码2、后遗症 三、其他1、Android设置界面层级为最上层小结2、悬浮框的主要代码悬浮框 注意事项&#xff08;1&#xff09;权限限制&#xff08…

Linux 了解篇

一、GNU 项目与 GPL 许可 &#xff08;一&#xff09;GNU 项目 GNU &#xff1a;GNU 是一个递归缩写&#xff0c;代表 "GNUs Not Unix"。GNU 项目旨在开发一个完全自由的操作系统&#xff0c;该操作系统基于 Unix 的设计理念但不包含 Unix 的代码。GNU 项目提供了大…

word 如何让公式居中,公式编号右对齐

问题&#xff1a; 如何让输入的公式居中&#xff0c;公式编号右对齐&#xff1f; 解决方法&#xff1a; 方法一&#xff1a;使用制表符 1、输入内容&#xff1a;先按一次“Tab”键&#xff08;制表符&#xff09;&#xff0c;然后键入公式&#xff0c;然后再按一次“Tab”键…

华为OD机试真题——最小循环子数组 (2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 B卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

OpenCv高阶(十七)——dlib库安装、dlib人脸检测

文章目录 前言一、dlib库简介二、dlib库安装1、本地安装&#xff08;离线&#xff09;2、线上安装 三、dlib人脸检测原理1、HOG 特征提取2、 SVM 分类器训练3、 滑动窗口搜索4、非极大值抑制&#xff08;NMS&#xff09; 四、dlib人脸检测代码1、导入OpenCV计算机视觉库和dlib机…

AD-PCB--AD20软件安装及中英文切换 DAY 2

1.软件安装 1.1 软件包下载 给你一个捷径&#xff1a; 1.2 安装过程&#xff08;安装过的人跳过就好&#xff0c;一般很多都支持懒人安装&#xff09; 双击其中的exe文件 点击下一步 选择中文 接受用户协议 下面这个弹窗有的没有。 建议勾选导入导出 安装目录&#xff0c…

单向循环链表与双向链表

单向循环链表的原理与应用 思考&#xff1a;对于单向链表而言&#xff0c;想要遍历链表&#xff0c;则必须从链表的首结点开始进行遍历&#xff0c;请问有没有更简单的方案实现链表中的数据的增删改查&#xff1f; 回答&#xff1a;是有的&#xff0c;可以使用单向循环的链表进…

Windows鼠标掉帧测试与修复

前言 这两天突然发现鼠标似乎有掉帧&#xff0c;但是掉的又不太明显&#xff0c;用着感觉似乎快速移动的时候会有一瞬间卡一下&#xff0c;但是眼睛又看不清楚&#xff0c;不太确定是不是自己的心理作用&#xff0c;非常难受。 如何判断鼠标是否掉帧 根据我的经验&#xff0…

U 盘数据恢复全攻略

目录 &#x1f4be; U盘数据误删怎么办&#xff1f;两款实用工具助你找回丢失文件&#xff01;1️⃣ Recover My Files&#xff1a;数据恢复的得力助手&#x1f4cc; 主要特点&#x1f6e0; 使用步骤详解1. 下载与安装2. 启动软件并选择恢复类型3. 选择U盘所在分区4. 选择文件恢…

HarmonyOS NEXT~鸿蒙系统运维:全面解析与最佳实践

HarmonyOS NEXT&#xff5e;鸿蒙系统运维&#xff1a;全面解析与最佳实践 摘要 本文深入探讨鸿蒙(HarmonyOS)系统的运维管理&#xff0c;从架构特点到日常维护操作&#xff0c;全面分析这一全场景分布式操作系统的运维要点。文章将介绍鸿蒙系统特有的分布式能力运维管理、性能…

基于 STM32 的智慧农业温室控制系统设计与实现

摘要 本文提出一种基于 STM32 微控制器的智慧农业温室控制系统设计方案,通过集成多类型环境传感器、执行机构及无线通信模块,实现对温室内温湿度、光照、土壤湿度等参数的实时监测与自动调控。文中详细阐述硬件选型、电路连接及软件实现流程,并附关键代码示例,为智慧农业领…

Appium+python自动化(五)- 模拟器

简介 Appium是做安卓自动化的一个比较流行的工具&#xff0c;对于想要学习该工具但是又局限于没 android 手机来说&#xff0c;可以通过安卓模拟器来解决该问题&#xff0c;下面就讲解使用appium连接安卓模拟器的操作步骤。而是由于手机数据线问题&#xff0c;也只好先用模拟器…

汽车充电桩专用ASCP210系列电气防火限流式保护器

1.概述汽车充电桩专用电气防火限流式保护器 电气防火限流式保护器可有效克服传统断路器、空气开关和监控设备存在的短路电流大、切断短路电流时间长、短路时产生的电弧火花大&#xff0c;以及使用寿命短等弊端&#xff0c;发生短路故障时&#xff0c;能以微秒级速度快速限制短…

Linux中磁盘分区与挂载

一、磁盘划分 1.1 了解磁盘 硬盘的接口类型 接口类型发展方向应用场景IDESATA I/II/III个人PC机SCSISAS服务器上 磁盘命名规则 OSIDE(并口)SATA(串口)SCSIRHEL5/dev/hda/dev/sda/dev/sdaRHEL6/dev/sda/dev/sda/dev/sdaRHEL7/dev/sda/dev/sda/dev/sda 1.2 磁盘划分 磁盘划…

【数据分析】什么是特征蒸馏?

引言 —— “ 在数据洪流中提炼真金——解密特征蒸馏的艺术。” 在数据爆炸的时代&#xff0c;我们每天产生的信息量已远超人类处理能力的极限。当企业拥有百万维的用户行为数据&#xff0c;医疗研究者面对TB级的基因测序记录&#xff0c;工程师试图从千万张图像中识别关键模式…

机器学习笔记【Week4】

一、 为什么要用神经网络&#xff1f; 逻辑回归只能处理线性可分问题。例如&#xff0c;经典的 XOR 异或问题无法用单层逻辑回归准确分类。神经网络通过多层结构和非线性激活函数&#xff0c;能学习复杂的决策边界&#xff0c;解决非线性问题。 二、神经网络的基本组成 神经网…

java交易所,多语言,外汇,黄金,区块链,dapp类型的,支持授权,划转,挖矿(源码下载)

目前这套主要是运营交易所类型的&#xff0c;授权的会贵点&#xff0c;编译后的是可以直接跑的&#xff0c;图片也修复了&#xff0c;后门也扫了 都是在跑的项目支持测&#xff0c;全开源 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/90887047 更多…

2025CCPC河北省赛题解

题目区分度不错&#xff0c;不过两题手快铜确实没想到。 Attention is all you need&#xff01; H - What is all you need? 签到题 #include <bits/stdc.h> #define x first #define y second #define int long long #define double long doubleusing namespace st…

【IOS】【OC】【应用内打印功能的实现】如何在APP内实现打印功能,连接本地打印机,把想要打印的界面打印成图片

【IOS】【OC】【应用内打印功能的实现】如何在APP内实现打印功能&#xff0c;连接本地打印机&#xff0c;打印想打印的界面 设备/引擎&#xff1a;Mac&#xff08;14.1.1&#xff09;/cocos 开发工具&#xff1a;Xcode 开发语言&#xff1a;OC/C 开发需求&#xff1a;工程中…

AWS WebRTC:获取信令服务节点和ICE服务节点

建立WebRTC的第一步是获取信令服务节点和ICE服务节点。 前提条件是有访问AWS的密钥&#xff0c;主要是ak&#xff0c;sk&#xff0c;token&#xff0c;我这边是业务云有接口可以返回这些信息&#xff0c;所以我直接从业务云获取。 先介绍一下什么是ak&#xff0c;sk&#xff…