【C++】继承(一)

news2024/11/18 22:58:12

目录

1、继承的概念与定义

1.1 继承的概念

1.2 继承的定义

1.2.1 定义格式

1.2.2 继承基类成员访问方式的变化

2、基类和派生类对象赋值转换

3、继承中的作用域

4、派生类的默认成员函数

4.1 构造函数

4.2 拷贝构造

4.3 operator=

4.4 析构函数

面向对象的三大特性是封装、继承和多态,前面我们已经学习了封装,回顾一下

第一层封装是将变量与方法封装到一起,成了类

第二层封装是迭代器和适配器

面向对象就是关注类和类之间的关系

1、继承的概念与定义

1.1 继承的概念

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

就比方说要实现一个外卖系统,那么这个外面系统需要有三个类,骑手、商家、顾客,而这三个类中会有一些共同的成员变量或成员函数,如姓名、地址、电话、上线下线函数等等。所以,可以将这些公共的封装成一个类,让骑手、商家、用户的类去继承。这个公共的类称为父类或基类,骑手、商家、顾客这三个类称为子类或派生类。

1.2 继承的定义

1.2.1 定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};
class Student : public Person
{
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
protected:
	int _jobid; // 工号
};

像上面这个例子中,学生和老师都是人,都会有姓名和年龄,所以可以用一个Person类来存放共有的成员(成员变量+成员函数),然后再让Student类和Teacher类去继承Person类

继承的本质就是复用

注意:使用上面3个类来创建对象时,当创建出来的对象调用基类中的函数Print时,调用的是同一个函数,因为函数不存在于对象中,而是存在公共代码段。但是创建出来的对象里面的成员变量是单独的

1.2.2 继承基类成员访问方式的变化

基类中的成员在派生类中的访问形式是什么样的呢?

1. 看最下面一行,即基类中的private的成员,在派生类中都是不可见的。不可见是指基类的私有成员虽然被继承到了派生类中,但是无论是在派生类内还是在派生类外,都是无法被访问的。虽然不可见,但是派生类中的的确确是有这些私有成员的。若要访问,可通过调用基类中的成员函数来访问。实际上,基类中的private成员就是不想被继承

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

3. 看前面两行,会发现在派生类中的访问方式 = Min(成员在基类的访问限定符,继承方式),其中public > protecte > private

4. 写派生类时,继承方式是可以不写的,class默认继承方式是private,struct默认继承方式是public。但是最好还是协商

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

2、基类和派生类对象赋值转换

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里称为基类与派生类的赋值兼容转换,简称为切片或者切割。寓意把派生类中父类那部分切来赋值过去。

基类对象不能赋值给派生类对象

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)。其实也就是说,只能将基类对象 = 派生类对象,不能 派生类对象 = 基类对象

void test_incise()
{
	Person p;
	Student s;
	p = s; // 将派生类对象赋值给基类对象
	Person* ptr = &s; // 将派生类对象的地址传给基类的指针
	Person& ref = s; // 让基类引用派生类对象
}

注意,在将派生类对象赋值给基类对象中,不是用派生类的对象s去产生一个基类临时对象,再将这个临时对象赋值给p,而是直接将s中成员变量的值赋值给p中的成员变量。否则也无法解释为什么引用不需要加const

指针只是单纯指向派生类对象中基类的那一部分

引用也是引用的是派生类对象中基类的那一部分,单纯只是这一部分的别名

3、继承中的作用域

目前我们已经学习过四种域,局部域、全局域、命名空间域、类域,域会影响两个东西,语言编译查找规则和生命周期

其中命名空间与和类域不影响生命周期

1. 在继承体系中基类和派生类都有独立的作用域

2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)。子类和父类之所以可以有同名成员,是因为不同域是可以有同名成员的
 

class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};
void test()
{
	Student s;
	s.Print();
}

结果是

所以,若是派生类中有和基类同名的成员变量,在派生类中直接访问,访问的是派生类中的成员变量,若在派生类中需要访问基类中同名的那个成员变量,则需要指定类域

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

class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}
};
void test()
{
	B b;
    b.fun(10);
    b.A::fun();
}

在上面这两个类当中,A中的成员函数fun和B中的成员函数fun就改成隐藏。注意,不构成函数重载,因为函数重载的前提是同一个作用域

与成员变量类似。若是派生类中有和基类同名的成员函数,派生类对象直接访问,访问的是派生类中的成员函数,若在派生类中需要访问基类中同名的那个成员变量,则需要指定类域

当派生类对象调用函数时,首先是去派生类的类域中找,没找到再到基类中找,还没找到就到全局域中找,还找不到就报错(一般查找都是最后到全局域中找)

上面是派生类的默认查找规则,无论是调用函数还是访问成员变量都是如此

class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}
};
void test()
{
	B b;
	b.fun();
}

上面这个程序为什么会编译报错呢?

在编译检查语法时,发现B类中确实有一个fun函数,但是这个函数需要参数,没传参数所以报错,因为在子类已经找到了,所以不会去父类中寻找。注意,这里是根据fun这个函数名来找的,不是根据符号表,符号表是在链接时的查找规则,所以会直接在派生类B中找到

4、派生类的默认成员函数

前面说过,继承的本质就是复用

class Person
{
public:
	Person(const char* name = "peter")
		: _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;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
    string _address; //地址
};

4.1 构造函数

首先我们来看看子类中的默认构造函数是怎么实现的

那要如何自己实现一个子类的构造函数呢?

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

子类的构造不能直接去动父类的成员变量,需要手动调用父类的构造

父类的构造子类仍然可以用,所以需不需要自己写子类的构造,完全看情况。当然,如果父类没有默认构造函数,即父类的构造函数需要传参,那么就必须显示写一个子类的构造函数。因为子类的默认构造函数只能调用父类的默认构造函数

注意:构造函数一定要在初始化列表中调用父类的构造函数,否则子类对象中父类的那部分已经初始化过了,再去函数体中调用父类的构造函数就是错误的,下面的拷贝构造是同理的,也就是下面这样是错误的

Student(const char* Name = "", int num = 0, const char* address = "")
{
	Person(Name); // 实际上此时子类对象中父类那一部分已经初始化过了
	_address = address;
	_num = num;
	cout << "Student()" << endl;
}

下面这样子是可以的

Student(const char* Name = "", int num = 0, const char* address = "")
	:Person(Name) 
{
	_address = address;
	_num = num;
	cout << "Student()" << endl;
}

4.2 拷贝构造

首先,我们来看看子类的默认拷贝构造是怎么实现的

那要如何自己实现一个子类的拷贝构造呢?

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

调用父类的拷贝构造需要将子类中父类的那一部分传给父类的拷贝构造函数,是将整个子类对象传过去,因为父类的拷贝构造函数会进行切割

所以在子类当中要调用父类的成员函数,并且这个成员函数还需要传一个父类对象时,直接将子类对象传过去即可,会自动切割,下面operator=中也有应用

拷贝构造只有当子类中设计到深拷贝才一定要自己实现,其他情况随意

4.3 operator=

默认的operator=与默认的拷贝构造函数是类似的,这里就不过多说明了

直接看如何自己实现一个

Student& operator=(const Student& st)
{
	if (this != &st)
	{
		operator=(st);
		_num = st._num;
		_address = st._address;
	}
	return *this;
}

按照上面拷贝构造的思路,很容易写成这样子,实际上,这是错的,会造成无穷递归

因为子类的operator=和父类的operator=构成隐藏,而此时在子类中并没有指明类域,所以会一直调用子类中的operator=,导致无穷递归

Student& operator=(const Student& st)
{
	if (this != &st)
	{
		Person::operator=(st);
		_num = st._num;
		_address = st._address;
	}
	return *this;
}

4.4 析构函数

首先,子类默认的析构是:

父类成员(整体) :调用父类的析构
子类自己的内置成员 :不处理
子类自己的自定义成员 :调用他的析构

自己实现:

此时发现会有报错,原因是,由于多态,析构函数的名字会被统一处理为destructor(),即~Person()和~Student()都被处理成destructor(),所以~Person()和~Student()构成隐藏,解决方法是指定类域

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

可这样为什么会调用两次父类的析构呢?

实际上,此时仍然是错的,因为父类析构不需要在子类中显示调用,子类的析构完成后,会自动调用父类的析构

那为什么前面3个可以显示调用父类相应的成员函数,析构就不行呢?

在子类的成员变量声明中,虽然看不见父类的部分,但是父类这部分是放在声明的最前面的。析构从后往前析构的,即先析构子类特有的成员变量,如何再调用父类的析构去析构掉子类从父类中继承的成员变量,因为在子类析构时,是有可能会去访问子类对象中从父类继承的那一部分成员变量的。而前面3个函数可以显示调用父类相应的成员函数,以构造函数为例。构造函数是从前往后构造的,因为子类中特有的那部分成员变量在构造时可能会利用子类中从父类继承下来的那部分成员变量来构造,所以需要从前往后构造。

构造函数是从前往后构造的,不过在构造函数的初始化列表中,不把Person(name)放在最前面也可以,因为初始化并不是按初始化列表走的,而是按照声明顺序走的

所以,析构函数可以写为:

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

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

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

相关文章

CAN转PN网关模块连接激光切割机的配置方法

激光切割机在工业生产中被广泛应用&#xff0c;而激光发射器与控制设备常以不同的协议存在两者之间&#xff0c;CAN总线和Profinet以各自的特点被广泛用于设备当中。本文将介绍介绍兴达易控CAN转Profinet网关模块&#xff08;XD-PN_CAN20&#xff09;连接 CAN 激光切割机的使用…

使用Python绘制双向条形图

使用Python绘制双向条形图 双向条形图效果代码 双向条形图 双向条形图用于比较两个类别的数值分布&#xff0c;条形在中轴线两侧对称排列。这种图表常用于显示两个变量的对比情况&#xff0c;例如男女不同年龄段人口数量对比。 效果 代码 import matplotlib.pyplot as plt i…

实现模型贴图的移动缩放旋转

技术&#xff1a;threejscanvasfabric 效果图&#xff1a; 原理&#xff1a;threejs中没有局部贴图的效果&#xff0c;只能通过map 的方式贴到模型上&#xff0c;所以说换一种方式来实现&#xff0c;通过canvasfabric来实现图片的移动缩放旋转&#xff0c;然后将整个画布以map…

APP项目测试 之 APP性能测试-- 性能测试工具(SoloPi工具)

1.SoloPi简介 &#xff08;1&#xff09;什么是SoloPi&#xff1f; SoloPi&#xff1a; 是一个无线化、非侵入式的 Android 自动化工具 &#xff0c;具备 录制回放、性能测试 等功能。 &#xff08;2&#xff09;SoloPi的作用是什么&#xff1f; 基础性能测试&#xff1a;能够…

STM32-I2C硬件外设

本博文建议与我上一篇I2C 通信协议​​​​​​共同理解 合成一套关于I2C软硬件体系 STM32内部集成了硬件I2C收发电路&#xff0c;可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能&#xff0c;减轻CPU的负担 特点&#xff1a; 多主机功能&#x…

[word] Word如何快速生成一段文本 #知识分享#学习方法

Word如何快速生成一段文本 Word如何快速生成一段文本&#xff1f;有时候我们会用一大段文字来做一些功能测试&#xff0c;不少朋友的做法就是脸滚键盘&#xff0c;一顿乱按&#xff0c;这样看起来文笔不通&#xff0c;看着也会比较难受&#xff0c;测试功能的效果也不怎么理想…

【全面讲解下Foxit Reader】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

微信小程序毕业设计-学生实习与就业管理系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

一款免费的PDF编辑软件,内置了OCR功能,识别效果好

主要是想分享给大家他的OCR功能&#xff0c;面对无法编辑的PDF或者图片&#xff0c;如何批量的转成文字或者带有格式的word文档&#xff0c;很多时候或者很多工具做的不理想&#xff0c;今天分享的这款工具应该是目前为止&#xff0c;我遇到的最好的批量OCR工具。他不是简单的O…

spring boot实现短信验证码功能

1、到阿里云网站申请 https://market.aliyun.com/products/5700000 2/cmapi00046920.html2、配置文件&#xff0c;可申请测试 sms:app-code: xxxxxxxxxtemplate-id: xxxxxxx3、使用restTemplate用于第三方接口调用 package com.example.rsocketclient.config;import org.spr…

笔记14:程序中的循环结构

生活中的循环现象&#xff1a; -日复一日&#xff0c;年复一年 -春夏秋冬&#xff0c;四季交替 -周日&#xff0c;周一&#xff0c;周二&#xff0c;周三&#xff0c;周四&#xff0c;周五&#xff0c;周六 -人生是一个轮回&#xff0c;多年后&#xff0c;又会回到最初的原点 …

Python学习从0开始——Kaggle实践可视化001

Python学习从0开始——Kaggle实践可视化001 一、创建和加载数据集二、数据预处理1.按name检查&#xff0c;处理重复值&#xff08;查重&#xff09;2.查看存在缺失值的列并处理&#xff08;缺失值处理&#xff09;2.1按行或列查看2.2无法推测的数据2.3可由其它列推测的数据 3.拆…

大数据Spark 面经

1: Spark 整体架构 Spark 是新一代的大数据处理引擎&#xff0c;支持批处理和流处理&#xff0c;也还支持各种机器学习和图计算&#xff0c;它就是一个Master-worker 架构&#xff0c;所以整个的架构就如下所示&#xff1a; 2: Spark 任务提交命令 一般我们使用shell 命令提…

【HICE】web服务搭建之仓库

1.首先将1.conf变成vhost&#xff0c;从而使监听号只有最普通的&#xff0c;并且进行更新。 2.挂载 mount /dev/sr0 /var/www/html 3.更改本地仓库路径 4.测试&#xff1a;下载软件包&#xff0c;在删除 5.删除软件包在取消挂载&#xff0c;在下载软件包失败

计算机网络-IP组播基础

一、概述 在前面的学习交换机和路由协议&#xff0c;二层通信是数据链路层间通信&#xff0c;在同一个广播域间通过源MAC地址和目的MAC地址进行通信&#xff0c;当两台主机第一次通信由于不清楚目的MAC地址需要进行广播泛洪&#xff0c;目的主机回复自身MAC地址&#xff0c;然后…

C++:this指针到底是什么东西

一、this指针概述 在C中&#xff0c;this是一个隐含的指针&#xff0c;它指向当前正在被调用的函数的对象实例。当你在一个成员函数内部引用self, me, 或者是无名的"this"时&#xff0c;实际上是访问了这个特殊的变量。this通常用于区分函数参数和局部变量&#xff0…

linux驱动编程 - kfifo先进先出队列

简介&#xff1a; kfifo是Linux Kernel里面的一个 FIFO&#xff08;先进先出&#xff09;数据结构&#xff0c;它采用环形循环队列的数据结构来实现&#xff0c;提供一个无边界的字节流服务&#xff0c;并且使用并行无锁编程技术&#xff0c;即当它用于只有一个入队线程和一个出…

机器学习筑基篇,​Ubuntu 24.04 编译安装 Python 及多版本切换

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] Ubuntu 24.04 编译安装最新Python及多版本切换 描述&#xff1a;说到机器学习&#xff0c;人工智能&#xff0c;深度学习不免会提到Python这一门编程语言&#xff08;人生苦短&#xff0c;及时Pyt…

Redis的zset的zrem命令可以做到O(1)吗?

事情是这样的&#xff0c;当我用zrem命令去移除value的时候&#xff0c;我知道他之前会做的几个步骤 1、查找这个value对应的score&#xff08;通过zset中的dict&#xff09;2、根据这个score查找到跳表中的节点3、删除这个节点 我就想了一下为什么dict为什么要保存score呢&a…

Caffeinated for Mac v2.0.6 Mac防休眠应用 兼容 M1/M2/M3

Caffeinated 可以防止您的 Mac 进入休眠状态、屏幕变暗或者启动屏幕保护。 应用介绍 您的屏幕是否总是在您不希望的时候变暗&#xff1f;那么Caffeinated就是您解决这个大麻烦的最好工具啦。Caffeinated是在Caffeine这个非常便捷、有用的工具的基础上开发而来的。Caffeinated…