c++:面向对象三大特性--继承

news2024/11/27 6:16:29

在这里插入图片描述

面向对象三大特性--继承

  • 一、继承的概念及定义
    • (一)概念
    • (二)继承格式
      • 1、继承方式
      • 2、格式写法
      • 3、派生类继承后访问方式的变化
    • (三)普通类继承
    • (四)类模板继承
  • 二、基类和派生类的转换
    • (一)基类转换派生类
    • (二)派生类转换基类
  • 三、几个重要细节
    • (一)继承与作用域
      • 1、作用域
      • 2、隐藏
    • (二)继承与友元
    • (三)继承与静态成员
  • 四、继承中派生类的构造函数
  • 五、多继承与菱形继承
    • (一)多继承
      • 多继承的指针偏移问题
    • (二)菱形继承
    • (三)虚继承
  • 六、继承和组合
    • 结束语:

一、继承的概念及定义

(一)概念

继承是⾯向对象程序设计使代码可以复用的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。

(二)继承格式

1、继承方式

我们前面对类的成员有三种限制方式,这里也就对应了三种继承方式
在这里插入图片描述

2、格式写法

在这里插入图片描述

3、派生类继承后访问方式的变化

1、通过表格可以发现,如果是private成员,那么无论哪种继承方式都不可以访问到这个权限
2、此外,structclass这两个关键字在继承时也有差距,struct默认继承方式为公有,而class默认继承方式为私有。

我们如果将权限的大小定义为 public > protected > private, 那么其余访问方式变化就是将大于该继承方式的权限降到继承方式的权限即可

在这里插入图片描述

(三)普通类继承

这里用到的是继承最基本的语法,采用public继承,那么除了父类的private变量不可访问以外,成员的权限保持不变。

class Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
	}
protected:
	string _name = "张三"; // 姓名
private:
	int _age = 18; // 年龄
};

class Student : public Person
{
public:
	void func()
	{
		Print();
	}
protected:
	int _stunum; // 学号
};

(四)类模板继承

在之前我们实现stack时,采用的是新建了一个容器类型,在这里我们亦可以采用继承的方式来实现。
需要注意的是,派生类在继承时,如果需要访问父类的成员函数,需要指定类域,模板的成员函数采用的是按需实例化

namespace wgm
{
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: “push_back”: 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			vector<T>::push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

二、基类和派生类的转换

(一)基类转换派生类

1、基类对象不能赋值给派⽣类对象。
2、基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)dynamic_cast 来进⾏识别后进⾏安全转换。

(二)派生类转换基类

1、public继承的派⽣类对象 可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切
。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

值得注意的是,之前在隐式类型转换时会生成临时变量,因此在应用时需要加上const,而在切片时不会生成中间的临时变量

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
public:
	int _age = 18; // 年龄
};

class Student : public Person
{
public:
	int _No; // 学号
};

int main()
{
	string s1 = "11111";
	const string& s2 = "11111";

	Student sobj;
	// 赋值兼容转换,特殊处理
	// 1.派生类对象可以赋值给基类的指针/引用
	Person* pp = &sobj;
	Person& rp = sobj;
	rp._age++;
	return 0;
}

在这里插入图片描述

接下来通过下面的例子发现,继承后的基类私有变量虽然访问不到,但是我们可以发现它在派生类的对象中依旧占据相应的空间,而经过赋值兼容转换变量的大小为基类的大小

在这里插入图片描述

在这里插入图片描述

接下来更加深层的来了解赋值兼容,发现基类的指针或引用在调用重名函数的时候,调用的是父类的函数,而派生类调用时因为隐藏的特点,派生类对象调用的是派生类的函数

class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}

protected:
	int _a;
	int _b;
private:
	int _c;
};

class B : public A
{

public:
	void func()
	{
		cout << "B::func()" << endl;
	}

public:
	int _d;
};

int main() {
	B obj_b;
	A* ptr_a = &obj_b;
	A& ref_a = obj_b;
	obj_b.func();
	ptr_a->func();
	ref_a.func();
	return 0;
}

2、子类的变量可以复制给父类。

	Person pobj = sobj;

在这里插入图片描述

三、几个重要细节

(一)继承与作用域

1、作用域

在继承体系中基类和派⽣类都有独⽴的作⽤域。

2、隐藏

派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

(二)继承与友元

在继承时,友元关系是不接受继承的。所以如果友元函数需要访问派生类的成员,需要重新声明友元。

(三)继承与静态成员

在继承后,静态成员变量始终只有基类在定义的这一份。通过下面的代码可以发现,我们可以用类域加静态变量的方式来访问静态变量,但是打印的地址是同一份。

class Person
{
public:
	string _name;
	static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum;
};
int main()
{
	Person p;
	Student s;
	cout << &p._count << endl;
	cout << &s._count << endl;
	cout << Person::_count << endl;
	cout << Student::_count << endl;
	return 0;
}

在这里插入图片描述

四、继承中派生类的构造函数

  1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。基类没有默认的构造函数必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
  2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
  3. 派⽣类的operator=必须要调⽤基类的operator=。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以指定基类作⽤域显⽰调⽤基类的operator=
  4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。才能保证先清理派⽣类成员再清理基类成员。因为多态中⼀些场景析构函数需要构成重写。,那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
class Person
{
public:
	Person(const char* name)
		: _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;
	}

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

protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	Student(int num, const char* address, const char* name)
		:_num(num)
		, _address(address)
		, Person(name)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		: Person(s)
		, _num(s._num)
		, _address(s._address)
	{
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			_num = s._num;
			_address = s._address;
			Person::operator=(s);
		}

		return *this;
	}

	// destructor()
	~Student()
	{
		// 不需要写,子类析构函数结束后,会自动调用父类析构
		//Person::~Person();
		cout << "~Student()" << endl;
	}

protected:
	int _num; //学号
	string _address;
};

五、多继承与菱形继承

(一)多继承

单继承:⼀个派⽣类只有⼀个直接基类时称为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称为多继承

多继承的指针偏移问题

多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯
在这里插入图片描述
通过上面的例子,我们可以清晰的认识到基类在派生类的储存情况。

(二)菱形继承

菱形继承:菱形继承是多继承的⼀种特殊情况,有数据冗余和⼆义性的问题

在这里插入图片描述

class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

//给类加上 virtual 关键字,解决菱形继承造成的二义性和数据冗余。

//class Student : virtual public Person
//{
//protected:
//	int _num; //学号
//};
//
//class Teacher : virtual public Person
//{
//protected:
//	int _id; // 职工编号
//};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main() {
	Assistant obj;
	obj.Student::_name = "张三";
	obj.Teacher::_name = "李四";

	return 0;
}

在这里插入图片描述
通过调试窗口,可以发现我们在继承时同时继承了来自Person和来自Teacher_name我们在写代码时无法处理这个二义性,同时也形成了数据冗余。

(三)虚继承

为了解决这个现象,我们只需要在继承同一个基类成员的派生类加上一个virtual关键字,底层会自行加工,使得我们后面访问的_name只是一份数据。

class Person
{
public:
	string _name; // 姓名
};

//给类加上 virtual 关键字,解决菱形继承造成的二义性和数据冗余。
class Student : virtual public Person
{
protected:
	int _num; //学号
};

class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main() {
	Assistant obj;
	obj.Student::_name = "张三";
	obj.Teacher::_name = "李四";

	return 0;
}

下列窗口显示出来的_name实则是同一份数据,最开始指定类域Student::初始化_name为张三
在这里插入图片描述
我们通过Teacher::修改数据为李四,那么数据被修改为李四。
在这里插入图片描述

切记,尽量不用使用菱形继承,因为virtual关键字在解决问题的同时造成了效率的降低,代价有点大。

六、继承和组合

继承组合
定义public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
复用方式白箱复用:在继承⽅式中,基类的内部细节对派⽣类可⻅⿊箱复⽤:通过调用对象的接口实现,对象的内部细节是不可⻅的
耦合度继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度高组合类之间没有很强的依赖关系,耦合度低

我们可以发现,组合的好处要大于继承,在两种都可以的情况下,优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。

结束语:

感谢一直以来支持的朋友,支持一路走来披荆斩棘的道友,或许不识,一路同行!

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

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

相关文章

【Linux学习】【Ubuntu入门】2-5 shell脚本入门

1.shell脚本就是将连续执行的命令携程一个文件 2.第一个shell脚本写法 shell脚本是个纯文本文件&#xff0c;命令从上而下&#xff0c;一行一行开始执行&#xff0c;其扩展名为.sh&#xff0c;shell脚本第一行一定要为&#xff1a;#!/bin/bash&#xff0c;表示使用bash。echo…

Jmeter中的测试片段和非测试原件

1&#xff09;测试片段 1--测试片段 功能特点 重用性&#xff1a;将常用的测试元素组合成一个测试片段&#xff0c;便于在多个线程组中重用。模块化&#xff1a;提高测试计划的模块化程度&#xff0c;使测试计划更易于管理和维护。灵活性&#xff1a;可以通过模块控制器灵活地…

VisionPro 机器视觉案例 之 凹点检测

第十六篇 机器视觉案例 之 凹点检测 文章目录 第十六篇 机器视觉案例 之 凹点检测1.案例要求2.实现思路2.1 方式一&#xff1a;斑点工具加画线工具加点线距离工具2.2 方法二 使用斑点工具的结果集边缘坐标的横坐标最大值ImageBoundMaxX2.3 方法三 使用斑点工具的结果集凹点结果…

Java ArrayList 与顺序表:在编程海洋中把握数据结构的关键之锚

我的个人主页 我的专栏&#xff1a;Java-数据结构&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 前言&#xff1a;在 Java编程的广袤世界里&#xff0c;数据结构犹如精巧的建筑蓝图&#xff0c;决定着程序在数据处理与存储时的效率、灵活性以…

【k8s】资源限制管理:Namespace、Deployment与Pod的实践

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Kubernetes航线图&#xff1a;从船长到K8s掌舵者》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、什么是k8s 2、在k8s使用资源配额的作…

lua除法bug

故事背景&#xff0c;新来了一个数值&#xff0c;要改公式。神奇的一幕出现了&#xff0c;公式算出一个非常大的数。排查是lua有一个除法bug,1除以大数得到一个非常大的数。 function div(a, b)return tonumber(string.format("%.2f", a/b)) end print(1/73003) pri…

微信小程序学习指南从入门到精通

&#x1f5fd;微信小程序学习指南从入门到精通&#x1f5fd; &#x1f51d;微信小程序学习指南从入门到精通&#x1f51d;✍前言✍&#x1f4bb;微信小程序学习指南前言&#x1f4bb;一、&#x1f680;文章列表&#x1f680;二、&#x1f52f;教程文章的好处&#x1f52f;1. ✅…

《基于FPGA的便携式PWM方波信号发生器》论文分析(三)——数码管稳定显示与系统调试

一、论文概述 基于FPGA的便携式PWM方波信号发生器是一篇由任青颖、庹忠曜、黄洵桢、李智禺和张贤宇 等人发表的一篇期刊论文。该论文主要研究了一种新型的信号发生器&#xff0c;旨在解决传统PWM信号发生器在移动设备信号调控中存在的精准度低和便携性差的问题 。其基于现场可编…

计算机操作系统——进程控制(Linux)

进程控制 进程创建fork&#xff08;&#xff09;函数fork() 的基本功能fork() 的基本语法fork() 的工作原理fork() 的典型使用示例fork() 的常见问题fork() 和 exec() 结合使用总结 进程终止与$进程终止的本质进程终止的情况正常退出&#xff08;Exit&#xff09;由于信号终止非…

【贪心算法第四弹——376.摆动序列】

目录 1.题目解析 题目来源 测试用例 2.算法原理 3.实战代码 代码解析 本题还可以使用动态规划的解法来解决&#xff0c;不过动态规划的时间复杂度为O(N^2)&#xff0c;而贪心解法的时间复杂度为O(N)&#xff0c;动态规划方法的博客链接: 动态规划-子序列问题——376.摆动…

我谈离散傅里叶变换的补零

有限序列的零延拓——零延拓不会改变离散傅里叶变换的形状的续篇。 L点序列可以做N点傅里叶变换&#xff0c;当 L ⩽ N L\leqslant N L⩽N时不会产生混叠。这部分内容在Rafael Gonzalez和Richard Woods所著的《数字图像处理》完全没有提到。 补零是序列末尾补零&#xff0c;不…

day18 结构体

有参宏和函数的区别 1.展开时机&#xff1a;有参宏而言&#xff0c;在预处理阶段展开&#xff0c;而函数在调用时才展开 2.内存使用&#xff1a;有参宏而言&#xff0c;占用的是所在函数的空间&#xff0c;而函数在调用时会单独开辟空间 3.效率上&#xff1a;有参宏的效率比…

C嘎嘎探索篇:栈与队列的交响:C++中的结构艺术

C嘎嘎探索篇&#xff1a;栈与队列的交响&#xff1a;C中的结构艺术 前言&#xff1a; 小编在之前刚完成了C中栈和队列&#xff08;stack和queue&#xff09;的讲解&#xff0c;忘记的小伙伴可以去我上一篇文章看一眼的&#xff0c;今天小编将会带领大家吹奏栈和队列的交响&am…

Postman设置接口关联,实现参数化

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 postman设置接口关联 在实际的接口测试中&#xff0c;后一个接口经常需要用到前一个接口返回的结果&#xff0c; 从而让后一个接口能正常执行&#xff0c;这…

大模型的RAG微调与Agent:提升智能代理的效率与效果

目录 ​编辑 引言 RAG模型概述 检索阶段 生成阶段 RAG模型的微调 数据集选择 损失函数设计 微调策略 超参数调整 RAG模型在智能代理中的应用 客户服务 信息检索 内容创作 决策支持&#xff1a; 结论 引言 在人工智能的快速发展中&#xff0c;大型预训练模型&a…

前端---CSS(部分用法)

HTML画页面--》这个页面就是页面上需要的元素罗列起来&#xff0c;但是页面效果很差&#xff0c;不好看&#xff0c;为了让页面好看&#xff0c;为了修饰页面---》CSS CSS的作用&#xff1a;修饰HTML页面 用了CSS之后&#xff0c;样式和元素本身做到了分离的效果。---》降低了代…

【R语言管理】Pycharm配置R语言及使用Anaconda管理R语言虚拟环境

目录 使用Anaconda创建R语言虚拟环境1. 安装Anaconda2. 创建R语言虚拟环境 Pycharm配置R语言1. 安装Pycharm2. R Language for IntelliJ插件 参考 使用Anaconda创建R语言虚拟环境 1. 安装Anaconda Anaconda的安装可参见另一博客-【Python环境管理工具】Anaconda安装及使用教程…

互联网视频推拉流EasyDSS视频直播点播平台视频转码有哪些技术特点和应用?

视频转码本质上是一个先解码再编码的过程。在转码过程中&#xff0c;原始视频码流首先被解码成原始图像数据&#xff0c;然后再根据目标编码标准、分辨率、帧率、码率等参数重新进行编码。这样&#xff0c;转换前后的码流可能遵循相同的视频编码标准&#xff0c;也可能不遵循。…

Linux Shell 脚本题目集

1、执行 ping 命令对指定主机进行测试&#xff0c;以确定该主机是否处于存活状态并输出相应结果。 #!/bin/bashread -p "请输入主机号&#xff1a;" pc # 读取用户输入的主机号if [ -z "$pc" ];then # 检查用户输入是否为空echo "主…

使用ENSP实现默认路由

一、项目拓扑 二、项目实现 1.路由器AR1配置 进入系统试图 sys将路由器命名为R1 sysname R1关闭信息中心 undo info-center enable 进入g0/0/0接口 int g0/0/0将g0/0/0接口IP地址配置为2.2.2.1/24 ip address 2.2.2.1 24进入g0/0/1接口 int g0/0/1将g0/0/1接口IP地址配置为1.…