C++ 继承 文字+图片+代码 超详细解刨

news2024/11/23 15:41:58

什么是继承?

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

3437c5f7cf254eefa6dc75be82cd8ac6.png

我们称被继承的类为基类(父类),称继承的类叫做派生类(子类)。

 

继承

 

继承方式

继承有三种方式,分为public继承,protected继承,private继承,每种继承都有不同之处,我们用一张图来很好的总结它们的区别

1f24d85be919497eb55f0533333e4668.png

public继承

public继承是我们最常用的继承方式,而大部分情况下我们也只会用到这种继承方式。

class Person {                     //基类(父类)

public:
	string _name = "张三";
protected:
	string _tel;
private:
	string _sex;
};


class Student : public Person{    // 派生类(子类)
};

public继承对于基类的public成员继承给派生类仍然是public成员,

对于基类的protected成员继承给派生类仍然是protected成员,

但是对于基类的private成员,继承给是派生类是不可见状态,你无法在派生类访问到基类的private成员,就像是在类外一样。

继承对于基类的private成员是特殊的,无论是什么继承,基类的private成员在派生类中都是不可见的!

5a10fb834d8c4fe5823c62078ed9d86c.png

 

 protected继承

class Person {                     //基类(父类)

public:
	string _name = "张三";
protected:
	string _tel;
private:
	string _sex;
};


class Student : protected Person{    // 派生类(子类)
};

protected继承对于基类的public成员继承给派生类会变为protected成员,

对于基类的protected成员继承给派生类仍然是protected成员,

对于基类的private成员,继承给是派生类是不可见状态。

 1a24526e5bd84b43a7147071da4f9f43.png

 

private继承

class Person {                     //基类(父类)

public:
	string _name = "张三";
protected:
	string _tel;
private:
	string _sex;
};


class Student : private Person{    // 派生类(子类)
};

了解了上面两种继承,是否掌握了规律?

private继承对于基类的public成员继承给派生类会变为private成员,

对于基类的protected成员继承给派生类会变为private成员,

对于基类的private成员,继承给是派生类是不可见状态。

24751a4879d74e4c87b4a157b4a95e93.png

 

 默认继承

class和struct有各自的默认继承方式,既然是默认继承,那么就说明可以不指定继承方式

class Person {                     //基类(父类)
public:
	string _name = "未知";
};

class Student : Person {    // class的默认继承方式是private继承
protected:
	int _id;
};

class Teacher : Person {    // struct的默认继承方式是public
protected:
	int _workid;
};

即使有默认继承方式,我们也推荐显式写上它的继承方式。

 

总结

1f24d85be919497eb55f0533333e4668.png

1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。


2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。


3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected
> private。


4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。


5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

 

基类的隐藏

当基类和派生类存在同名成员变量或者同名成员函数会发生什么?

会使得基类的同名成员构成隐藏,使得基类的同名成员无法直接访问,需要指定基类的类域才能访问!

class Person {                     //基类(父类)

public:
	void func()
	{
		cout << "Person" << endl;
	}
	string _name = "未知";
};


class Student : private Person {    // 派生类(子类)
public:
	void func()
	{
		cout << "Student" << endl;
	}
	string _name = "张三";
};

如果我们不指定类域直接访问

9cd2155f0db944cead27d97d61681a4a.png

会发现都是访问的派生类的同名成员。

 

那么有没有办法访问到基类的同名成员呢?  指定类域!

5a862894ac1b4c08a44137bdabf74402.png

  从这里也可以发现,派生类是会储存基类的数据的。

 

派生类对基类的赋值转换

派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。 而我们称这种现象叫做“切片”,为什么叫切片呢?   我们看下面这张图
 

5b5e1e23278a4f4cbf1ff64f7f23d3ca.png

 这种赋值对于基类来讲,就相当于派生类把自己特有的那一部分切掉变成了基类。

而这种赋值,并不是我们之前遇到的赋值,会发生隐式类型转换。

这种赋值更适合叫它为一种特殊的转换。

 

我们也可以借着切片来了解派生类的结构

bb7b719400b94eaeb410884362c8d648.png

 我们调用内存监控,就可以发现我们这里的被转化的基类Person ps1在内存中只占了4个字节,相对于派生类的8个字节被切掉了派生类本身自有的4个字节。

 

而对于基类指针和基类引用的转换

c907dcdc0e5543bfb14db1565c46c4fa.png

 可以发现 基类指针 和 基类引用  的地址与派生类的地址是一致的。说明它们会访问同一块空间,只不过Person* 和 Person& 访问的空间有限,只能访问自己(基类)的成员。

 

那么,基类对象能否转化为派生类对象呢?

答案是不能的!  不过基类指针可以通过强制类型转换 变为派生类指针 。(这会存在隐患,会有非法访问的问题)

 8223ac90f7e849f094b1850cef1ae0b2.png

 

 

派生类的默认成员函数

继承的派生类的默认成员函数相对于没有继承的类的默认成员函数是有不同的,为什么呢?

首先要明白派生类的地址结构

15cdac0a66c44540a7d5dd56beedfee4.png

 既然一个派生类要基类和派生类的成员,那么对于基类的成员,派生类怎么操作呢?

 

默认构造函数

class Person {                     //基类(父类)

public:
	Person(string name)
		:_name(name)
	{}

public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	Student(string name,int id = 2023, int age = 18)
		:Person(name)       //是通过这样的格式来给基类成员初始化的
		,_id(id)
		, _age(age)
	{}
	void Print()
	{
		cout << "姓名:" << _name << "   学号:" << _id << "   年龄:" << _age << endl;
	}
//protected:
public:
	int _id;
	int _age;
};


int main()
{
	Student st("张三");
	st.Print();
	return 0;
}

通过构造函数的初始化列表来给基类成员进行初始化!

注意:我们这里模拟的是编译器默认生成的默认构造函数。对于派生类的构造函数,我们可以调用基类的构造函数来完成对基类成员的初始化!

 

拷贝构造函数

class Person {                     //基类(父类)

public:
	Person(const Person& st)
		:_name(st._name)
	{}
public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	Student(const Student& st)
		:Person(st)         //这里就用了基类的赋值转换知识
		,_id(st._id)
		,_age(st._age)
	{}

	void Print()
	{
		cout << "姓名:" << _name << "  学号:" << _id << "  年龄:" << _age << endl;
	}
//protected:
public:
	int _id;
	int _age;
};

注意:我们这里模拟的是编译器默认生成的默认拷贝构造函数。对于派生类的拷贝构造函数,我们可以调用基类的拷贝构造函数来完成对基类成员的初始化!

 

赋值重载函数

class Person {                     //基类(父类)

public:
	Person& operator=(const Person& ps)
	{
		if (this != &ps)
		{
			_name = ps._name;
		}
		return *this;
	}
public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	Student& operator=(const Student& st)
	{
		if (this != &st)
		{
			Person::operator=(st);
			_id = st._id;
			_age = st._age;
		}
		return *this;
	}
	void Print()
	{
		cout << "姓名:" << _name << "   学号:" << _id << "   年龄:" << _age << endl;
	}
//protected:
public:
	int _id;
	int _age;
};

这里的赋值重载就与 刚刚写的构造函数和拷贝构造函数不一样了,他需要在派生类中指定基类的类域来访问基类的赋值重载函数,并且这里也运用了切片的知识!

efaa31557f13485d9de587174772851b.png

 

析构函数

class Person {                     //基类(父类)

public:
	~Person()
	{}

public:
	string _name = "未知";
};


class Student : public Person {    // 派生类(子类)
public:
	~Student()
	{
		//Person::~Person();   //这里我们需要手动去调基类的析构函数吗?
                       //-> 不需要,因为派生类的析构函数结束时会自动调用基类的析构函数!
	}

//protected:
public:
	int _id;
	int _age;
};

派生类的析构函数需要特别注意,在派生类析构函数结束的时候,会自动去调用基类的析构函数,所以我们就不需要手动去调用基类的析构函数,不然的话会调用两次析构。

而如果你想手动去调用 则跟赋值重载一样指定类域即可。(一般不会这么做)

 

注意:调用析构的话,析构顺序是先析构派生类的成员,然后再析构基类的成员。

 

继承与友元

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

class Person {                     //基类(父类)

public:
	friend void func();
protected:
	string _name;
};


class Student : public Person {    // 派生类(子类)
public:
	void Print()
	{
		cout << "姓名:" << _name << "   学号:" << _id << "   年龄:" << _age << endl;
	}
protected:
	int _id;
	int _age;
};

void func()
{
	Person ps;
	ps._name = "张三";      //因为是友元,所以可以直接进行访问
	cout << ps._name << endl;

	Student st;
	st._id = 2023;           //友元关系无法被继承
	cout << st._id << endl;

}

 

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
 

class Person {                     //基类(父类)

public:
	string _name;
	static int _count;
};

int Person::_count = 0;

class Student : public Person {    // 派生类(子类)
public:
protected:
	int _id;
	int _age;
};

13149796400f4e8781446eb8aa0ac21d.png

 继承体系中的静态成员是共享的。

 

多继承

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

class Animal {
public:
	string _species = "human";
};

class Person {                     //基类(父类)

public:
	string _name;
};

class Student : public Person ,public Animal{    // 派生类(子类)
public:
protected:
	int _id;
	int _age;
};

 

 

菱形继承

菱形继承是基于多继承的一种复杂且特殊的情况

8b6c14d2310a43808dae1869bc36b664.png

 菱形继承就会出现这样一个问题,继承的B类和C类同时拥有A类的结构,如果B、C再多继承给D类,就会出现D类拥有两个A类的结构,这会导致什么问题?   二义性和数据冗余

 

class A {
public:
	int _num;
};


class B : public A{
public:
	int _Bid;
};

class C : public A {
public:
	int _Cid;
};

class D : public C, public B {
public:

};

二义性问题

d711c02679be4321932fbc445a25abc3.png

 如果不指明类域,就会有二义性问题,所以指定类域可以解决这种问题

2069f821127c4e43ae221b0a47f027d6.png

 从这张图可以看出来,C空间的数据是先存储的,其次是B空间的数据,最后才是D空间的数据,而导致这个顺序的原因就是我们是先继承的C,再继承的B。 

31af7031f135496ba7bba7f248880607.png

 

虽然说指定类域可以解决二义性的问题,但是实际我们真的需要两个A类的数据吗?

一般是不需要的!  既然如此,这是不是就属于浪费了空间!

为了解决这个问题,C++专门有一个关键字来处理这个问题————virtual(虚拟继承)

 

虚拟继承(virtual)

使用方法: 在菱形继承体系的腰部加入virtual关键字

class A {
public:
	int _num;
};


class B : virtual public A{
public:
	int _Bid;
};

class C : virtual public A{
public:
	int _Cid;
};

class D : public C, public B {
public:
	int _Did;
};

如果使用了虚拟进程,那么D的数据结构就会发生变化 

b101455aecf54e39861c60977119831f.png

先来看看C空间的数据,本该存放它的_num的区域存放了一个像是指针的数据。(其实就是一个指针,等会讲这个指针存放了什么)

B空间的数据与C一样,本该存放它的_num的区域也存放了一个像是指针的数据。

再看_num的数据竟然是存放在最后面

如此看来,虚拟继承的数据结构就与正常继承的数据结构有了很大的区别。

 

首先就引出一个问题 ,这个时候如果我们指定了类域去访问_num,它怎么去找到存放真正的_num的地址?现在B,C空间本该存放_num的地址存放了一个奇怪的指针。

我们这就探讨这个指针到底存放了什么东西

b45cd0f49b4a4ff389c33bf11a8a911d.png

 这时候发现了什么,这个指针竟然存放着与存放着真正的_num地址的偏移量!

2e3e71621f724ddda12204a2b750a19b.png

 冗余问题

我们看上面的例子,虚拟继承貌似并没有解决代码冗余的问题,它不是仍然存在着数据的浪费吗?

不仅浪费了,甚至还多开了一部分空间来存放数据。

其实不然,这里举例是A只有了一个int,但是如果不仅仅只是一个int数据,或者是一个数组,那么就很好的节约了空间。

class A {
public:
	int _num;
	int _num1;
	int _num2;
	int arr[10];
};


class B : virtual public A {
public:
	int _Bid;
};

class C : virtual public A {
public:
	int _Cid;
};

class D : public C, public B {
public:
	int _Did;
};

fa914e266daf4a2da92226798691871f.png

 这种情况就节约了一大部分空间

 

 

 

继承的总结

C++的继承是比较复杂的,因为它有多继承,从而衍生出了十分复杂的菱形继承,所以可以理解多继承就是C++的一个缺陷。

而相对于java的继承来讲,java并没有那么多种继承方式,java只有一种public继承,并且也没有多继承的概念,也就更没有了菱形继承。

但是,C++作为走在最前沿的语言,是肯定要踩一些坑的,这些无可厚非。

 

 

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

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

相关文章

二十分钟入门计算机视觉开源神器——课堂笔记

1&#xff0c;统一的深度学习框架&#xff0c;2.0 2&#xff0c;现状 3&#xff0c;代表算法库 &#xff08;1&#xff09;目标检测MMDetection 任务支持&#xff1a;目标家呢&#xff0c;实力分割&#xff0c;全景分割 覆盖广泛 算法丰富 使用方便 &#xff08;2&#xff0…

2023年Q1美团财报解读:拨开云雾 始见月明

原文出处&#xff1a;走马财经 5月底&#xff0c;随着京东、阿里巴巴、腾讯、快手、拼多多、美团等相继发布财报&#xff0c;中国互联网主流大公司的财报发布季结束。 一方面他们体量够大&#xff0c;另一方面他们要么深耕零售&#xff0c;要么与零售、消费息息相关&#xff…

复原IP地址-回溯

1题目 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址&#xff0c;但是 "0.01…

17-Vue3中其它的 Composition API

目录 1、shallowReactive 与 shallowRef2、readonly 与 shallowReadonly3、toRaw 与 markRaw4、customRef5、provide 与 inject6、响应式数据的判断7、Composition API 的优势7.1 Options API 存在的问题7.2 Composition API 的优势 1、shallowReactive 与 shallowRef shallowR…

华为OD机试真题 Java 实现【快速开租建站】【2023Q1 200分】,附详细解题思路

一、题目描述 当前IT部门支撑了子公司颗粒化业务&#xff0c;该部门需要实现为子公司快速开租建站的能力&#xff0c;建站是指在一个全新的环境部署一套IT服务。 每个站点开站会由一系列部署任务项构成&#xff0c;每个任务项部署完成时间都是固定和相等的&#xff0c;设为1。…

细看SLMi823x系列SLMi8233BD双通道隔离驱动在 OBC 上的典型应用

数明深力科SLMi823x系列SLMi8233BD隔离驱动技术优势&#xff1a;具有高性能、高性价比、高可靠性的产品特性&#xff0c;应用覆盖UPS、充电桩、服务器电源、通信电源、新能源汽车动力总成系统的车载 OBC 领域。通过CQC认证的。 车载充电器&#xff08;OBC&#xff09;是电动汽…

如何研究带有不可微项的目标函数的局部极小值?

以optimtool的算法为例来解释 在Python >3.7的编程环境下&#xff0c;按如下方式下载optimtool&#xff0c;一个基于符号微分与数值近似的优化方法库&#xff1a; pip install optimtool --upgrade pip install optimtool>2.4.2目前没有为目标函数中不可微项增加预处理…

golang http请求封装

http请求封装在项目中非常普遍&#xff0c;下面笔者封装了http post请求传json、form 和get请求&#xff0c;以备将来使用 1、POST请求 1.1、POST请求发送 json 这里发送json笔者使用了2种方式&#xff0c;一种是golang 自带的 http.Post方法&#xff0c;另一是 http.NewReq…

iphone苹果手机如何备份整个手机数据?

手机上的数据变得越来越重要&#xff0c;大家也越来越注重数据安全。如果手机设备丢失的话&#xff0c;不仅是设备的丢失&#xff0c;还是数据的丢失。因此&#xff0c;备份数据就显得很重要。那么&#xff0c;iphone如何备份整个手机&#xff0c;苹果怎么查备份的照片&#xf…

14.3:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中字典序最小的结果

给定一个由字符串组成的数组strs&#xff0c;必须把所有的字符串拼接起来&#xff0c;返回所有可能的拼接结果中字典序最小的结果 贪心写法 首先注意的一点是&#xff1a;如果两个字符串的长度相同&#xff0c;“abc”&#xff0c;“abd”&#xff0c;肯定是“abc”的字典序最…

15.2:分金条的最小代价

一块金条切成两半&#xff0c;是需要花费和长度数值一样的铜板 比如长度为20的金条&#xff0c;不管怎么切都要花费20个铜板&#xff0c;一群人想整分整块金条&#xff0c;怎么分最省铜板? 例如&#xff0c;给定数组{10,20,30}&#xff0c;代表一共三个人&#xff0c;整块金条…

情绪管理ABC法

情绪管理ABC法 是由著名心理学家艾利斯&#xff08;Albert Ellis&#xff09;提出的一种情绪管理方法。 模型介绍 情绪&#xff0c;不取决于发生的事实&#xff0c;取决于我们如何看待这件事ABC理论认为&#xff0c;我们的情绪©&#xff0c;其实与发生的事件(A)无关&…

亚马逊云科技赋能敦煌网集团上云,云上新架构带来价值

敦煌网成立于2004年&#xff0c;是领先的B2B跨境电子商务交易平台&#xff0c;敦煌网在品牌优势、技术优势、运营优势、用户优势四大维度上&#xff0c;已建立起竞争优势。随着跨境电商的日趋成熟&#xff0c;经营范围持续扩大、品类和渠道的增加&#xff0c;以及AIGC等行业新技…

Java程序猿搬砖笔记(十三)

文章目录 MySQL数据库生成自动增长序号MySQL修改密码SpringBoot定时任务解决Mybatis出现的各种Parameter not found. Available parameters are [ , ]Mybatis的foreach标签遍历mapSpringBoot项目打包SpringBoot Async使用注意事项Spring Cloud Config bootstrap文件&#xff…

内蒙古自治区出台加快充换电基础设施建设实施方案

摘要&#xff1a;为深入贯彻落实《国务院办公厅关于印发新能源汽车产业发展规划&#xff08;2021—2035年&#xff09;的通知》&#xff08;国办发 ﹝2020﹞39号&#xff09;、《国家发展改革委等部门关于进一步提升电动汽车充电基础设施服务保障能力的实施意见》&#xff08;发…

杭州久斗贸易有限公司官网上线 | LTD酒水行业案例分享

​一、公司介绍 杭州久斗贸易有限公司是集现代化电子商务与传统销售网络于一体的新兴企业。成立于2022年12月&#xff0c;是杭州地区一家专业的酒类销售贸易公司。 公司股东及经营者拥有二十年以上国内名酒销售经验&#xff0c;目前主营茅台.五粮液等高端白酒批发业务&am…

halcon matlab 中pose matrix之间的转换

pose_to_hom_mat3d (TransPose, HomMat3D) 原理&#xff1a; matlab 验证&#xff1a; function [a,b]getMatrix(pose) syms x y z; xdeg2rad(pose(4)); ydeg2rad(pose(5)); zdeg2rad(pose(6)); mat_x[1 0 0 0;0 cos(x) -sin(x) 0;0 sin(x) cos(x) 0; 0 0 0 1]; mat_…

Object.defineProperty到底有啥用

Object.defineProperty Object.defineproperty 的作用就是直接在一个对象上定义一个新属性&#xff0c;或者修改一个已经存在的属性 使用Object.defineProperty之前 number和person之间并无关联 使用Object.defineProperty之后 person和number之间有了关联&#xff1b;修…

1164 Good in C(38行代码+超详细注释)

分数 20 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 When your interviewer asks you to write "Hello World" using C, can you do as the following figure shows? Input Specification: Each input file contains one test case. For each case, the …

Ceph 分布式存储

Ceph概述 存储基础 单机存储设备 ●DAS&#xff08;直接附加存储&#xff0c;是直接接到计算机的主板总线上去的存储&#xff09; IDE、SATA、SCSI、SAS、USB 接口的磁盘 所谓接口就是一种存储设备驱动下的磁盘设备&#xff0c;提供块级别的存储 ●NAS&#xff08;网络附加存…