【C++】面向对象三大特性之—— 继承 | 详解

目录

继承的概念

继承语法格式

继承方式

隐藏

继承下来的成员和父类是不是同一份

隐藏

基类和派生类对象赋值转换

继承中的作用域

派生类的默认成员函数

构造

拷贝构造

赋值重载

析构

继承与友元

继承与静态成员

菱形继承及菱形虚拟继承

多继承

菱形继承

菱形虚拟继承

结语


继承的概念

首先我们来谈谈什么是继承

比如有一天,你回到家了之后你爸爸突然对你说:哎呀最近看你学那么辛苦,实在是有些不忍心了

悄悄告诉你吧,我们家其实有一些你不知道的产业(滑稽

在现实生活中,继承就是家里的东西传下来给你了

在C++中,继承就是一个类的成员(成员变量与成员函数)都传给你了

举个例子:

我们现在有一个外卖系统,这里面的骑手、商家、用户都有着共同的信息,比如名字、地址等等

那我们如果每一个类都写一样的信息的话,是不是就有点冗余啊,浪费空间,那我们能不能想办法不劳而获一下呢?(滑稽

这时候就可以引出我们的继承了

我们可以写一个父类(也叫基类)来存储这些共同的信息,然后让下面的三个类全部继承这个父类即可

继承语法格式

class 子类: 继承方式  父类
{//内容
};

举个例子:

class Person
{
public:void Print(){cout << _num << endl;}
protected:int _num = 0;
};class Student : public Person
{
private:int _id = 3;
};

继承方式

继承方式分为公有、保护、私有继承,我们先来看一张表:

好,大家把这张表背一背哈(滑稽

开玩笑哒,我们只需要总结规律即可,这个总结了规律之后,以后可以直接现推关系

我们可以将三种继承方式抽象成一种大小关系

public   >    protected    >    private

而我们在继承的时候,只需要将两个进行比较,取小的那个即可

什么意思呢?

假设父类成员有一个公有继承,而我们的继承方式也是公有,所以在子类,这个成员就是公有的

如果这个成员在父类是保护,但我们继承方式是公有继承,公有 > 保护,小的那个就是保护,所以在子类里面这个成员就是保护

如果这个成员在父类里面是私有成员,但我们是公有继承,公有 > 私有,取小就是私有,所以这个成员在子类里面就是私有成员

同理,如果在父类是公有成员,但继承方式是保护,那么这个成员在子类里面就是保护

。。。。。。

隐藏

继承下来的成员和父类是不是同一份

在解决隐藏之前,我们先来看一个问题:继承下来的东西是同一份吗?

举个例子:

class Person
{
public:void Print(){cout << _num << endl;}int _num = 0;
};class Student : public Person
{
private:int _id = 3;
};int main()
{Student s;Person p;//继承下来的变量s._num += 1000;p._num += 500;cout << s._num << endl;cout << p._num << endl;cout << endl << endl;//继承下来的函数s.Print();p.Print();return 0;
}

这是一个子类继承了一个父类,然后我们在main函数里面分别定义了一个父类对象和子类对象

这时父类和子类是分开的两个对象,只不过子类对象继承了父类对象的成员

我们可以看到,我们对两个类的对象成员进行了不同的处理,结果也不同,证明继承下来的成员和父类并不是共用同一个

但是成员函数是同一个

这是因为我们的成员函数本身就不存在类中,而是存在一个公共的区域,就好比一个小区

我们可以建一个公共的篮球场,何必每家每户都建一个?

隐藏

我们在写继承的时候,可能会遇到这样一个问题:如果子类和父类成员重名了怎么办?会报错吗?

答案是不会报错,这会构成一个隐藏关系,如下:

class person
{
public:void print(){cout << _num << endl;}int _num = 0;
};class student : public person
{
public:int _id = 3;int _num = 3;
};int main()
{student s;cout << s._num << endl;return 0;
}

我们可以看到,这两个重名的成员可以同时存在,但如果我们要访问这个变量的话,我们就只能访问到子类中定义的那个变量

那如果我们非要访问父类中的呢?

那我们就只能借助域作用限定符——指定类域(指定了我就要访问父类的成员)

int main()
{student s;//  子类cout << s._num << endl;//  父类cout << s.person::_num << endl;return 0;
}

说完了成员变量,我们来说一说成员函数

class person
{
public:void print(){cout << _num << endl;}int _num = 0;
};class student : public person
{
public:void print(int i){cout << _id << endl;}int _id = 3;
};int main()
{student s;s.print(1);// √s.print();//  ×return 0;
}

因为隐藏了,所以我们在子类和父类中相同的函数是只会去找子类的那个并匹配的

如果我们传的参数或其他不符合子类的条件,也不会去匹配父类的函数,而是会直接报错

基类和派生类对象赋值转换

首先我们需要知道的一点是,我们的子类(派生类)是可以给父类(基类)赋值的

这是因为,我们子类中本来就有一部分是和父类相同的,我们继承了父类,然后我们还有属于自己这个类里面的信息

比如:骑手、商家、客户都有相同的信息,比如名字、地址、电话等等,这些都是相同的

但是像用户,有骑手是否接单、评价这些成员是独属于用户这个类的,所以相比于父类,用户这个子类又多出了一些他自己的东西

而编译器在赋值转换这里其实是走了一个特殊的,也就是为这个语法开了绿灯

规定,子类给父类赋值,会将子类身上父类的那一部分赋值过去

这时我们再想想:父类能不能给子类赋值?

答案是否定的,因为你想啊,子类有一些成员变量父类都是没有的,那父类如果能给子类赋值的话,那些值不就成随机值了,这不就是一个大坑嘛

继承中的作用域

首先我们来总结一下,目前我们所知道的一共有几个域:

  1. 局部域
  2. 全局域
  3. 类域
  4. 命名空间域

这四个都有一个共同的特点,就是都能影响查找规则,我要找一个变量,必须先去局部域找,然后再到全局域找,如果找不到就报错

这时如果我们将命名空间域展开或者指定命名空间域的话,我们就能够进入命名空间域中去查找

如果我们定义了一个类的话,那我们就能够通过这个类实例化出的对象来找类里面的公有函数

如果我就在这个类里面,那么这个类里面的变量我就都可以用

那么今天我们再来学习一下,继承中的作用域

首先,子类和父类是两块不同的空间域,如果子类里面有和父类一样的函数的话,就不会去父类的空间里面去找,这叫做隐藏,有些地方也叫做重定义

我们可以想象一下局部域和全局域,如果我们在局部域里面就有了一个变量的话,全局域也有一个变量,那我们是不是就只会拿局部域里面的变量啊,除非指定类域

并且!!!如果子类和父类有相同名字的函数话,不是重载!!!!!

要想清楚,子类和父类都不是一个类,而函数重载的要求就是,两个函数必须在同一个域里面,然后参数不一样函数名一样,这才叫函数重载

派生类的默认成员函数

这里的内容是,如果我们在继承了父类之后,可能有些情况下我们是需要显示写默认成员函数的

接下来我们会一个一个讲解:

  • 构造
  • 拷贝构造
  • 赋值重载
  • 析构

至于剩下两个取地址和const取地址,这俩直接使用编译器默认生成的即可,知道有这么个东西就行,不需要我们学习

我们这里先写好一个父类,到时候下面的类都继承的这个父类

class Person
{
public://      构造Person(const char* 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;}
protected:string _name; // 姓名
};

构造

首先,我们要知道的是,父类是一个类,那么类就会有构造,而且编译器为了防止我们乱搞,就规定了我们在子类构造函数这里,必须使用父类的构造函数

所以我们就是直接使用父类的构造,然后再一个一个初始化子类成员

子类的构造:(不显示写的情况下)

父类那一部分:调用父类的构造

内置类型:默认不做处理

自定义类型:编译器会调用其构造

假设我们子类写一个student,如下:

class Student : public Person
{
public:private:int _id;
};

这时我们的构造就是:

class Student : public Person
{
public:Student(const char* name = "Peter", int id = 0):Person(name),_id(id){}private:int _id;
};

拷贝构造

子类的拷贝构造:(不显示写的情况下)

父类那一部分:调用父类的拷贝构造

内置类型:默认不做处理

自定义类型:浅拷贝

但是,如果我们要显示写的话,那我们就需要一个一个处理了

首先,内置类型和自定义类型就和之前一样,直接拷贝即可(如果要深拷贝的话就开空间)

至于父类,还记得我们上面讲过的子类能赋值给父类

子类赋值父类就是将子类的父类那一部分直接赋值过去,这也叫切割

所以我们的拷贝构造可以直接传一个子类对象过来,父类那一部分就靠子类切割赋值来拷贝,剩下的就和之前一样

class Student : public Person
{
public://   构造Student(const char* name = "Peter", int id = 0):Person(name),_id(id){}//   拷贝构造Student(const Student& s):Person(s),_id(s._id){}private:int _id;
};

赋值重载

子类的赋值重载:(不显示写的情况下)

父类那一部分:浅拷贝

内置类型:默认不做处理

自定义类型:浅拷贝

这个赋值重载和上面的构造、拷贝构造大体逻辑一样,显示写的话都是以调用父类的赋值重载为主,剩下的就是老老实实拷贝了,就是正常类的拷贝了

我们先来看看这么写:

//   赋值重载
Student& operator=(Student& s)
{//  确保不是我赋值给我自己if (this != &s){operator=(s);_id = s._id;}return *this;
}

这么一看我们写的好像是对的,运行之后发现,编译器崩溃了

我们会看到这里显示栈溢出了

其实回想一下我们上面一直在说的隐藏,两个类的赋值重载函数是不是都叫做operator=啊

那么子类的就会将父类的隐藏,所以就会不断调用子类的这个赋值重载,所以,就栈溢出了

正确的解决方法应该是指定类域,我要访问的就是父类的operator=

如下:

//   赋值重载
Student& operator=(Student& s)
{//  确保不是我赋值给我自己if (this != &s){Person::operator=(s);_id = s._id;}return *this;
}

析构

析构这里就有一个大坑了,因为我们这里的析构是不能显示调用父类的析构的

我们开的函数会存到栈里面,栈要符合先进先出,也就是,我先创建的父类,再是子类,那我就应该先析构子类,再析构父类:

我们先说构造吧,为什么要先父后子:

如果我构造的时候,子类有一部分成员要靠父类的成员来初始化呢?那如果此时父类成员还没初始化,就是随机值,我们拿随机值初始化就完蛋了

再来说说为什么析构要先子后父:

如果我在析构完父类的成员之后,我子类还会调用父类成员变量或成员函数呢?

而且父类也没法调到子类的,因为是继承,子类继承父类

但是子类可以在父类析构之后调用父类啊,所以我们需要先析构子类,再析构父类

那为什么构造的时候要我们显示调用,析构的时候就不行呢?

因为构造的时候,一定能保证先父后子

想想,我们构造的时候,用的是初始化列表,初始化列表的顺序就是声明顺序

而编译器在此时默认规定了,我们在继承的时候,就相当于已经声明了

所以无论你顺序怎么换,先初始化的永远是父类

但是析构不一样,我们没办法保证一定会先子后父,所以C++就规定了,不用显示写父类的析构,编译器会在结束后自动调用,我们只需要析构子类的就行了

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

继承与友元

这个部分,只需要一句话就能解释清楚:友元不能继承

我们可以拿生活中的场景举例子:你爸有一个朋友,那那个叔叔是你的朋友吗?不是吧

所以我们父类的友元,并不能继承给子类

同样的,我们的子类的友元,父类同样不能使用(这个更离谱,子类继承的父类,父类连子类成员都用不了,更别提友元了)

/ 友元关系不能继承 父的友元子不能用  子友元不能用父的成员class Person
{friend class Show;
protected:int _num = 0;
};class Student : public Person
{
private:int _id = 3;
};class Show
{Show(Student& s, Person& p){cout << s._id << endl;cout << p._num << endl;}
};

继承与静态成员

这个地方也只有一个内容就是:静态成员是属于整个继承的,也就是说,都是同一个

举个例子:父类里面定义了一个静态成员变量,子类继承下去了,那么子类里面的那个继承自父类的静态成员变量,和父类的都是同一个,也就是说,修改了一个,另一个会跟着修改

     静态成员变量属于整个继承类class Person
{
public:static char x;
protected:int _num = 0;
};char Person::x = 'x';class Student : public Person
{
private:int _id = 3;
};int main()
{Student s;Person p;printf("%p\n", &Student::x);printf("%p\n", &Person::x);return 0;
}

我们这个代码写的是,取父类和子类中静态成员变量的地址,我们根据上图可以发现,地址相同,所以可以得知都是同一个

菱形继承及菱形虚拟继承

多继承

在了解菱形继承之前,我们需要了解一下,什么是多继承

我们可以举一个生活中的例子:助教,既可以是老师,也可以是学生对吧

就像是一些学校里的老教授会有一些研究生学员,这些学员有一部分会选择当新生的助教,会负责什么批改卷子之类的

那么这时,这个助教就有两个身份:

  1. 老师
  2. 学生

这时候我们就引出了多继承了,如下图:

多继承的格式如下:

其实就是在原本的继承后面加一个逗号,然后再在后面接继承的类名

菱形继承

而菱形继承是基于多继承的基础的更复杂的继承

还是举上面的例子,助教可以同时是学生和老师,而这两个身份,都是一种职业,所以我们可以举出这么一个例子:

职业里面分为老师和学生,而助教可以同时是学生和老师

这时就会有一个问题,职业这个类的信息,已经各自存进老师和学生这两个类里面了,而我们的助教这个类,就会同时存两份职业这个类里面的内容

这就会造成代码冗余和二义性!!!

试想一下,如果职业这个类里面有一个name成员,这时老师和学生都会各自存一份name成员

如果我们再继承给助教这个类的话,再去调用name,那我应该调用老师这个父类的name,还是学生这个父类的name

编译器不知道,所以就会报错,这就是二义性

class job
{
public:int _name = 100;
};class teacher : public job
{};class student : public job
{};class assistant : public student, public teacher
{};int main()
{assistant a;cout << a._name << endl;return 0;
}

因为编译器不知道是要调用哪一个类的name,所以就报错了

菱形虚拟继承

这时,祖师爷就想了这么一个办法,就是菱形虚拟继承

具体就是,在接收的类那里,在继承方式前面加上一个virtual

加上这个之后,编译器就会将继承的类的内容放到一个公共的区域,到时候这个类再被继承的时候,如果是菱形继承这样子的情况,就不会继承多份了

class job
{
public:int _name = 100; // 姓名
};
class Student : virtual public job
{
protected:int _num; //学号
};
class Teacher : virtual public job
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main()
{Assistant a;cout << a._name << endl;return 0;
}

结语

到这里,我们有关继承的相关内容就讲完啦~( ̄▽ ̄)~*

如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)

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

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

相关文章

探索Linux -- 冯诺依曼体系、初始操作系统、初始进程、fork函数

一、冯诺依曼体系结构 1、概念 冯诺依曼结构也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构。 最早的计算机器仅内含固定用途的程序。若想要改变此机器的程序&#xff0c;就必须更改线路、更改结构甚至重新设计此机器。当然最早的计…

三防平板满足多样化定制为工业领域打造硬件解决方案

在当今工业领域&#xff0c;数字化、智能化的发展趋势日益显著&#xff0c;对于高效、可靠且适应各种复杂环境的硬件设备需求不断增长。三防平板作为一种具有坚固耐用、防水防尘防摔特性的工业级设备&#xff0c;正以其出色的性能和多样化的定制能力&#xff0c;为不同行业的应…

8.7 Day15 匿名用户访问FTP与日志查看

查看配置文件 vsftpd是一个认证文件&#xff0c;意味着ftp是通过vsftpd这个认证文件来对我们输入的用户名和密码进行认证的&#xff0c;那么这个认证文件在哪里呢&#xff1f; 所在位置如下&#xff1a; 查看文件配置内容 默认通过系统来验证&#xff0c;但现在我们欲做一个类…

Flink-DataWorks第二部分:数据集成(第58天)

系列文章目录 数据集成 2.1 概述 2.1.1 离线&#xff08;批量&#xff09;同步简介 2.1.2 实时同步简介 2.1.3 全增量同步任务简介 2.2 支持的数据源及同步方案 2.3 创建和管理数据源 文章目录 系列文章目录前言2. 数据集成2.1 概述2.1.1 离线&#xff08;批量&#xff09;同步…

VulnHub靶场-VulnOS:2

1.环境准备 下载地址&#xff1a;VulnOS: 2 ~ VulnHub 前言&#xff1a;由于我们下载的靶场是vdi文件&#xff0c;而我使用的是虚拟机&#xff0c;我们需要安装VirtualBox将vdi文件转换成虚拟机的vmdk文件vdi转vmdk VirtualBox与VMware硬盘格式转换及使用方法-CSDN博客 虚拟…

【中等】 猿人学web第一届 第2题 js混淆 动态cookie 1

目录 调试干扰Hook Function 加密参数定位hook Cookie AST 解混淆字符串解密还原解密函数AST 配合解密函数还原字符串 ASCII 编码字符串还原字符串相加花指令(对象)剔除无用代码虚假 if剔除无引用代码剔除无引用的对象数值还原 switch 还原完整的 AST 代码代码注意 还原加密 请…

pygame小游戏

代码存在一些bug&#xff0c;感兴趣可自行修改&#xff0c;游戏运行后玩法与吃金币游戏类似。&#xff08;代码及结果比较粗糙&#xff0c;仅供参考&#xff09; 注&#xff1a;&#xff08;图片、音乐、音效文件老是上传上传不上&#xff0c;想要可私&#xff0c;也可以自己找…

如何在银河麒麟操作系统上搭建 Electron (含 Electron 打包指南)

本次教程所用版本 Eletron版本&#xff1a;31.3.1 Electron-packager版本&#xff1a;17.1.2 VScode版本&#xff1a;1.92.0 Node版本&#xff1a;18.19.0 npm版本&#xff1a;10.2.3 前言&#xff1a; 随着跨平台应用开发的需求日益增长&#xff0c;Electron 和 Qt 成为…

Midjourney入门-提示词基础撰写与公式

​ 前言 在前几篇教程里我们已经可以初步使用Midjourney进行出图了。 包括也了解了Midjourney的指令与参数。 但如果你想用Midjourney去生成各种各样高质量的图片&#xff0c; 并且生成的图片是你想要的画面内容&#xff0c;也就是更好控制生成图片的画面内容与风格&#xf…

书生大模型实战营闯关记录----第八关:书生大模型全链路开源开放体系

书生大模型全链路开源开放体系 一、概述 书生大模型&#xff0c;即InternLM系列模型&#xff0c;是由上海人工智能实验室书生团队开发的一系列大语言模型。这些模型以其强大的功能而著称&#xff0c;涵盖了从基础的语言理解到复杂的数学解题和图文创作等多个领域。 发展历程…

【每日面经】快手面经

ConcurrentHashMap和HashMap的区别&#xff1f;使用场景&#xff1f; 线程安全性 concurrentHashMap是线程安全的&#xff0c;HashMap不是线程安全的锁机制 ConcurrentHashMap采用的是分段锁&#xff08;Sagment&#xff09;机制&#xff0c;降低所得粒度提高了并发性能 Curr…

4章4节:临床数据科学中如何用R来进行缺失值的处理

在临床科研中,由于失访、无应答或记录不清等各种原因,经常会遇到数据缺失的问题。本文将深入探讨医学科研中数据缺失的成因、分类、影响以及应对方法,结合R语言的实际应用,为医学研究人员提供全面的解决方案。 一、认识缺失数据 其实,很多医学的纵向研究因获取数据资料时…

38.【C语言】指针(重难点)(C)

目录: 8.const 修饰指针 *修饰普通变量 *修饰指针变量 9.指针运算 *指针或-整数 *指针-指针 *指针关系运算 往期推荐 承接上篇37.【C语言】指针&#xff08;重难点&#xff09;&#xff08;B&#xff09; 8.const 修饰指针 const 全称 constant adj.不变的 *修饰普通变量 #…

java.lang.NoClassDefFoundError: ch/qos/logback/core/util/StatusPrinter2

1、问题 SpringBoot升级报错&#xff1a; Exception in thread "main" java.lang.NoClassDefFoundError: ch/qos/logback/core/util/StatusPrinter2 类找不到&#xff1a; Caused by: java.lang.ClassNotFoundException: ch.qos.logback.core.util.StatusPrinter22、…

HDFS写入数据的流程图

1.客户端向namenode发送请求&#xff0c;请示写入数据 2.namenode接受请求后&#xff0c;判断这个用户是否有写入权限&#xff0c;如果不具备直接报错&#xff1b;如果有写入权限&#xff0c;接着判断在要写入的目录下是否已经存在这个文件&#xff0c;如果存在&#xff0c;直…

PHP语言特性漏洞汇总【万字详解】

文章目录 任意文件下载PHP弱类型比较字符比较绕过代码示例过程 SHA1比较绕过MD5比较绕过SESSION比较绕过STRCMP比较绕过科学计算法绕过概念复现复现2 json_decode&#xff08;&#xff09;绕过概念源码分析 ereg绕过概念复现复现2 array_search强相等绕过概念复现 文件包含生成…

螺旋矩阵 | LeetCode-59 | LeetCode-54 | 分类讨论

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 &#x1f383;分类不好&#xff0c;这道题就做不出来&#xff01;&#x1f388; &#x1f4cc;LeetCode链接&#xff1a;59. 螺旋矩阵 II &#x1f4cc;LeetCode链接…

Mac平台M1PRO芯片MiniCPM-V-2.6网页部署跑通

Mac平台M1PRO芯片MiniCPM-V-2.6网页部署跑通 契机 ⚙ 2.6的小钢炮可以输入视频了&#xff0c;我必须拉到本地跑跑。主要解决2.6版本默认绑定flash_atten问题&#xff0c;pip install flash_attn也无法安装&#xff0c;因为强制依赖cuda。主要解决的就是这个问题&#xff0c;还…

移动端上拉分页加载更多(h5,小程序)

1.h5,使用原生方式监听页面滚动上拉分页加载更多 <template><div></div> </template><script> export default {data() {return {loadflag: true,maxpages: 0, //最大页码currentpage: 0, //当前页listData: [],config: {page: 1,pageSize: 15,…

Netty技术全解析:DelimiterBasedFrameDecoder类深度解析

❃博主首页 &#xff1a; 「码到三十五」 &#xff0c;同名公众号 :「码到三十五」&#xff0c;wx号 : 「liwu0213」 ☠博主专栏 &#xff1a; <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 &#xff1a…