C++ | 继承

news2024/11/23 12:11:04

目录

前言

一、继承的基本概念与使用

1、继承的概念 

2、继承的定义 

3、继承的访问限定符与继承方式

二、基类与派生类之间的赋值转换(切片)

 三、继承中的作用域

1、继承中的作用域

2、隐藏(重定义)

四、派生类的默认构造函数

1、构造函数

2、拷贝构造

3、赋值重载 

4、析构函数

五、友元与继承 

六、继承与静态成员

七、菱形继承与菱形虚拟继承

1、菱形继承 

2、菱形继承的问题

3、菱形虚拟继承

4、虚拟继承的底层实现原理


前言

        在学习面向对象语言时,我们绕不开类这个概念,而我们可能都听过类的三大特性,分别为类的封装,继承与多态;封装我们在之前学习类中早已介绍,本文主要介绍类的继承这一特性;如果不了解类的封装特性,可以点击下方链接先去了解;

类与对象(上) 类与对象(中) 类与对象(下)

一、继承的基本概念与使用

1、继承的概念 

        生活中,我们也有继承的概念,子女继承父亲的财产,同时继承也是面向对象语言代码复用的重要手段;它允许程序员在不破坏原来类的情况下,派生出一个新的类,可以为这个新类自定义化出一些特殊功能;如下面这个Person类,里面有姓名与年龄;我们又想设计出一个新的学生类,也有姓名与年龄,同时也有学号,但是我们不想破坏以前的类,因此我们可以通过继承来获取person类的姓名和年龄,同时不破坏原来的类的基础上增加一个学号;

// 基类(父类)
class Person
{
public:

protected:
	string _name;
	int _age;
};

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

private:
	int _stuid;
};

        此时子类Student同时也具有了父类的姓名与年龄的信息;(我们一般把被继承的类称为父类或者基类,把继承的类称为派生类或者子类);

2、继承的定义 

 继承的定义如下图所示;

 

3、继承的访问限定符与继承方式

        以前我们在学习类的封装时,我们学过public与private两个限定符;本章我们又添加了一个新的访问限定符protected;

        protected与private在访问限定这里看起来好像没有任何区别,但其实在继承这就有了很大区别;实际上,这三个限定符不仅是用来修饰访问权限,还可以决定继承方式;

 

        以下为不同的继承方式下不同成员在子类的访问权限;其中protected与private成员在继承中的不同体现出来了;我们发现private成员无论在哪一种情况下都是不可见状态;

 综上:对其进行总结解释

1、不可见:所谓不可见即派生类也不能访问的成员,基类的private成员无论以何种继承方式,派生类都不可直接访问,但是可以通过基类提供的一些函数进行间接访问;(保护限定符也因此才出现)

2、对于上述表格,实际上,无需死记硬背,我们大体分为两类,一类为基类的private成员,此类成员无论何种继承方式,派生类都不可见;另一类为基类的public与protected成员,当选择某种继承方式时,其在子类的权限取基类成员修饰权限继承方式的最小值;如基类的publc成员,选择protected继承方式,那么派生类该成员的权限为其中较小的protected;

3、在定义派生类时,我们的继承权限符可以省略,这时,默认的继承权限与访问默认的权限相同,若为class定义,默认继承权限为private,若为struct定义,默认的继承权限则为public;

二、基类与派生类之间的赋值转换(切片)

        派生类对象 / 指针 / 引用 可以赋值给 基类对象/ 指针 / 引用;当基类不可赋值给派生类;这里有个形象的比喻,有人称这里为切片;

int main()
{
	Person p1;
	Student stu1;

	// 派生类对象赋值给基类对象
	// stu1 = p1; err
	p1 = stu1;
	
	// 派生类指针赋值给基类指针
	// Student* ptr2 = ptr1; err
	Person* ptr1 = &p1;
	// 指针可通过强制类型转换的方式将基类指针赋值给派生类(但会出现越界访问的问题)
	Student* ptr3 = (Student*)ptr1;

	// 派生类引用赋值给基类引用
	// Student& rstu1 = rp1; err
	Person& rp = stu1;
	
	return 0;
}

 三、继承中的作用域

1、继承中的作用域

        基类与派生类并不共用同一个作用域,他们都有着自己各自的作用域;基类成员在基类的类作用域中,派生类成员在派生类的类作用域中;如上面的student类与person类,student类继承了person类中的_age成员,其age位于person类的作用域中;

2、隐藏(重定义)

        由于继承中的作用域,如果我们的基类与派生类都存在一个同名对象;此时,派生类屏蔽了对基类同名对象直接访问的现象称为隐藏;如下代码;

class A
{
public:

	A(int aa = 1)
		:_aa(aa)
	{}
	int _aa;
};


class B : public A
{
public:
	B(int aa = 10, int bb = 20)
		:_aa(aa)
		,_bb(bb)
	{}

	int _aa;
	int _bb;
};

int main()
{
	B b;
    // 访问派生类中的_aa
	cout << b._aa << endl;
    // 指定访问基类类中的_aa
	cout << b.A::_aa << endl;
	// 调用派生类的print函数
	b.print();
	// 调用基类被隐藏的print函数
	b.A::print();
	return 0;
}

注意:只要基类与派生类的函数重名就会构成隐藏;与参数和返回值并没有什么关系;

四、派生类的默认构造函数

        派生类也是类,是类就会有默认成员函数;下面我们一一说明在派生类中的默认构造函数分别会做哪些事情;

1、构造函数

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

        派生类的对象初始化先调用基类的构造函数初始化基类那一部分的成员,再初始化派生类那一部分的成员;

// 基类
class Person
{
public:
	// 基类的构造
	Person(const string& name = "Jack", int age = 18)
		:_name(name)
		,_age(age)
	{
		cout << "Person()" << endl;
	}
	// 基类的拷贝构造
	Person(const Person& p)
	{
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		cout << "Person(const Person& p)" << endl;
	}
	// 基类的赋值重载
	Person& operator=(const Person& p)
	{
		_name = p._name;
		_age = p._age;
		cout << "Person& operator=(const Person& p)" << endl;
        return *this;
	}
	// 析构函数
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
	int _age;
};

// 派生类
class Student : public Person
{
public:
	// 派生类的默认构造
	Student(const string& name = "Jack", int age = 18, int stuid = 101)
		:Person(name, age)
		,_stuid(stuid)
	{
		cout << "Student()" << endl;
	}
protected:
	int _stuid;

};

        正如我们所说,我们构造一个派生类对象时,先调用了基类的构造,再初始化派生类的那一部分;

2、拷贝构造

        派生类的拷贝构造也必须先调用基类的拷贝构造,拷贝基类的那一部分,然后拷贝派生类那一部分;

	// 派生类拷贝构造
	Student(const Student& stu)
		:Person(stu) // 调用基类的拷贝构造(这里有切片)
		,_stuid(stu._stuid)
	{
		cout << "Student(const Student& stu)" << endl;
	}

         这里调用父类的拷贝构造运用了我们之前介绍过的切片,下面的赋值重载也是如此;

3、赋值重载 

        赋值重载也是如此;先调用基类的赋值重载;然后对派生类的成员进行赋值拷贝;

	// 派生类赋值重载
	Student& operator=(const Student& stu)
	{
		// 这里有切片与隐藏
		Person::operator=(stu); 
		_stuid = stu._stuid;
		cout << "Student& operator=(const Student& stu)" << endl;
		return *this;
	}

        注意,这里调用基类的赋值重载有切片与隐藏;我们的复制重载与父类同名,构成隐藏;而调用时,我们传参过程中又发生了切片;

4、析构函数

        派生类的析构略有一些复杂,因为后面某些原因,编译器通常会将基类与派生类的析构函数都处理为同一个名字destructor,故派生类与基类的析构函数重名,构成隐藏,而我们想显示调用就得显示写出作用域;如下

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

        而我们仔细观察发现这里的基类析构调用了两次,实际上,析构的顺序也是先调用派生类的析构,释放资源,然后由由编译器自动调用基类析构,上述代码,我们在派生类析构函数里显示调用了基类析构函数,后来编译器又自己调用了一次,所以这里基类析构调用了两次,所以正常应该这么写析构;

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

五、友元与继承 

        在继承这一套体系中,友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员;如下代码

class B;
class A
{
public:
	friend void print(const A& a, const B& b);
	A(int a)
		:_a(a)
	{}
protected:
	int _a;
};

class B : public A
{
public:
	B(int a, int b)
		:A(a)
		, _b(b)
	{}

private:
	int _b;
};
// 基类的友元
void print(const A& a, const B& b)
{
	cout << a._a << endl;
    // 并不是子类的友元
	//cout << b._b << endl; err
}
int main()
{
	A a(1);
	B b(10, 20);
	print(a, b);
	return 0;
}

六、继承与静态成员

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

class A
{
public:
	A()
	{
		_count++;
	}
	static int GetCount()
	{
		return _count;
	}
protected:
	static int _count;
};
int A::_count = 0;

class B : public A
{

};

int main()
{
	A a1;
	A a2;
	B b1;
	cout << B::GetCount() << endl;
	return 0;
}

        由于整个继承体系公共一个static成员,因此当A、B类共用一个static成员变量;

七、菱形继承与菱形虚拟继承

1、菱形继承 

        所谓菱形继承是多继承引起的一种现象;即一个子类有两个或两个以上直接父类;如下图所示;

        其中Student类与Teacher类继承于Person类,而Graduate有同时继承了Student类与Teacher类;类似与相近于一种菱形的形状;

2、菱形继承的问题

        菱形继承存在二义性与冗余性;

class Person
{
public:
	Person(const string& name = "Jack", int age = 18)
		:_name(name)
		,_age(age)
	{}
	string _name;
	int _age;
};

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

class Teacher : public Person
{

public:
	int _workid;
};

class Graduate : public Student, public Teacher
{
public:
	int _Graid;
};

int main()
{
	Graduate g;
	// 二义性
	// g._name = "张三"; err

	// 冗余性
	g.Student::_name = "张三";
	g.Student::_age = 18;
	g.Teacher::_name = "李四";
	g.Teacher::_age = 24;
	return 0;
}

 

        观察发现,我们的Graduate对象有两份名字与年龄;并且每次赋值都需要指定特定的类域;这便是分别是菱形继承的二义性与冗余性;

3、菱形虚拟继承

        那么C++是如何解决这种菱形继承产生的二义性和冗余性呢?C++采用的是一种虚拟继承的方法,在Student与Teacher类继承方式前加上virtual即可;如下所示;

class Person
{
public:
	Person(const string& name = "Jack", int age = 18)
		:_name(name)
		, _age(age)
	{}
	string _name;
	int _age;
};

class Student : virtual public Person
{
public:
	int _stuid;
};

class Teacher : virtual public Person
{

public:
	int _workid;
};

class Graduate : public Student, public Teacher
{
public:
	int _Graid;
};

        此时,所有Graduate中只会有一份name,age;(可通过内存窗口观察,监视窗口仍然存在多份);下面,我们来研究虚拟继承是如何实现这种机制的,我带着大家通过内存窗口来观察;

4、虚拟继承的底层实现原理

以下分别为普通菱形继承与菱形虚拟继承的内存图;

        我们看到,虚拟继承玩的是一套虚基表的模型,D类的基类B与C中都会存放一个虚基表指针,它们各自指向各自的虚基表;虚基表中存放了偏移地址,其中我们可以观察到,B类的虚基表中储存了一个偏移地址20,我们从B类起始地址出发,向下偏移20个字节,刚好访问到他们的基类A;而C类中的虚基表中存放了一个偏移地址为12的指针,我们从C类的其实地址出发,向下偏移12个字节,也刚好访问到A类,这时,解决了二义性与数据冗余,他们都会通过自己的虚基表找到对应的偏移地址,从而找到他们的基类;

        那么很多问题的小伙伴们就疑惑了,使用虚基表这一套体系,反而会使用更多的字节,从第一张图可以看到,D类占5个字节,而虚基表这套,第二站图,D类占6个字节(不算虚基表所占空间);

        是的,在D类这种结构中,确实要多开辟一些空间,但是如果A类的大小不止4个字节呢?如果是8字节甚至更多呢?按照普通菱形继承来看,我们是否需要存几个A类,而虚基表这套体系,只需要储存一个虚机指针,在虚基表中存下基类的偏移地址即可;而且,所有该类实例化出的对象,都只使用这一张虚基表即可;

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

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

相关文章

知识付费小程序怎么做

知识付费小程序是一种通过在线平台提供知识和教育内容的应用程序。下面将详细介绍其功能&#xff1a; 1. 音频视频课程&#xff1a; 知识付费小程序提供了丰富的音频和视频课程&#xff0c;在这些课程中&#xff0c;用户可以通过观看或听取专业讲师的讲解来学习各种知识领域。…

【文章系列解读】Nerf

1. Nerf NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis 2020年8月3日 &#xff08;0&#xff09;总结 NeRF工作的过程可以分成两部分&#xff1a;三维重建和渲染。&#xff08;1&#xff09;三维重建部分本质上是一个2D到3D的建模过程&#xff…

Java习题之实现平方根(sqrt)函数

目录 前言 二分查找 牛顿迭代法 总结 &#x1f381;博主介绍&#xff1a;博客名为tq02&#xff0c;已学C语言、JavaSE&#xff0c;目前学了MySQL和JavaWed &#x1f3a5;学习专栏&#xff1a; C语言 JavaSE MySQL基础 &#x1f384;博主链接&#xff1a;tq02的…

【第四章 flutter-初识flutter】

文章目录 一、目录结构二、创建一个flutter项目三、创建自定义组件四、Container组件 就是divalignment 内容对齐方式decoration 类似border 为BoxDecoration的类 五、Text属性六、image组件总结、 一、目录结构 android、ios各自平台的资源文件 lib 项目目录 linux macos PC平…

Linux宝塔Mysql读写分离配置,两台服务器,服务器存在多个库

Linux宝塔Mysql读写分离配置&#xff0c;两台服务器&#xff0c;服务器存在多个库 一、主库操作 #登录数据库&#xff0c;用root登录方便&#xff0c;用其他账号会提示权限不足&#xff0c;需要登录root给予权限 mysql -u root -p 密码#创建一个账号&#xff0c;供从库用该账…

电商企业需要部署WMS仓储管理系统吗

随着电子商务行业的迅速发展&#xff0c;电商企业面临着日益增长的订单量和复杂的物流流程。为了提高仓储管理的效率和准确性&#xff0c;许多电商企业开始考虑部署WMS仓储管理系统。然而&#xff0c;是否真的需要部署WMS仓储管理系统&#xff0c;仍然是一个值得探讨的问题。本…

vLLM大模型推理加速方案原理(PagedAttention)

一、vLLM 简介 vLLM 用于大模型并行推理加速&#xff0c;核心是 PagedAttention 算法&#xff0c;官网为&#xff1a;https://vllm.ai/。 vLLM 主要特性&#xff1a; 先进的服务吞吐量通过 PagedAttention 对注意力 key 和 value 进行内存管理对传入请求的批处理针对 CUDA 内…

纯代码和低代码的本质区别

一、前言 纯代码和低代码是现代软件开发中两种不同的方法。 纯代码需要专业的编程技能&#xff0c;掌握编程语言、算法和数据结构等专业知识。而低代码则是一种新兴的开发方法&#xff0c;它大大降低了对编程技能的要求&#xff0c;让非技术人员也能够创建应用程序。随着低代码…

【SpringBoot】--03.数据访问、基础特性(外部化和内部外配置、整合JUnit)

文章目录 SpringBoot3-数据访问1.整合SSM场景1.1创建SSM整合项目1.2配置数据源1.3配置MyBatis1.4CRUD编写 2.自动配置原理3.扩展&#xff1a;整合其他数据源3.1 Druid 数据源 SpringBoot3-基础特性1. SpringApplication1.1 自定义 banner1.2.自定义 SpringApplication1.3Fluent…

nvm 管理node 环境配置

nvm安装&#xff1a; nvm&#xff08;Node Version Manager&#xff09;是一个用来管理node版本的工具。我们之所以需要使用node&#xff0c;是因为我们需要使用node中的npm(Node Package Manager)&#xff0c;使用npm的目的是为了能够方便的管理一些前端开发的包&#xff01;…

ColorOS凭什么夺冠?

摘要&#xff1a;五大主流安卓系统流畅度PK&#xff0c;谁的体验最好&#xff1f; 评价一款手机&#xff0c;你最先看的是什么&#xff1f; 是处理器平台&#xff1f;CPU核心频率&#xff1f;还是内存配置&#xff1f; 虽然这些硬件参数能够清晰地反映几款不同配置手机之间的性…

20230712-----阻塞IO驱动按键控制LED灯的亮灭

驱动程序 #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/of.h> #in…

TeeChart for.NET Crack

TeeChart for.NET Crack TeeChart for.NET为各种图表需求提供了图表控件&#xff0c;包括金融、科学和统计等重要的垂直领域。它可以处理您的数据&#xff0c;在各种平台上无缝创建信息丰富、引人入胜的图表&#xff0c;包括Windows窗体、WPF、带有HTML5/Javascript渲染的ASP.N…

敢不敢和AI比猜拳?能赢算我输----基于手势识别的AI猜拳游戏【含python源码+PyqtUI界面+原理详解】-python手势识别 深度学习实战项目

功能演示 摘要&#xff1a;手势识别是一种通过技术手段识别视频图像中人物手势的技术。本文详细介绍了手势识别实现的技术原理&#xff0c;同时基于python与pyqt开发了一款带UI界面的基于手势识别的猜拳游戏。手势识别采用了mediapipe的深度学习算法进行手掌检测与手部的关键点…

字符设备驱动开发(最初方式)

目录&#xff1a; 1.字符设备驱动简介2.字符设备驱动开发步骤2.1. 驱动模块的加载与卸载2.2. Makefile的编写2.3.字符设备的注册与注销2.3.1.设备号的组成2.3.2.设备号的分配 2.4.具体操作函数的实现2.4.1.进行打开和关闭操作2.4.2.对chrdev进行读写操作 3.具体程序的实现3.1.驱…

第十一章——使用类

运算符重载 运算符重载是一种形式的C多态。之前介绍过的函数重载&#xff08;定义多个名称相同但特征标不同的函数&#xff09;让程序员能够用同名的函数来完成相同的基本操作&#xff0c;即使这些操作被用于不同的数据类型。 运算符重载将重载的概念扩展到运算符上&#xff0…

gulimall-性能监控与压力测试

性能监控与压力测试 前言一、性能监控1.1 jvm 内存模型1.2 jvisualvm 作用1.3 监控指标 二、压力测试2.1 概念2.2 性能指标2.3 JMeter 压测工具 前言 本文继续记录B站谷粒商城项目视频 P141-150 的内容&#xff0c;做到知识点的梳理和总结的作用。 一、性能监控 1.1 jvm 内存…

灯具小程序怎么制作

灯具小程序怎么制作&#xff0c;有什么功能 1. 商品展示&#xff1a;灯具小程序商城提供了丰富多样的灯具产品&#xff0c;并通过清晰的商品展示页面展示给用户。用户可以浏览不同种类的灯具&#xff0c;包括吊灯、台灯、壁灯等&#xff0c;了解产品的图片、规格、价格等详细信…

python 文件夹py文件相互引用

文章目录 前言Python文件相互调用情况一&#xff1a;同级文件情况二&#xff1a;非同级文件上层调用下层下层调用上层sys.path.append(.)详细测试同名测试引入结论 跨文件夹调用 总结 前言 我之前学过一些别的语言&#xff0c;例如Java,C#,JS。所以我上手python还是挺快的&…

Threejs模型切片转3DTiles加载

个人主页&#xff1a; 左本Web3D&#xff0c;更多案例预览请点击》 在线案例 个人简介&#xff1a;专注Web3D使用ThreeJS实现3D效果技巧和学习案例 &#x1f495; &#x1f495;积跬步以至千里&#xff0c;致敬每个爱学习的你。获取模型或源码请点赞收藏加留言&#xff0c;有问…