c++编程(20)——类与对象(6)继承

news2024/9/21 14:46:12

欢迎来到博主的专栏——c++编程
博主ID:代码小豪


文章目录

    • 继承
      • 继承与权限访问
    • 基类和派生类
      • 基类和派生类的赋值兼容转换
      • 基类与派生类的类作用域
      • 派生类与基类的构造函数
        • 基类与派生类拷贝构造函数
      • 继承与静态成员
      • final关键字

面向对象编程的核心思想是封装、继承和多态,通过封装,我们可以将类的接口与实现分离,封装在前面已经了解过了,现在来讲讲继承这一特性

继承

通过继承联系在一起的类构成一种层次关系。在层次关系的根部的类称为基类,其他类从基类继承而来,称为派生类。基类负责的定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

所以在面对对象编程中,设计一个合理的基类以及派生类是一个非常重要的步骤。我举一个例子。

比如,如果我们想要设计出一款游戏,那么首先我们可以考虑设计一个基类entity(实体),这个entity指的是游戏内可以交互的物体,以英雄联盟举例,英雄是一个实体,小兵和野怪是一个实体,防御塔也是一个实体,包括地形也是一个实体。

OK,根据上面所说关于继承的层次关系,基类应该设计成拥有层次中所有类共同拥有的成员(即成员函数和成员变量)。当前构思中设想的派生类有:英雄、小兵、防疫塔等。那么这些派生类拥有的统一特征是什么?

英雄和小兵可以移动,但是防御塔不能,因此移动不是基类的成员,英雄和防御塔都能进行攻击,但是地形不能进行攻击,因此攻击也不是基类的成员

博主这里想到了3个共同特征,(1)坐标,实体会有在地图中的坐标(2)碰撞体积,所有实体具有碰撞体积(3)实体会在游戏当中显示出位置

因此基类entity被博主设计成这样

class entity//基类enity
{
public: void display();
protected: 
	int _x;//x坐标
	int _y;//y坐标
	int _Hitbox;//碰撞体积
};

继承与权限访问

现在我们来设计enity的派生类,先来设计玩家操作的英雄类player吧。什么是player具有,但是其他派生类未必具有的操作呢?首先,player会有名字,第二、player会攻击,第三、player会移动,因此,我们可以把player设计成如下情况。

class entity
{
	public: void display();
	protected: 
	int _x;//x坐标
	int _y;//y坐标
	int _Hitbox;//碰撞体积
};

class player :public entity
{
public:
	void attack();//攻击
	void move();//移动
private:
	string _name;
};

前面滔滔不绝讲了这么多,但是博主竟然遗漏的最重要的内容,那就是派生类如何继承基类。以及基类的成员在派生类中的访问权限是什么。

首先,我们要为派生类指出,该派生类是由哪些基类继承而来的。方法如下:
我们在派生类player后面加上一个冒号(:),然后加上继承方式public,然后指出继承的基类是entity。

继承方式分为三种(1)public继承,(2)protected继承,(3)private继承,现在程序员在设计派生类时基本只采用public继承,因此博主只讲public继承的规则

基类的成员权限分为3种,分别是public,protected、private。在public继承下,这三种权限的成员在派生类中的访问权限如下:

基类中的成员权限派生类继承的基类权限
public可以在类域外访问,可以在类域内访问
protected不可在类域外访问,可以在类域内访问
private不可在类域外访问,不可在类域内访问

例子如下:

class A
{
public:
	void printA() {cout<<_x<<_y;};
protected:
	int _x;
private:
	int _y;
};

class B :public A
{
public:
	void printB()
	{
		printA();//可以访问基类的public成员
		cout << _x;//可以访问基类的protected成员
		cout << _y;//error不可访问基类的private成员
	}
};

int main()
{
	B b;
	b.printA();//可以在类外访问
	b._x;//error,不可在类外访问
	b._y;//error,不可在类外访问
	return 0;
}

由于B public 继承了A,因此在B类域内,可以访问A的public成员和protected成员,不可访问private成员_y。在B类域外,可以访问A的public成员printA(),不可访问A的protected和private成员。

此外,·派生类可以继承多个基类。

class a{};
class b{};
class c :public a, public b {};//c同时继承了a和b

基类和派生类

派生类拥有从基类继承而来的成员,也就是说,基类拥有的成员是所有派生类的共同成员,还记得前面举得关于游戏设计的例子吗?它们的层次关系为
在这里插入图片描述
实际上博主觉得这种图更能体现继承的关系
在这里插入图片描述
乍一看这个图和数学中的关于集合文恩图有点类似,但实际文恩图越小的圈表示包含的内容越少,而继承则是越小层次的派生类,包含的功能会越多。

基类和派生类的赋值兼容转换

以player和entity为例,entity的内存形式如下:
在这里插入图片描述
而player的内存形式如下
在这里插入图片描述
一个派生类对象包含两部分,一部分是从基类继承下来的数据,另一部分是派生类自己定义的数据。

基类对象可以用派生类对象进行赋值操作,其效果是得到派生类中基类部分的值。(反之不行)

以enity和player为例,博主为player增加了移动的功能

class entity
{
public: void display() { cout << _x << _y; };
protected: 
	int _x=0;//x坐标
	int _y=0;//y坐标
	int _Hitbox = 0;//碰撞体积
};

class player :public entity
{
public:
	//void attack();//攻击
	void move()//移动
	{
		_x += 1;
		_y += 1;
	}
private:
	string _name;
};

player调用move1次,x轴和y轴上的坐标就相应的增加1(大概就是往斜上方移动)。

entity entity1;//enity1坐标{0,0}
player player1;//player1坐标{0,0}
player1.move();//enity{0,0},player{1,1}
entity1 = player1;//entity{1,1},player1{1,1}
entity1.display();//1 1

player1和entity并非相同类型,也不符合类之间隐式转换赋值的规则(即赋值对象与赋值重载函数的参数类型一致。),那么为什么派生类可以赋值给基类呢?

c++允许派生类的对象赋值给基类的对象,其基类的值会变成派生类中有关基类部分的值。
在这里插入图片描述

基类的指针或引用可以指向派生类。这个过程可以成为切片或切割。意思就是将派生类中有关基类的值进行操作。

为了方便展示接下来的示例,博主将entity中的_x,_y,_Hitbox的权限修改为public,其余代码不变。

class entity
	{
	public: 
		void display() { cout << _x << _y; }; 
		int _x=0;//x坐标
		int _y=0;//y坐标
		int _Hitbox = 0;//碰撞体积
	};

	class player :public entity
	{
	public:
		//void attack();//攻击
		void move()//移动
		{
			_x += 1;
			_y += 1;
		}
	private:
		string _name;
	};
player player1;//player1坐标{0,0}

entity* eptr = &player1;//基类的指针
entity& eref = player1;//基类的引用

eptr->_x = 5;//将player1中的_x赋值为5
eref._y = 5;//将player1中的_y赋值为5
eptr->_name="LY";//error,不能操作派生类中除了基类以外的数据
eref->_name="LY";//error,同上
player1.display();//(5,5)

派生类的对象不能用基类赋值,派生类的指针和引用也不能指向基类。

基类与派生类的类作用域

基类与派生类拥有各自的类域,当派生类继承基类时,会在派生类中嵌套在基类的作用域(类似于局部域嵌套在全局域,即优先使用局部域中的标识符)。

如果一个标识符在派生类的作用域中无法被查找到,编译器就会在基类的类域中寻找该标识符的定义。

比如我们定义基类X

class X
{
public:
	void printX() { cout << _x << _y << _z; }
	int _x=0;
protected:
	int _y=0;
private:
	int _z=0;
};

标识符_x,_y,_z都存在基类X的类域当中,因此定义在基类类域的成员函数print可以访问这些成员。

现在我们用Y来继承基类X。即Y成为X的派生类

class Y:public X
{
public:
	void printY() { cout << _x << _y << _z; }//error,_z没有访问权限
};

在派生类Y的类域内使用了_x,_y,_z三个标识符,编译器会首先在派生类Y的类域中寻找这三个变量的定义,如果Y的类域中不存在这些标识符的定义,就会到基类的类域中寻找这些变量,而_x,_y,_z定义在基类X的类域当中,因此会访问基类类域中的变量。

由于_x,_y的访问权限分别是publc和protected,因此,我们可以在派生类Y的类域中访问到_x,_y,而_z的权限是private,不能再派生类Y中访问。

因此我们调用Y的printY时,会打印基类部分_x,_y的值(_z不能访问)。

void printY() { cout << _x <<_y; }//打印结果是0 0

由于派生类域和基类类域属于不同的作用域,C++中允许在不同的作用域中声明相同的标识符,比如局部域中的标识符可以和全局与的标识符重名,这个规则在类域当中也适用。

因此我们可以在派生类中定义和基类一致的标识符。

class X
{
public:
	void printX() { cout << _x << _y << _z; }
	int _x=0;
protected:
	int _y=0;
private:
	int _z=0;
};

class Y:public X
{
public:
	void printY() { cout << _x << _y <<_z; }
private:
	int _x = 1;
	int _y = 2;
	int _z = 3;
};

由于编译器会优先在派生类中查找定义,因此当访问派生类和基类同名的标识符时,会优先访问到派生类的成员,而基类中的同名成员则无法被访问,我们称这一特性为隐藏

X x;
Y y;
y.printY();//打印结果为1,2,3
x.printX();//打印结果为0,0,0

如果想要在派生类的类域中访问到被隐藏的基类的成员,就要用到类限定符(::)。

class Y:public X
{
public:
	void printY() { cout << X::_x << X::_y <<_z; }//没有访问X::_Z的权限
private:
	int _x = 1;
	int _y = 2;
	int _z = 3;
};

这样访问的就是基类X的_x,_y成员。

Y y;
y.printY();//打印结果为0,0,3

如果使用相同的成员函数,也会触发隐藏机制,比如:

class X
{
public:
	void print() { cout << _x << _y << _z; }//同名函数print
	int _x=0;
protected:
	int _y=0;
private:
	int _z=0;
};

class Y:public X
{
public:
	void print() { cout << _x << _y <<_z; }//同名函数print
private:
	int _x = 1;
	int _y = 2;
	int _z = 3;
};

此时如果y对象调用print,输出结果是1,2,3。如果想要调用X类域中的print,就要用到域限定符。

Y y;
y.print();//打印结果为1,2,3
y.X::print();//打印结果为0,0,0

派生类与基类的构造函数

派生类拥有从基类当中继承而来的成员,但是我们并非可以在派生类中初始化所有的基类成员,因为基类当中的private成员在派生类的类域当中无法访问,这也就导致无法在派生类的构造函数中通过直接赋值的方式完成从初始化。

于是c++允许在派生类的构造函数的初始化列表中调用基类的构造函数。以此来完成构造函数的初始化。

class entity
{
public:
	entity(int x, int y, int Hitbox)
		:_x(x)
		,_y(y)
		,_Hitbox(Hitbox){}
protected: 
	int _x=0;//x坐标
	int _y=0;//y坐标
	int _Hitbox = 0;//碰撞体积
};

class player :public entity
{
public:
	player(int x,int y,int Hitbox,string name)
		:entity(x,y,Hitbox)
		,_name(name){}
private:
	string _name;
};

当构造player时,将x,y,Hitbox实参传递给entity的构造函数,由entity的构造函数负责初始化player的基类部分,即(_x,_y,_Hitbox)成员。

c++规定派生类进行初始化时,会优先初始化基类,再初始化派生类定义的成员,即无论如何,当派生类调用构造函数时,一定会优先构造派生类的基类部分,表现为会优先调用基类相应的构造函数。

基类与派生类拷贝构造函数

以基类entity及其派生类player为例,我们先来看看这两个类的拷贝构造函数是怎样的。

entity(const entity& rhs)
{
	_x = rhs._x;
	_y = rhs._y;
	_Hitbox = rhs._Hitbox;
}
player(const player& player)
		:entity(player)
		,_name(player._name){}

派生类的拷贝构造函数依然可以调用基类的拷贝构造函数完成拷贝,但是有一个奇怪的点,那就是基类的拷贝构造的函数形参是entity类型,但是在派生类player中却传递了一个player类型的参数,为什么这种传参方式是可行呢?

其实这就是前面提到的基类与派生类之间的赋值兼容转换,即基类的引用、指针可以指向派生类。

派生类的赋值重载函数也是同理。

entity& operator=(const entity& entity)
{
	_x = entity._x;
	_y = entity._y;
	_Hitbox = entity._Hitbox;
}	
player& operator=(const player& player)
{
	entity::operator=(player);//调用entity的赋值重载函数
	_name = player._name;
}

继承与静态成员

如果基类存在一个静态成员,那么对于整个继承体系来说,都存在唯一的一个静态成员,即无论从基类中派生出多少派生类,该静态成员都是唯一的实例。

比如在基类entity中声明一个静态成员变量_num,

class entity
{
protected: 
	int _x=0;//x坐标
	int _y=0;//y坐标
	int _Hitbox = 0;//碰撞体积
	static int _num;
};

在其派生类player中,与entity共用同一个_num。

如果在基类中存在static成员函数,效果也是同理。

class entity
{
public:
	static void statement() { cout << _num; }
protected: 
	int _x=0;//x坐标
	int _y=0;//y坐标
	int _Hitbox = 0;//碰撞体积
	static int _num;
};

我们可以通过基类访问这个函数,可以通过派生类访问这个函数。

entity::statement();//通过基类访问
entity(1, 1, 1).statement();
player::statement();//通过派生类访问
player(1, 1, 1, "ly").statement();

final关键字

如果我们定义某个类无法被继承,就在类名后面加上关键字final。

class entity final
{
};

此时player将无法作为entity的派生类,发生报错

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

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

相关文章

LeetCode - 209 - 长度最小的子数组

力扣209题 题目描述&#xff1a;长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度**。**如果不存在符合条件的子数组&…

unordered_map与unordered_set的实现

目录 1.底层结构 1&#xff09;方式 2&#xff09;哈希冲突 3&#xff09;哈希函数 4&#xff09;哈希冲突的解决 1.闭散列 1&#xff09;线性探测 扩容&#xff1a; 2&#xff09;二次探测 2.开散列 1&#xff09;概念 2&#xff09;实现 插入操作&#xff1a; 删…

8个不可错过的高清视频素材网

寻找优质高清视频素材是许多创意工作者和内容创作者必不可少的一项任务。无论是在制作广告、影视作品&#xff0c;还是在进行视频编辑和设计工作&#xff0c;高质量的视频素材都能为你的作品增色不少。 推荐8个备受好评的高清视频素材网站&#xff0c;这些网站提供丰富多样、高…

Django基础知识

文章目录 新建Django项目helloworld关联数据库admin 新建Django项目 创建django-admin startproject project_name 运行 python manage.py runserver 创建app: python manage.py startapp app_name 目录&#xff1a; 配置文件 settings.py 路由配置 urls.py 项目管理 manage.p…

facebook广告效果下降,可能是进入了疲劳期

在做facebook广告的时候&#xff0c;你是否遇到过原本效果很好的广告&#xff0c;突然开始走下坡路的情况&#xff0c;这可能是因为广告进入了疲劳期&#xff0c;广告疲劳期指的是广告展现频率过高&#xff0c;用户多次看到相同广告后就对此感到了厌倦&#xff0c;所以会出现广…

IMA自动接触系统ACS操作手测

IMA自动接触系统ACS操作手测

飞轮科技首席执行官马如悦受邀出席可信数据库发展大会,三大事件,一文解读!

近日&#xff0c;由中国通信标准化协会、大数据技术标准推进委员会主办&#xff0c;InfoQ 极客传媒联合主办的 “2024 可信数据库发展大会” 在京成功召开。此次大会汇聚了众多数据库行业领军企业、专家学者&#xff0c;共同探讨全球数据库发展趋势&#xff0c;分享最具权威性的…

合成数据的pipline

参考&#xff1a;https://github.com/yizhongw/self-instruct 总体来说就是 大模型自己遵循一套流程来生成数据&#xff0c;然后来微调自己。 1.指令生成 每一个迭代都选8个任务的指令作为该任务的提示样本&#xff0c;其中6个是人写的&#xff0c;2个是生成的。 然后组成输…

从零到一:家政保洁小程序搭建全攻略与功能作用深度解析

目录 一、家政保洁小程序主要功能 二、家政保洁小程序搭建教程 &#xff08;一&#xff09;前期准备 &#xff08;二&#xff09;注册与选择工具 &#xff08;三&#xff09;设计与开发 &#xff08;四&#xff09;测试与优化 &#xff08;五&#xff09;发布与推广 一、…

笔记本CPU天梯图(2024年8月),含AMD/骁龙等新CPU

原文地址&#xff08;高清无水印原图/持续更新/含榜单出处链接&#xff09;&#xff1a; 2024年8月笔记本CPU天梯图 2024年8月笔记本CPU天梯图 2024年8月5日更新日志&#xff1a;常规更新Cinebench R23、PassMark笔记本CPU天梯图&#xff0c;新增Geekbench 6.2单核多核天梯图&…

您真的了解人类反馈强化学习(RLHF)吗?

生成性人工智能&#xff0c;就像ChatGPT和Gemini这样的应用&#xff0c;现在可火了&#xff0c;感觉我们生活中越来越离不开它们。 不过呢&#xff0c;这些工具虽然厉害&#xff0c;但用的时候也得留个心眼&#xff0c;因为它们可能会搞出些问题来。比如&#xff0c;有时候AI可…

DP转Type-c方案 带PD快充(外接显卡与VR)

DP转Type-C技术允许用户将DisplayPort信号转换为Type-C接口&#xff0c;‌以便连接和支持Type-C接口的设备。‌ DP转Type-C技术主要应用于需要将DisplayPort信号转换为Type-C接口的情况&#xff0c;‌以便连接和支持只有Type-C接口的设备。‌这种转换技术通过专门的转换器或连…

前端项目中的Server-sent Events(SSE)项目实践及其与websocket的区别

前端项目中的Server-sent Events(SSE)项目实践 前言 在前端开发中&#xff0c;实时数据更新是提升用户体验的重要因素之一。Server-SentEvents(SSE)是一种高效的技术&#xff0c;允许服务器通过单向连接将实时数据推送到客户端。下面将从SSE的基本改变&#xff0c;使用场景展…

TCP问题总结

TCP三次握手与四次挥手 1.TCP 头格式有哪些&#xff1f; 标注颜⾊的表示与本⽂关联⽐较⼤的字段&#xff0c;其他字段不做详细阐述。 序列号&#xff1a;在建⽴连接时由计算机⽣成的随机数作为其初始值&#xff0c;通过 SYN 包传给接收端主机&#xff0c;每发送⼀次数 据&am…

MapReduce入门教程

这可不是目录 入门定义与说明数据分析Map和Reduce阶段的任务<Kn,Vn>分析MapReduce的数据类型其他说明(持续更新) 开发案例(持续更新)自定义的wordcountcsv文件操作序列化操作 入门 定义与说明 数据分析 以下未数据分析示意图 Map和Reduce阶段的任务 Map阶段的任务&a…

安科瑞Acrel-2000ES储能能量管理系统在新型电力系统下分布式储能的研究

摘要&#xff1a;传统电力系统的结构和运行模式在以新能源为主体的新型电力系统中发生了巨大的变化&#xff0c;分布式储能作为电力系统中重要的能量调节器&#xff0c;也迎来了新的发展机遇。立足于储能技术发展现状&#xff0c;分析了分布式储能技术特点及在清洁可再生能源方…

ALLEGRO直接转PADS方法

1.ALLEGRO转PADS之前系统上添加用户变量 打开环境变量窗口&#xff0c;以WIN10为例 添加变量和值 变量名&#xff1a;AEX_BIN_ROOT值&#xff1a; PADS软件中translators软件的bin目录路径比如我的&#xff1a;AEX_BIN_ROOTD:\MentorGraphics\PADSVX.2.3\SDD_HOME\translator…

用python的manim库实现表格的绘制和制作【table 上】

表格的定义&#xff1a;按行和列排列的信息&#xff08;如数字和说明&#xff09;。 这是人们参加的运动的表格&#xff1a; 接下来绘制一些表格&#xff1a; 1. 创建一个包含小数数字的表格{DecimalTable} DecimalTable 是 Manim 中用于创建一个包含小数数字的表格的类。这个…

SD-WAN在海外网络加速中的优势

随着全球化的加剧&#xff0c;企业在海外业务拓展中面临着网络延迟、数据安全和成本等一系列挑战。针对这些问题&#xff0c;SD-WAN(软件定义广域网&#xff09;成为了企业网络架构的良好选择。本文将详细介绍SD-WAN在海外加速中的应用和优势。 1.SD-WAN是什么? SD-WAN是一种网…

软件开发者申请代码签名证书流程

软件目前已经成为人们生活和工作不可或缺的一部分&#xff0c;随着互联网的普及和软件分发渠道的多样化&#xff0c;软件的安全性和可信度变得越来越重要。 为了确保软件的完整性、安全性和来源可信性&#xff0c;代码签名证书应运而生。 在软件完成了开发之后&#xff0c;开…