【C++】继承(二)深入理解继承:派生类默认成员函数与友元、静态成员的奥秘

news2024/12/25 12:25:34

目录

  • 派生类的默认成员函数
    • ①派生类的构造函数
    • ②派生类的拷贝构造函数
    • ③派生类的赋值构造
    • ④派生类的析构函数
  • 继承与友元
  • 继承与静态成员

前言

我们在上一章讲解了: 继承三部曲,本篇基于上次的基础继续深入了解继承的相关知识,欢迎大家和我一起学习继承

派生类的默认成员函数

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

①派生类的构造函数

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

1.1、有默认构造的情况:

class Person
{
public:
	Person(const char* name="hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
protected:
	int _stuid;
};
int main()
{
	Student s;
	return 0;
}

在有默认构造的情况下,Student s,创建的派生类s对象会自动调用自己的默认构造,它里面的内置类型_stuid不做处理,但是继承父类里面的_name会被当做一个Person类的对象,也就是自定义类型成员,_name会调用Person的默认构造来初始化自己

1.2、没有默认构造的情况:

这个基类我们没有写无参默认构造,但是我们写了带参的默认构造,所以编译器不会为我们生成默认无参的构造函数

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person() " << endl;
	}
protected:
	string _name;
};

在派生类中如何初始化成员?
下面是❌示范

class Student:public Person
{
public:
	Student(int stuid=1001,const char* name="peter")
		:_name(name)//这里这样写会直接报错,有红色波浪线的那种,只是这里看不出来
		,_stuid(stuid)
	{
		cout << "Student()" << endl;
	}
protected:
	int _stuid;
};

我们不能够直接拿父类的成员出来单独进行初始化,父类的初始化要看作是一个整体,可以理解为父类的成员是隐藏在子类中的自定义类型成员,然而在初始化时,自定义类型需要走它的默认构造,即使我们不在初始化列表显示写,它也会走初始化列表,然而这里我们没有默认的Person构造函数,所以我们需要显示调用这个构造,我们看下面的正确写法

✔写法
我们显示调用Person类的构造来初始化从Person那边继承过来的成员变量就行了,这里就充分体现了父类的初始化要看成是一个整体

class Student:public Person
{
public:
	Student(int stuid=1001,const  char* name="peter")
		:Person(name)//如果这个构造函数有多个参数,那我们就传多个参数,看具体构造函数来传参
		,_stuid(stuid)
	{
		cout << "Student()" << endl;
	}
protected:
	//Person _p;//父类的成员就好似这样,需要我们走Person的构造,不能单独初始化里面的成员
	int _stuid;
};

当然除了上面这种写法我们还可以去父类自己写一个无参默认构造,这里我就不做演示了,如果不会可以评论,我再进行补充✍

总结:

派生类的初始化=父类+自己(内置类型和自定义类型),父类调用父类的构造函数初始化自己(这里体现了复用),在派生类中,要把父类成员当成一个整体的自定义类型成员,子类的其他成员和以前一样(对内置类型不做处理,对自定义类型去调它的默认构造)

形象的理解一下:父类是一个整体的概念

class BB
{
public:
	BB(int num,const char* name)
		:_p(name)//会在初始化列表调用Person的构造函数来初始name
		,_num(num)
	{}
private:
	Person _p;//这里显示有Person的对象
	int _num;
};

②派生类的拷贝构造函数

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

以下面这个父类来举例:

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
	Person(const Person& p)//拷贝构造
		:_name(p._name)
	{}
protected:
	string _name;
};

2.1、子类中不显示写拷贝构造,就使用编译器默认生成的拷贝构造

class Student :public Person
{
public:
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
protected:
	int _num;
};
int main()
{
	Student s(1002, "okk");
	Student s1(s);
	return 0;
}

在实现用s拷贝s1时,派生类的拷贝构造和上面我们所说的默认构造有异曲同工之妙,他们都把父类成员当成一个整体的自定义类型成员,在走拷贝构造时,会去调用自定义类的拷贝构造

2.2、假如派生类需要写拷贝构造完成一些深拷贝,那我们要显示的写出拷贝构造,要怎么写父类的那一块呢?

class Student :public Person
{
public:
	Student(const Student& s)
		:_num(s._num)
		,Person(s)//显示调用基类的拷贝构造函数,用s来初始化Person部分 
	{}
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
protected:
	int _num;
};

另外这里我们要知道,当一个派生类(如Student)的对象被创建时,其基类(如Person)的部分会首先被初始化。这是对象构造过程的一部分,它确保基类部分在派生类部分之前处于有效状态

③派生类的赋值构造

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

基类

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{}
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

protected:
	string _name;
};

子类

class Student :public Person
{
public:
	Student(const Student& s)
		:_num(s._num)
		,Person(s)
	{}
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			//operator=(s);
			//在子类中这样调用父类中的赋值构造是不对的,他们函数名相同,会隐藏掉父类的operator=函数
			//这里如果这样写,会一直反复调用子类中的operator=,这样会栈溢出
			//如果想调到父类的operator=函数可以显示调用:Person::operator=(s);
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num;
};

④派生类的析构函数

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

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{}
	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
{
public:
	Student(const Student& s)
		:_num(s._num)
		,Person(s)
	{}
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		//~Person();//这里不能直接访问父类的析构函数,因为后续多态的需要,析构函数名字会被统一处理成destructor
		//所以这里析构也是被隐藏了,根本找不到这个析构名,所以直接报红色波浪线了
		//如果你想访问也可以,就显示调用他就好了
		Person::~Person();
		cout << "~Student()" << endl;
	}
Student s(1002, "okk");

如果我在子类析构函数中显示调用父类的析构函数,就会出问题1
在这里插入图片描述
这里我们父类的构造只构造了一次,却析构了两次,这样会造成不可预料的问题,所以我们就不该显示的写父类的析构函数

问题2:

~Student()
{
	Person::~Person();
	cout<<_name<<endl;//这里是父类的成员
	cout<<"~Student()"<<endl;
}

还有就是,如果我们先析构了父类,但是我们还需要用到父类的成员就会出现访问不到的情况,或者是其他不可预料的问题,在继承机制中,子类的析构函数通常会自动调用其父类的析构函数,所以父类的析构不需要我们显示写,不要画蛇添足

派生类对象析构清理先调用派生类析构再调基类的析构,要保证这个原则,所以我们不能显示调用父类的析构,将上面的子类析构函数改成:

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

总结:
派生类对象在初始化时:先父后子(如果你不信,可以调试看一下)
派生类对象在析构时:先子后父(这个就是继承机制的原因了)

继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Person
{
public:
 friend void Display(const Person& p, const Student& s);
protected:
 string _name; // 姓名
};
class Student : public Person
{
protected:
 int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
 cout << p._name << endl;
 //cout << s._stuNum << endl;Display()函数是父类的友元,并不是子类的友元,不能访问子类的私有或者保护
}

如果你需要访问子类和父类的私有成员和保护成员,那你可以让这个函数即是父类的友元,也是子类的友元,一个函数可以同时是多个类的友元

继承与静态成员

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

注意:静态成员是属于类本身的,而不是类的实例(对象)的

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。---统计Person及其Person对象一共产生了多少个
};
int Person::_count = 0;
//注意静态成员要在外面定义,定义的时候才会为他开空间
//由于静态成员是属于类本身的,而不是类的任何实例,所以它们需要有一个唯一的存储空间
//类外的定义确保了这一点

class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
int main()
{
	//_count 静态成员只有一份,当前类和它的派生类共用一个
	//父类静态成员属于当前类,也属于当前类的所有派生类
	cout << &(Person::_count) << endl;
	cout << &(Student::_count) << endl;
	cout << &(Graduate::_count) << endl;
	return 0;
}

由于Student类继承了Person类,所以他可以使用这个静态_count成员,至于Graduate 类他继承的是Student类,但是Student类继承了Person类,所以Graduate也可以使用_count成员,上面分别是从这三个类中找到_count对象并取出它的地址,打印出来我们会发现这是同一个地址,这就更验证了 父类静态成员属于当前类,也属于当前类的所有派生类

有了这个特性之后,我们可以用他来求父类在一个程序中总共创建了多少个对象,在构造函数里面加上_count++就可以统计出该程序从运行到结束一共创建了多少个对象,如果只想知道现在还存在的对象一共有多少个,就可以在析构函数里面写上_count--


本篇暂且先到这里,我们下篇见✋

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

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

相关文章

Python小游戏——打砖块

文章目录 打砖块游戏项目介绍及实现项目介绍环境配置代码设计思路代码设计详细过程 难点分析源代码代码效果 打砖块游戏项目介绍及实现 项目介绍 打砖块游戏是一款经典的街机游戏&#xff0c;通过控制挡板来反弹小球打碎屏幕上的砖块。该项目使用Python语言和Pygame库进行实现…

牛客NC392 参加会议的最大数目【中等 贪心+小顶堆 Java/Go/PHP 力扣1353】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/4d3151698e33454f98bce1284e553651 https://leetcode.cn/problems/maximum-number-of-events-that-can-be-attended/description/ 思路 贪心优先级队列Java代码 import java.util.*;public class Solution {/**…

纽曼新品X1000:轻巧便携仅重9.9公斤的1度电应急电源

在户外救援行动和应急设备中&#xff0c;电力供应的稳定性和安全性直接影响到救援工作的效率和成功率。在现代救援工作中&#xff0c;常见的光学声波探测仪、通信联络设备、气象检测仪、生命探测仪、照明设备等装备均需有持续的电力供应&#xff0c;才能保障救援工作的有序开展…

一文带你搞懂DiT(Diffusion Transformer)

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备面试攻略、面试常考点等热门话题进行了深入的讨论。 总结链接…

Redis 源码学习记录:集合 (set)

无序集合 Redis 源码版本&#xff1a;Redis-6.0.9&#xff0c;本篇文章无序集合的代码均在 intset.h / intset.c 文件中。 Redis 通常使用字典结构保存用户集合数据&#xff0c;字典键存储集合元素&#xff0c;字典值为空。如果一个集合全是整数&#xff0c;则使用字典国语浪费…

java图书电子商务网站的设计与实现源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的图书电子商务网站的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 图书电子商…

pikachu-Unsafe Filedownload

任意点击一个图片进行下载&#xff0c;发现下载的url。 http://127.0.0.1/pikachu/vul/unsafedownload/execdownload.php?filenamekb.png 构造payload&#xff1a; 即可下载 当前页面的源码&#xff0c;可以进行路径穿越来下载一些重要的配置文件来获取信息。 http://127.0.…

《书生·浦语大模型实战营》第一课 学习笔记:书生·浦语大模型全链路开源体系

文章大纲 1. 简介与背景智能聊天机器人与大语言模型目前的开源智能聊天机器人与云上运行模式 2. InternLM2 大模型 简介3. 视频笔记&#xff1a;书生浦语大模型全链路开源体系内容要点从模型到应用典型流程全链路开源体系 4. 论文笔记:InternLM2 Technical Report简介软硬件基础…

光电直读抄表技术详细说明

1.技术简述 光电直读抄表是一种智能化智能计量技术&#xff0c;主要是通过成像原理立即载入电度表里的标值&#xff0c;不用人工干预&#xff0c;大大提升了抄表效率数据可靠性。此项技术是智慧能源不可或缺的一部分&#xff0c;为电力公司的经营管理提供了有力的适用。 2.原…

在winnas中使用docker desktop遇到的问题及解决方法记录

最近在尝试从群晖转向winnas&#xff0c;一些简单的服务依然计划使用docker来部署。群晖的docker简单易用且稳定&#xff0c;在win上使用docker desktop过程中遇到了不少问题&#xff0c;在此记录一下以供后来人参考。 一、安装docker desktop后启动时遇到无法启动docker引擎 …

VMware虚拟机开机卡在Boot Manager

问题情况 虚拟机启动停留在Boot Manager 解决办法1 解决办法2 1、关闭虚拟机&#xff0c;并将其移除 2、找到虚拟机储存位置清除储存数据 3、使用360清除残留数据 4、重启VMware&#xff0c;重新创建虚拟机 关键词&#xff1a; BIOS 蓝色界面

超级初始网络

目录 一、网络发展史 1、独立模式 2、局域网 LAN&#xff08;Local Area Network&#xff09; 3、广域网 WAN (Wide Area Network) 二、网络通信基础 1、IP地址&#xff1a;用于定位主机的网络地址 2、端口号&#xff1a;用于定位主机中的进程 3、网络协议 4、五元组 …

GIT 新建分支和合并分支

文章目录 前言一、新建分支二、切回老分支&#xff0c;保留新分支的更改三、合并分支 前言 本文主要针对以下场景进行介绍&#xff1a; 场景一&#xff1a;创建新的分支 当前分支(dev_1)已经开发完毕&#xff0c;下一期的需求需要在新分支(dev_2)上进行开发&#xff0c;如何创…

Dubbo源码及总结

Springboot整合Dubbo启动解析Bean定义 根据springboot启动原理&#xff0c;会先把启动类下的所有类先进行解析bean定义&#xff0c;所以要先EnableDubbo这个注解&#xff0c;再根据这个注解里面的注解&#xff0c;可以知道import的两个类DubboComponentScanRegistrar和DubboCo…

嵌入式单片机寄存器操作与实现方法

大家好,今天给大家分享一下,单片机中寄存器该如何操作与实现。 “芯片里面的寄存器访问方式一般是: 1.可使用地址访问,2.可使用指令访问,3.不可访问” 第一:挂载到内存地址总线上了的 挂载到内存地址总线上了的,可以使用分配到的地址访问 如下是STM32单片机存储器映像…

hbase版本从1.2升级到2.1 spark读取hive数据写入hbase 批量写入类不存在问题

在hbase1.2版本中&#xff0c;pom.xml中引入hbase-server1.2…0和hbase-client1.2.0就已经可以有如下图的类。但是在hbase2.1.0版本中增加这两个不行。hbase-server2.1.0中没有mapred包&#xff0c;同时mapreduce下就2个类。版本已经不支持。 <dependency><groupId>…

3d全景电商网站搭建为用户的生产力、想象力和创造力插上腾飞的翅膀

为解决用户搭建3D电商网站制作费用高、难度大的困扰&#xff0c;华锐视点隆重推出全新3D电商网站制作编辑器&#xff0c;以全新的设计思维、交互范式和编辑工具&#xff0c;打破传统3D设计的专业界限&#xff0c;为用户的生产力、想象力和创造力插上腾飞的翅膀! 这款创新的3D电…

PGP软件安装文件加密解密签名实践记录

文章目录 环境说明PGP软件安装PGP软件汉化AB电脑新建密钥并互换密钥对称密钥并互换密钥 文件加密和解密A电脑加密B电脑解密 文件签名A电脑签名文件B电脑校验文件修改文件内容校验失败修改文件名称正常校验 环境说明 使用VM虚拟两个win11,进行操作演示 PGP软件安装 PGP软件下…

STM32 CubeMX使用记录

取消DMA中断默认使能 DMA中断默认使能勾选无法取消选中 取消勾选Force DMA channels interrupts

算法入门----小话算法(1)

下面就首先从一些数学问题入手。 Q1&#xff1a; 如何证明时间复杂度O(logN) < O(N) < O(NlogN) < O(N2) < O(2N) < O(N!) < O(NN)? A&#xff1a; 如果一个以整数为参数的不等式不能很容易看出不等的关系&#xff0c;那么最好用图示或者数学归纳法。 很显…