【C++】面向对象---继承(万字详解)

news2024/12/24 20:46:59

目录

  • 前言
  • 一、继承的定义及概念
  • 二、继承方式
  • 三、基类和派生类之间的转换
  • 四、切片
  • 五、继承中的作用域
  • 六、派生类中的默认成员函数
  • 七、继承中的友元与静态成员
    • 继承与友元
    • 继承中的静态成员
  • 八、棱形继承和虚继承
    • 棱形继承
    • 虚继承
  • 总结


前言

继承是面向对象的一个重点,而继承和多态也息息相关。继承继承,顾名思义,就是继承父亲的所有。现实中有继承,那么C++里面也有继承。所以说,程序实际上就是对现实世界的抽象。废话不多说,接下来步入正题。


一、继承的定义及概念

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

那么继承如何定义呢? 很简单,继承的定义格式class 类名 : 继承方式(public/protected/private) 基类类名。
代码演示:
首先我们需要有一个基类,我暂且把基类的成员设置为公有。

class Person
{
public:
	string _name;
};

其次我们需要一个派生类,来继承基类。我们的基类是Person,那我们再定义一个Student类,来继承Person。

Student定义:

class Student : public Person
{
public:
	int _num;
};

这样,Student类和Person类就完成了继承关系。Student 就是Person的子类。但这也不意味着子类就可以使用父类的成员了。因为C++中的继承方式有9种。


二、继承方式

C++中的继承方式有以下9种。

类成员/继承方式public方式继承protected方式继承private方式继承
基类的public成员在派生类中是public在派生类中是protected在派生类中是private
基类的protected成员在派生类中是protected在派生类中是protected在派生类中是private
基类的private成员在派生类种不可见在派生类种不可见在派生类种不可见

也就是说,用public继承的话,基类的成员会原封不动的继承到派生类。所以我们上面用public继承,那么基类的public成员继承到派生类还是public成员。所以我们可以直接使用父类对象的public成员。

在这里插入图片描述


三、基类和派生类之间的转换

那么我们把子类(派生类) 赋值给 父类(基类),可以吗?
我们用下面这个代码来试试。

void test2()
{
	Person p;
	Student s;
	s._name = "李四";
	s._num = 2;
	p = s;
	cout << p._name << endl;
}

测试结果 :
在这里插入图片描述
子类对象是可以给父类的。那么父类对象可以给子类吗?

在这里插入图片描述
答案是不行,但是如果你真的想转,也是可以的。

//父类传给子类
void test3()
{
	Person p;
	Student s;
	Person* pp = &p;
	Student* ps = (Student*)pp;
	ps->_name = "张三";
	cout << ps->_name << endl;
}

但是这有很大的风险,那就是内存越界!因为基类没有派生类的成员。而用派生类的指针去强制指向父类,那么当派生类去访问自己的成员的时候,就会导致内存越界。
在这里插入图片描述

为什么会这样呢?那是因为在转换的过程会进行切片操作。


四、切片

当基类和派生类对象进行转换时,会发生切片操作。

基类有一个成员的_name,而派生类Student继承了Person。所以Student有个隐藏的成员_name。
在这里插入图片描述
当把Student 赋值给 Person时,会发生切片操作。
在这里插入图片描述

简单理解就是把 Person 给 Student的成员 赋值给了 Person,而Student自己的成员_num则不会赋过去。

为什么Person 不能赋值给Student? 因为Person没有足够的成员给Student,Student自己的成员_num无法被给予。
在这里插入图片描述

为什么指针强制转换就可以呢?
因为pp一开始指向Person
在这里插入图片描述

而Person 的空间范围是
在这里插入图片描述

此时把它强制转换成 Student指针的话,那么它的范围会变成这样。
在这里插入图片描述
这就造成了越界访问,所以这是非常危险的操作。


五、继承中的作用域

如果2个类中,有相同的成员名,那么父类的成员会被隐藏。优先使用子类的成员,简单说就是就近原则。

代码演示:

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

	string _name;
};

class Student : public Person
{
public:
	void f() //和父类有相同的函数成员
	{
		cout << "Student" << endl;
	}
	int _num;
};

void test4()
{
	Student s;
	s.f();
}

运行结果:
在这里插入图片描述
我们会发现它只调用了自己的成员,如果想调用父类的成员,那么需要::域限定符才能调用父类成员。

void test4()
{
	Student s;
	s.Person::f();
}

这样,我们就调用了父类的成员函数
在这里插入图片描述


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

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

1 . 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

2 . 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

3 . 派生类的operator=必须要调用基类的operator=完成基类的复制。

4 . 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

5 . 派生类对象初始化先调用基类构造再调派生类构造。

6 . 派生类对象析构清理先调用派生类析构再调基类的析构。

7 . 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

接下来我们来测试一下

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

	Person(const Person& p)
	{
		if (&p != this)
		{
			_name = p._name;
		}
		cout << "&Person" << endl;
	}

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

	string _name;
};

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

void test1()
{
	Student s;
}

int main()
{
	test1();

}

我们可以发现,它先调用了基类的构造函数,再调用派生类的构造函数。随后派生类先调用析构函数,基类再调用析构函数。
在这里插入图片描述

同样的,派生类再调用拷贝构造函数前,必须显示调用基类的拷贝构造函数。

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

	Student(const Student& p)
		:Person(p) //先调用基类的拷贝构造
	{
		if (&p != this)
		{
			_num = p._num;
		}
		cout << "&Student" << endl;
	}

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

在这里插入图片描述

而重写派生类 赋值操作符时,也必须先调用基类的 赋值操作符重写。

//基类的 =重写函数
	Person& operator=(const Person& p)
	{
		_name = p._name;
	}

//派生类的=重写函数
	Student& operator=(const Student& p)
	{
		Person::operator=(p); //调用基类的 =赋值重载
		_num = p._num;
	}

那么派生类的析构函数是否也要先调用基类的构造函数呢? 答案是不用!因为栈是一个后进先出的结构。基类先调用构造函数,那么基类就必须后析构。 这样才符合栈先进后出的性质,如果在析构函数里面调用了基类的析构函数,那么基类就会析构俩次。此时如果基类有动态申请的空间。那么程序就会报错,因为一块空间被释放了2次。


七、继承中的友元与静态成员

继承与友元

父类的友元不会继承给子类。通俗来讲,就是你父亲的朋友你不一定认识。如果要父类的友元也是子类的友元,那么需要子类自己去friend一下。

继承中的静态成员

静态成员会被继承,但是不会给它新开空间。通俗的说,子类父类共用同一个静态成员。

我们给父类加一个静态成员 _count,每次调用构造函数就++一次。

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

	Person(const Person& p)
	{
		if (&p != this)
		{
			_name = p._name;
		}
		cout << "&Person" << endl;
	}

	Person& operator=(const Person& p)
	{
		_name = p._name;
	}

	~Person()
	{
		cout << "~Person" << endl;
	}
	static int _count;
	string _name;
};
	int Person::_count = 0;

然后我们打印一下子类父类的count值以及count的地址

void test3()
{
	Person p;
	Student s;
	cout << p._count << ":" << &p._count << endl;
	cout << s._count << ":" << &s._count << endl;
}

在这里插入图片描述
这里我们可以看出,子类会继承父类的 静态成员,并且与父类共用一个静态成员。


八、棱形继承和虚继承

棱形继承

C++是支持多继承的,而多继承就会面临一个问题,那就是菱形继承。什么是菱形继承呢? 看一下这张图就知道了。

在这里插入图片描述

如图,assistant是助手类,它既可以继承Teacher,也可以继承Student。而Student 和 Teacher 都继承 Person。而这就形成了一个菱形继承,而菱形继承具有很大的冗余性和二义性。

那么我们也这段代码来测测它的冗余性。

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

	Person(const Person& p)
	{
		if (&p != this)
		{
			_name = p._name;
		}
		cout << "&Person" << endl;
	}

	Person& operator=(const Person& p)
	{
		_name = p._name;
	}

	~Person()
	{
		cout << "~Person" << endl;
	}
	string _name;
};

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

	Student(const Student& p)
		:Person(p) //先调用基类的拷贝构造
	{
		if (&p != this)
		{
			_num = p._num;
		}
		cout << "&Student" << endl;
	}

	Student& operator=(const Student& p)
	{
		Person::operator=(p); //调用基类的 =赋值重载
		_num = p._num;
	}

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

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

	Teacher(const Teacher& p)
		:Person(p) //先调用基类的拷贝构造
	{
		if (&p != this)
		{
			_num = p._num;
		}
		cout << "&Teacher" << endl;
	}

	Teacher& operator=(const Teacher& p)
	{
		Person::operator=(p); //调用基类的 =赋值重载
		_num = p._num;
	}

	~Teacher()
	{
		cout << "~Teacher" << endl;
	}
	int _num;
};

class Assistant : public Teacher,public Student
{
public:
	Assistant()
	{
		cout << "Assistant" << endl;
	}

	~Assistant()
	{
		cout << "~Assistant" << endl;
	}
	int _num;
};

void test4()
{
	Assistant a;
}

int main()
{
	test4();
}

我们发现,创建一个对象,Person被调用了2次。如果Person上面有大型数组,那么就会造成数据冗余。
在这里插入图片描述
再调用Assistant类的构造函数之前,必须先调用其父类的构造函数。而其继承顺序是先继承Teacher,所以它会先调用Teacher的构造函数。但又因为Teacher又继承了Person,所以Person的构造函数要在Teacher之前调用。所以调用顺序就是 Person->Teacher->Person->Student->Assistant 。析构顺序则是反过来。


而棱形继承不仅有冗余性,还有二义性。我们用Assistant对象调用一下_name成员。

在这里插入图片描述
我们会发现它提示,_name不明确。那是因为不知道用Teacher的_name还是用Student的_name。所以会出现这种情况。要解决这个问题也很简单,加上域限定符指定即可。
在这里插入图片描述

即便解决了二义性的问题,但依然存在冗余的情况,所以这种情况我们可以用虚继承。

虚继承

虚继承是在"腰"上进行的,也就是在 :后面加上virtual
在这里插入图片描述
Teacher类虚继承Person
在这里插入图片描述
Student类虚继承Person
在这里插入图片描述
然后我们再运行看看
在这里插入图片描述
我们可以看到,二义性解决了,冗余性也得到了改善。Person只会被构造一次。可能看似没什么,但如果我Person 有一个成员变量是 1个大小为100000的数组呢? 那么构造2次就会产生2个这样的数组,这样就造成了严重的空间浪费。


那么虚继承在内存中是怎么操作的呢? 首先,我们先写个棱形继承。

在这里插入图片描述


class A
{
public:
	int _a;
};

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

class 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;
	d._a = 0;
	return 0;
}

然后我们调试,打开内存监测窗口。
在这里插入图片描述
然后一步一步调试观察
在这里插入图片描述
最后的结果是这样的
在这里插入图片描述
在这里插入图片描述
我们发现 B和C的公共成员都存放在最后一行。
那么B和C上面的地址是什么呢?我们在内存观测一下
48 7b 00 01 ,小端存储的话就是倒过来的,实际地址是 01 00 7b 48
在这里插入图片描述

54 7b 00 01 ,小端存储就是 01 00 7b 54
在这里插入图片描述
我们发现第二行全是0,这是给其他东西预留的,我们可以不用管。我们只看第二行。而14是16进制的值,转换为10进制就是20,0c转换成十进制就是12。
在这里插入图片描述
这样我们就可以得出,第二行存放的是到公共成员的偏移量/相对距离。而偏移量就是地址处到公共成员的距离。
在这里插入图片描述
也就是说,在D里面,A放在了一个公共的位置,不属于B也不属于C。而B和C要去找A的时候。会通过 地址处(虚基表) 保存的偏移量,来找到A。

总结

在java或者其他的一些语言中,是没有多继承的,只有单继承。因为多继承可能是一个缺陷,也很少用到。但这也正是C++的特点,而和继承息息相关的还有多态,多态的难度还要大于继承。所以后面会为大家写一篇关于多态的文章。如果文中有讲的不好的,或者错误的地方。欢迎各位指出。

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

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

相关文章

活动星投票医疗保障案例推介网络评选微信的投票方式线上免费投票

“医疗保障案例推介”网络评选投票_线上免费投票系统_功能齐全的微信投票_在线免费投票用户在使用微信投票的时候&#xff0c;需要功能齐全&#xff0c;又快捷方便的投票小程序。而“活动星投票”这款软件使用非常的方便&#xff0c;用户可以随时使用手机微信小程序获得线上投票…

图形编辑器:标尺功能的实现

大家好&#xff0c;我是前端西瓜哥。今天我们来实现图形编辑器的标尺功能。 项目地址&#xff1a; https://github.com/F-star/suika 线上体验&#xff1a; https://blog.fstars.wang/app/suika/ 标尺指的是画布上边和左边的两个有刻度的尺子&#xff0c;作用让用户知道他正在编…

java 探花交友day2 项目简介,环境搭建 登录验证码

技术方案&#xff1a; 项目结构&#xff1a; 项目概述 通过接口文档&#xff08;API文档&#xff09;定义规范 开发工具安装与配置 Linux虚拟机 YAPI 账号 tanhuaitcast.cn 密码123456 安装个安卓模拟器&#xff0c;然后安装APK 开发环境说明 初始工程搭建 阿里云短…

Leetcode:235. 二叉搜索树的最近公共祖先(C++)

目录 问题描述&#xff1a; 实现代码与解析&#xff1a; 递归&#xff1a; 原理思路&#xff1a; 精简版&#xff1a; 迭代&#xff1a; 原理思路&#xff1a; 问题描述&#xff1a; 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先…

1589_AURIX_TC275_PMU_Flash的基本特性以及操作

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 关于这部分&#xff0c;感觉能够看到的比较有实践指导价值的信息不多。这里关于是否支持cache的信息&#xff0c;之前在内核手册等地方其实也看过了。 DFlash不支持buffer命中的功能&#…

21.Isaac教程--GEMS 导航堆栈简介

Isaac GEMS 导航堆栈简介 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 导航堆栈必须执行以下高级功能&#xff1a; Mapping 映射用于自动创建操作环境的地图。 该地图既用于定位&#xff0c;又用于路径规划。 它可以由具有附加功能的人进行注…

deap遗传算法 tirads代码解读

deap遗传算法 tirads代码解读写在最前面Overview 程序概览参考deap框架介绍creator模块创建适应度类Types定义适应度策略创建个体类Toolbox类创建种群&#xff08;个体、策略以及粒子&#xff09;Initialization1. 创建 attr_int 运算符2. 创建 individual_guess() 运算3.创建新…

学会python后:收集每天热点内容信息,并发送到自己的邮箱

嗨害大家好鸭&#xff01;我是小熊猫~ 实现目的 本篇文章内容主要为如何用代码&#xff0c;把你想要的内容&#xff0c;以邮件的形式发送出去 内容可以自己完善&#xff0c;还可以设置一个定时发送&#xff0c;或者开机启动自动运行代码 代理注册与使用 注册账号并登录 生成ap…

使用TDengine时序数据库的介绍以及系统整合

目录 一、 如何使用 安装目录介绍 数据文件查看和备份 客户端连接 sql使用 二、 系统整合 Java连接配置 Demo示例 三、 对采集点、超级表、子表和设备等关系进行维护 一、 如何使用 安装目录介绍 目录/文件 说明 /usr/local/taos/bin TDengine 可执行文件目录…

css笔记2

目录 选择器进阶 1、复合选择器 1.1后代选择器&#xff1a;空格 1.2 子代选择器&#xff1a; > 2、并集选择器&#xff1a;&#xff0c; 3、交集选择器 4、hover伪类选择器 Emmet语法 背景相关属性 1.1背景颜色 2.1背景图片 3.1背景平铺 4.1背景位置 5.1背景相关属…

linux中断机制

目录 1.中断机制 1.1.中断流程图 1.2.代码结构图 2.中断代码 2.1.硬件中断 2.2.asm.s 2.3.trap.c 2.3.1.trap_init函数 2.3.2.die函数 2.4 .sys_call.s 2.4.1._system_call.s 3.总结 1.中断机制 何为中断&#xff0c;中断里面各种名词的区分&#xff0c;请看下面这几篇…

安装VSCode图文版(附安装所需插件)

安装VSCode安装地址下载安装安装成功安装所需插件安装go插件安装中文简体安装地址 VSCode 安装地址 https://code.visualstudio.com/ 下载 在下面两个地方都可以下载&#xff0c;左侧下载可以根据自己的需要进行版本或者系统的选择下载。 安装 同意协议 选择附加项 为什…

基于python知识图谱医疗领域问答系统实现(完整代码+数据可直接运行)

直接上结果展示: “让人类永远保持理智,确实是一种奢求” ,机器人莫斯,《流浪地球》 项目概况 本项目为一个使用深度学习方法解析问题,知识图谱存储、查询知识点,基于医疗垂直领域的对话系统的后台程序 运行效果:

【阶段四】Python深度学习04篇:深度学习项目实战:深度神经网络预测客户流失率(分类模型)

本篇的思维导图: 深度神经网络预测客户流失率(分类模型) 项目背景 应用Keras框架构建单隐层网络和深度神经网络进行金融客户流失率的预测,以及模型的优化。主要用来熟悉Keras全连接层网络的使用。 数据获取 本次建模数据来源于网络,数据项统计如下: 编号

uni-app Vue3实现一个酷炫的多功能音乐播放器支持微信小程序后台播放

前言 本文存在多张gif演示图&#xff0c;建议在wifi环境下阅读&#x1f4d6; 最近在做网易云音乐微信小程序开源项目的时候&#xff0c;关于播放器功能参考了一些成熟的微信小程序&#xff0c;如网易云音乐小程序和QQ音乐小程序&#xff0c;但是发现这些小程序端的播放器相对于…

【寒假每日一题】洛谷 P7471 [NOI Online 2021 入门组] 切蛋糕

题目链接&#xff1a;P7471 [NOI Online 2021 入门组] 切蛋糕 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述 Alice、Bob 和 Cindy 三个好朋友得到了一个圆形蛋糕&#xff0c;他们打算分享这个蛋糕。 三个人的需求量分别为 a,b,c&#xff0c;现在请你帮他们切蛋糕…

Linux文件的默认权限、软硬链接和属性

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;Java案例分…

Java-FileInputStream和FileOutputStream的使用,txt文件及图片文件的拷贝

Java-FileInputStream和FileOutputStream的使用什么是IO流&#xff1f;流是什么&#xff1f;IO流的类图流的分类字符与字节的区别FileInputStream的使用1.构造器2.常用方法3.使用FileInputStream来读取txt文件FileOutputStream的使用1.构造器2.常用方法3.使用FileOutputStream写…

(11)go-micro微服务雪花算法

文章目录一 雪花算法介绍二 雪花算法优缺点三 雪花算法实现四 最后一 雪花算法介绍 雪花算法是推特开源的分布式ID生成算法&#xff0c;用于在不同的机器上生成唯一的ID的算法。 该算法生成一个64bit的数字作为分布式ID&#xff0c;保证这个ID自增并且全局唯一。 1.第一位占用…

【嘉立创EDA】构建自己的元件库,绘制符号、封装的方法

器件问题 先选择需要的元器件&#xff0c;然后查看其数据手册&#xff0c;找到官方提供的元件封装进行绘制。 器件 选择一款卧贴式双排排针进行绘制。 器件模型 主要用到的就是 Recommended P.C.B Layout 前期资料准备完毕&#xff0c;下面开始绘制自己的元件库。 元件库制作…