新C++(6):继承那些事儿

news2024/11/16 10:29:43

"你在酒杯还未干的时间里,收藏这份情谊"

一、回顾继承

什么是继承?

继承是面向对象编程语言的三大特征之一。通过继承机制,面向对象的程序设计可以很大限度地对代码进行复用。

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

C++中继承定义格式/继承关系与访问限定符

二、切片对象vs临时对象

我们先来看看下面的代码。

int main()
{
    double i = 2.3;
    int j = i;
    cout << j << endl;
    return 0;
}

初始化一个int类型的变量j,但是我们用的不是同一类型的其他变量i。事实上,不同类型的变量是不能进行这样的操作的,因此,为了保证可行性,这里会发生“隐式类型转换”。不是将i的值赋值给j,而是在这个过程中产生一个临时变量 ,先将i的值给临时变量,再由临时变量赋值给j。

口说无凭,我们来看看下面的现象。临时变量具有常量性,因此不是不能用int&,而是需要将"权限"缩小 。

那么上面提到了,当不同类型进行赋值的时候,是会发生隐式类型转换的。那么我回到继承上来。这时我们想用父类对象引用子类对象。

int main()
{   
    Student s;
    Person p = s;
    Person& rp = s;
    return 0;
}

当父类对象引用子类时,并不需要+const修饰。也就意味着,编译器认为这种情况不同于上述发生隐式类型转换的条件。

"父类对象引用子类,不会发生隐式类型转换,也就不会生成临时变量。"

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

三、基类析构函数

我们先来回顾类里的6个重要的默认成员函数。

class Person
{
public:
    Person()
    {
        cout << "Person()" << endl;
    }

    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};

class Student :public Person
{
public:
    Student()
    {
        cout << "Student()" << endl;
    }

    ~Student()
    {
        cout << "~Student()" << endl;
    }
};

我们定义Student s,那么它不仅仅会去调用它自身的构造函数,还会去调用继承的父类对象部分的构造函数进行初始化。其次,当析构的时候,先调用自己的析构函数,完成清理本类的资源。

我们没有调用析构,为什么会自动调用父类的析构函数呢?我们能否手动调用父类的析构?

这两个函数析构在我们看来不是 不同吗?为什么需要指定类域?难道构成隐藏了吗?

这个知识牵涉到后面多态在析构函数的处理。

不管是Student的析构函数、还是Person中的析构函数。在编译器看来,最后都会被特殊处理成destructor函数名。

什么时候调用父类的析构函数?

在本段的开头,我们就发现,当定义一个派生类时,即便我们在该类的析构函数中没有显式调用其基类的析构函数,但是最后打印出来却是调用了基类的析构函数。

当子类调用析构后,会自动调用父类的析构函数。

四、友元关系不能继承

也就是说基类友元不能访问子类私有和保护成员。

五、解引用/静态成员变量

在开讲本段之前,我们先来回顾类成员你的存储方式。

(1)类对象的存储方式

类体中的成员分为两类,类变量与类方法(函数)。

上述结果清晰地告知我们,一个类的大小,取决于成员变量的大小。

而类里的方法(函数),是被放在一个公共的代码段。

(3)静态成员变量

我们来看看下面的代码段;

class Person
{
public:
    Person() { }

    void Print()
    {
        cout << this << endl;
    }
public:
    static int _count; // 统计人的个数。
    string _name;
};
int Person::_count = 0;

class Student : public Person
{
protected:
    int _stuNum; // 学号
};

此时基类中有一个静态变量count,那么student继承下去后,会生成一份新的count吗?

答案是不会!静态成员变量在基类,在派生类中也是同一个。"静态变量属于整个类"。

(3)nullptr解引用

    Person* ptr = nullptr;
    ptr->Print();
    cout << ptr->_name << endl;

看完上面的一份代码,是否觉得都会奔溃?

    (*ptr).Print();
    cout << (*ptr)._name << endl;

要理解这个点和类成员的存储方式十分密切。解引用的本质是,访问地址处的类型大小的字节。当我们用ptr->Print()是在解引用吗?当然不是!因为并非是在访问类成员变量,而是直接访问的是类成员方法,这些方法早不在类的大小里!

这也就是为什么ptr->name \ (*ptr).name 才会访问出错,因为name是类成员的变量!此时解引用是对空指针的解引用。

当我们再在print中打印_name时,此时this就是空指针(ptr->name),所以也就 成了对空指针的解引用。我们用ptr->Print()调用时,只传了一个值给Print()函数,那就是this(nullptr)。

六、菱形继承

(1)多继承

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

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

(2)二义性与数据冗余

C++设计继承有一个很大的坑,就是支持了菱形继承。那么什么是菱形继承呢?我们来看看这模型。

这是一种特殊的多继承情况。

也许仅凭图中的模型,不会让你对这菱形继承望而生畏或者攥紧拳头,我们简单地设计一套继承体系。

class Person
{
public:
    string _name; // 姓名
    // id 家庭住址 身份证号码
};

class Student:public Person
{
protected:
    int _num; //学号
};

class Teacher:public Person
{
protected:
    int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse; // 主修课程
};

这是一个人,但是他有"学生+老师"的双重身份。

我们定义对象Assistant,此时我们想要给这个对象的内部成员name初始化。但是无法直接进行初始化!因为继承的缘故,_name不仅仅在Student有一份,Teacher中也有一份,从而导致了这样的"二义性"问题!因此,我们不得不指名类域,初始化我们想要的值。

但是,一个人是否仅仅需要一个名字就可以了(在当前这个条件下)?当他是学生的时候有一个名字,当他是老师的名字又有另外一个名字?显然这是很不符合常理的。也许你会说,哎呀一个名字就几个字节大小罢了,多了的那份变量似乎显得不过尔尔。但是如果你发现的这份变量是一个100字节、1000字节甚至更多呢?本来我们仅仅需要一份代码不过100byte,却因为菱形继承,足足增加到了200byte大小却是一模一样的类型。 这种 情况,也叫做"数据冗余"。

(3)虚继承

当我们翻看C++的发展史,不难发现,当支持多继承后,肯定会出现菱形继承这样的不好的场景。为此,后一个版本C++又为解决多继承产生的二义性问题 增添了一个关键字virtual。

我们先来看看没有virtual时的菱形继承;

从内存角度来看,很清楚地看到d继承的各个类变量的分布情况。此时,有两个地址指向同一份int a基类。

virual继承

class Base
{}

//virtual加在腰部类上
class Derived1:virtual public Base
{}

class Derived2:virtual public Base
{}

class Example:public Derived1,public Derived2
{}

当加上virtual虚继承后,本来两份的a变成了一份。最先被初始化为4的a,后来被覆盖成了5。但是,我们却在d对象里发现两份像地址一样的数字。

我们找到地址处,得到它们的内容,其实记录的是从该位置到变量a的偏移量。

同样,当变为虚继承时,存储的方式也会发生变化,我们来看看子类B。

这里是通过了B和C的两个指针,指向的一张表。 这两个指针叫虚基表指针 ,这两个表叫虚基表。虚 基表中存的偏移量 。通过偏移量可以找到下面的A

总结:

①父类对象引用子类 不产生临时变量。不是发送隐式类型转换。

②当子类析构调用完成后,会自动调用父类的析构函数。

③友元关系不能继承

④类的静态成员属于整个类,类对象指针解引用并不全都是"解引用"。

⑤菱形继承是不好的,如果实在遇到菱形继承,为避免代码冗余和二义性,应当使用virtual虚继承。

本篇到此结束,感谢你的阅读

祝你好运,向阳而生~

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

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

相关文章

scrapy_redis概念作用和流程

scrapy_redis概念作用和流程 学习目标 了解 分布式的概念及特点了解 scarpy_redis的概念了解 scrapy_redis的作用了解 scrapy_redis的工作流程 在前面scrapy框架中我们已经能够使用框架实现爬虫爬取网站数据,如果当前网站的数据比较庞大, 我们就需要使用分布式来更快的爬取数…

制药行业中各种化合物的净化,大孔吸附净化氨基酸

基于吸附功能的聚苯乙烯特种树脂 Tulsimer ADS-600 是一款没有离子官能基的&#xff0c;由交联聚苯乙烯合成的功能强大的吸附型树脂。 Tulsimer ADS-600 主要应用于水溶液中吸附酚及其化合物&#xff0c;氯代烃等含氯物质&#xff0c;表面活性剂&#xff0c;氨基酸&#…

【C++修炼之路】C++入门(中)—— 函数重载和引用

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录一、前言二、函数重载1、重载规则2、函数名修饰规则三、引用1、区分2、本质3、特性4、应用a、做参数b、做返回值5、效率比较6、常…

windows安装cnpm

文章目录1 cnpm简介2 cnpm 安装步骤1 cnpm简介 npm下载速度比较慢&#xff0c;可以通过cnpm下载node包 2 cnpm 安装步骤 找到nodejs的安装路径&#xff1a; 使用nvm安装和管理node 直接安装node的方式 直接通过安装包安装node的方式&#xff0c;node会被安装在某个实际目录下…

docker pull nginx

取最新版的 Nginx 镜像 docker pull nginx:latest 查看本地镜像 使用以下命令来查看是否已安装了 nginx&#xff1a; $ docker images 运行容器 安装完成后&#xff0c;我们可以使用以下命令来运行 nginx 容器&#xff1a; $ docker run --name nginx-door -p 8080:80 -…

VS code的使用指南

VS code的使用指南 VS code的安装与下载 VS Code的安装地址的 在下面选择版本的位置进行安装&#xff08;一般选择Stable进行安装&#xff0c;这个软件是跨系统的安装与设置相关的内容信息&#xff09;。 插件安装 直接点击这个位置,选择自己需要的相应的插件 这些都是常用的…

软件工程(四)——结构化设计、模块独立性、面向对象设计、软件测试与维护

目录 一、界面设计 二、结构化设计 1.概要设计 2模块独立性 三、面向对象设计 1.面向对象设计的五大基本原则(SOLID)和其他5个原则 2.设计模式 三、软件测试与维护 1.白盒测试和黑盒测试 2.测试的阶段 3.软件维护阶段 四、系统演化策略 一、界面设计 人机界面设计&…

50条必背JAVA知识点(一)

1.编写&#xff1a;编写的Java代码保存在以“.java”结尾的源文件中。 2.编译&#xff1a;使用javac.exe命令编译java源文件&#xff0c;生成字节码文件。格式&#xff1a;javac 源文件名.java 3.运行&#xff1a;使用java.exe命令解释运行字节码文件。格式&#xff1a;java …

深入浅出PyTorch-PyTorch的主要组成模块

目录1.基本配置2.数据读入3.模型构建3.1神经网络的构造3.2神经网络中常见的层常见网络层的构造常见的网络层3.3模型示例卷积神经网络&#xff08;LeNet&#xff09;深度卷积神经网络&#xff08;AlexNet&#xff09;4.模型初始化5.损失函数6.训练和评估深度学习和机器学习在流程…

python小游戏——塔防小游戏代码开源

♥️作者&#xff1a;小刘在这里 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放&#xff0c;愿所有的美好&#…

TCP/IP网络编程——优雅的断开套接字的连接

完整版文章请参考&#xff1a; TCP/IP网络编程完整版文章 文章目录第 7 章 优雅的断开套接字的连接7.1 基于 TCP 的半关闭7.1.1 单方面断开连接带来的问题7.1.2 套接字和流&#xff08;Stream&#xff09;7.1.3 针对优雅断开的 shutdown 函数7.1.4 为何要半关闭7.1.5 基于半关闭…

AST入门与反混淆初体验

文章目录1.什么是AST&#xff1f;2. AST反混淆的目的3. babel库安装4. 直观的理解AST5.如何用AST解混淆&#xff1f;思路是什么&#xff1f;6. babel库的学习7. AST反混淆初体验-常量折叠1.什么是AST&#xff1f; ​ 在计算机科学中&#xff0c;抽象语法树&#xff08;Abstrac…

【服务器数据恢复】raid5中3块磁盘先后掉线的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某单位同友存储设备&#xff0c;该存储有大于5台的虚拟机&#xff0c;其中有3台linux系统虚拟机存储重要数据。存储设备中组建的raid5由于未知原因崩溃导致存储无法启动。 存储结构&#xff1a; 服务器数据恢复过程&#xff1a; 1、对…

C语言进阶——字符函数和字符串函数(上)

目录 一、前言 二、正文 1.求字符串长度 ♥strlen 2.长度不受限制的字符串函数 ♥strcpy ♥strcat ♥strcmp 三、结语 一、前言 一日不见&#xff0c;如隔三秋&#xff1b;几日不见&#xff0c;甚是想念。猜想小伙伴们在平常进行有关字符的练习时遇到有关字符的操作却无从下手…

GEC6818 移植 rtl8723bu wifi驱动

文章目录1. 配置内核2、RTL8723BU 模块驱动编译2.1 下载解压2.2 配置编译3. openssl 移植3.1 下载解压3.2 配置3.3 编译安装4. libnl 移植4.1 下载解压4.2 进入源码目录并配置4.3 编译安装5. wpa_supplicant 移植5.1 解压源码5.2 配置5.3 make编译6. 启动wifi网卡6.1 配置WiFi连…

Python调用Go语言编译的动态链接库(CGO)【待续】

CGO C 语言作为一个通用语言&#xff0c;很多库会选择提供一个 C 兼容的 API&#xff0c;然后用其他不同的编程语言实现。Go 语言通过自带的一个叫 CGO 的工具来支持 C 语言函数调用&#xff0c;同时我们可以用 Go 语言导出 C 动态库接口给其它语言使用。 hello world程序 p…

【定时同步系列11】各种开环定时同步算法与MCRB性能对比的MATLAB仿真

重要声明:为防止爬虫和盗版贩卖,文章中的核心代码和数据集可凭【CSDN订阅截图或公z号付费截图】私信免费领取,一律不认其他渠道付费截图! 引言 开环定时同步包括内插控制、定时误差估计等环节,其中内插控制在之前的博客里有详细的描述,请翻阅之前的博客! 【定时同步系…

Python编程技巧分享:6 个必知必会高效 Python

编写更好的Python 代码需要遵循Python 社区制定的最佳实践和指南。遵守这些标准可以使您的代码更具可读性、可维护性和效率。 本文将展示一些技巧&#xff0c;帮助您编写更好的 Python 代码 遵循 PEP 8 风格指南 PEP 8 是 Python 代码的官方风格指南。它提供了一组用于格式化…

Maven parent多项目打包找不到reversion变量问题

项目结构&#xff1a;packagetest&#xff1a;顶级父级c1&#xff1a;子项目&#xff08;web项目&#xff09;c2&#xff1a;子项目(jar包)c1依赖c2的jar包。在父级maven中deploy成功&#xff0c;package也成功&#xff0c;私服上有都有包了。但是在c1上package的时候&#xff…

二十、操纵管道

本章将讨论如何在应用程序中使用多种方式操作管道。本章的部分内容都是很底层的&#xff0c;所以在开始阅读之前&#xff0c;请确保你需要一些编程知识&#xff0c;并对GStreamer有很好的理解。 这里将讨论的主题包括如何从应用程序向管道中插入数据&#xff0c;如何从管道中读…