当前位置: 首页 > news >正文

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

目录

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

前言

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

派生类的默认成员函数

在这里插入图片描述
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--


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

相关文章:

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

目录 派生类的默认成员函数①派生类的构造函数②派生类的拷贝构造函数③派生类的赋值构造④派生类的析构函数 继承与友元继承与静态成员 前言 我们在上一章讲解了: 继承三部曲&#xff0c;本篇基于上次的基础继续深入了解继承的相关知识&#xff0c;欢迎大家和我一起学习继承 派…...

【MATLAB源码-第214期】基于matlab的遗传算法GA最短路径路由优化算法仿真。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 在现代网络通信和路径规划领域&#xff0c;最短路径路由优化算法是一项关键技术。它涉及在给定的网络拓扑中寻找从源点到目标点的最短或成本最低的路径。近年来&#xff0c;遗传算法&#xff08;GA&#xff09;因其出色的全局…...

数据结构(四)顺序栈 链式栈

一、概念 栈是一种先进后出的数据结构。FILO(firt in late out) 逻辑结构&#xff1a;线性结构 二、存储结构&#xff1a; &#xff08;一&#xff09; 顺序存储 顺序栈 基于一个数组配合一个栈顶"指针&#xff08;数组下标&#xff09;–top" 顺序栈的本质就是对…...

【linux】g++/gcc编译器

目录 背景知识 gcc如何完成 预处理(进行宏替换) 编译&#xff08;生成汇编&#xff09; 汇编&#xff08;生成机器可识别代码&#xff09; 链接&#xff08;生成可执行文件或库文件&#xff09; 在这里涉及到一个重要的概念:函数库 函数库一般分为静态库和动态库两…...

VBA批量合并带有图片、表格与文本框的Word

本文介绍基于VBA语言&#xff0c;对大量含有图片、文本框与表格的Word文档加以批量自动合并&#xff0c;并在每一次合并时添加分页符的方法。 在我们之前的文章基于Python中docx与docxcompose批量合并多个Word文档文件并逐一添加分页符&#xff08;https://blog.csdn.net/zhebu…...

市面上前 11 名的 Android 数据恢复软件

Android数据恢复软件是恢复无意中删除的文件或文件夹的必要工具。该软件还将帮助您恢复丢失或损坏的信息。本文介绍提供数据备份和磁盘克隆选项的程序&#xff0c;这些选项有助于在Android设备上恢复文件的过程。 如果您正在寻找一种有效的方法来恢复图像&#xff0c;文档&…...

【数据结构与算法 | 基础篇】数组模拟栈

1. 前言 前文我们刚提及了如何用单向链表来模拟栈. 我们还可以用数组来模拟栈.使用栈顶指针top来进行栈顶的操作. 2. 数组模拟栈 (1). 栈接口 public interface stack<E> {//压栈boolean push(E value);//弹栈, 栈非空返回栈顶元素E pop();//返回栈顶元素, 但不弹栈E…...

css卡片横线100%宽度

所需样式: 横线不用border, 用单独一个div, 这样就不会影响父组件的padding <div class"pumpDetailView"><div class"pump_title_name"><span>{{ pumpInfo.pointname }}</span><divclass"point_state":style"…...

回溯大法总结

前言 本篇博客将分两步来进行&#xff0c;首先谈谈我对回溯法的理解&#xff0c;然后通过若干道题来进行讲解&#xff0c;最后总结 对回溯法的理解 回溯法可以看做蛮力法的升级版&#xff0c;它在解决问题时的每一步都尝试所有可能的选项&#xff0c;最终找出所以可行的方案…...

基于Android Studio图书管理,图书借阅系统

目录 项目介绍 图片展示 运行环境 获取方式 项目介绍 用户 书架&#xff1a;搜索书籍&#xff0c;查看书籍&#xff0c;借阅书籍&#xff0c;收藏书籍&#xff0c;借阅书籍必须在一个月之内还书&#xff1b; 我的&#xff1a;可以修改密码&#xff0c;退出登录&#xff…...

SCSS 基本使用详解

一、引言 SCSS 是 Sass&#xff08;Syntactically Awesome Stylesheets&#xff09;的其中一种语法&#xff0c;是一种预处理器脚本语言&#xff0c;能够扩展 CSS 的功能&#xff0c;使其更加强大和高效。SCSS 保留了 CSS 的原有语法&#xff0c;同时增加了变量、嵌套规则、混…...

10.3.k8s的附加组件-图形化管理工具dashboard

目录 一、dashboard介绍 二、部署安装dashboard组件 1.下载dashboard本地文件 2.修改nodeport的端口范围 3.创建和查看dashboard 4.电脑浏览器访问测试 5.token登录方式登录dashboard 5.1.查看dashboard的token 5.2.继续查看用户token的secrets资源详细信息 5.3.复制…...

深入理解CPU缓存一致性

存储体系结构 速度快的存储硬件成本高、容量小&#xff0c;速度慢的成本低、容量大。为了权衡成本和速度&#xff0c;计算机存储分了很多层次&#xff0c;有寄存器、L1 cache、L2 cache、L3 cache、主存&#xff08;内存&#xff09;和硬盘等。 根据程序的空间局部性和时间局…...

python获取安装路径盘符

文章目录 一、前言二、实现方法一、前言 python写的客户端工具需要安装时,可以给用户一个默认的安装路径,如果直接写死个D、E、F盘什么的,那用户可能没有那个盘符,但是如果直接指定系统盘C盘,又不是那么友好,所以默认指定的安装路径应该尽量满足下面的要求: 盘符存在盘…...

CentOS 7.9安装NVIDIA P40显卡驱动、CUDA和cuDNN

文章目录 1、安装P40显卡驱动1.1 查看机器上有哪些显卡1.2 禁用nouveau1.3 安装依赖1.4 安装驱动 2、安装CUDA2.1 安装2.2 测试是否安装成功 3、安装cuDNN3.1 安装3.2 测试是否安装成功 4、总结 1、安装P40显卡驱动 1.1 查看机器上有哪些显卡 lspci | grep -i vga lspci | gr…...

SpringBoot多数据源启动出现循环依赖问题

在使用SpringBoot的项目中&#xff0c;如果是有使用多数据源&#xff0c;可能会存在启动时数据源循环依赖的报错&#xff0c;是因为使用了多数据源注入&#xff0c;和DataSourceAutoConfiguration数据源自动配置的DataSourceInitializerInvoker互相产生循环依赖导致。 这种错误…...

【一步一步了解Java系列】:何为数组,何为引用类型

看到这句话的时候证明&#xff1a;此刻你我都在努力加油陌生人个人主页&#xff1a;Gu Gu Study专栏&#xff1a;一步一步了解Java 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 数组 数组是一推相同数据…...

2024年5月份最新独角数卡使用USDT详细小白教程

直观配套视频教程 2024年5月份最新独角数卡安装及USDT使用详细小白教程 1、创建服务器 Centos或者Ubuntu2、宝塔面板开心版安装寶塔 Linux 面版 8.0.5 開心版 - 2024年1月12日 - 开心专区 - 异次元 - Powered by Discuz!Centos安装命令&#xff08;默认安装是 8.0.1 直接在线升…...

【idea】idea2024最新版本下载_安装_破解

1、下载 下载地址&#xff1a;下载 IntelliJ IDEA – 领先的 Java 和 Kotlin IDE 下载完成&#xff1a; idea破解脚本下载链接&#xff1a;https://pan.baidu.com/s/1L5qq26cRABw8XuEn_CngKQ 提取码&#xff1a;6666 下载完成&#xff1a; 2、安装 1、双击idea的安装包&…...

部署CNI网络组件+k8s多master集群部署+负载均衡

一、环境部署 主机服务 192.168.91.5 K8S集群master01192.168.91.8 K8S集群master02192.168.91.6K8S集群node01192.168.91.7K8S集群node02192.168.91.9 负载均衡nginxkeepalive01&#xff08;master&#xff09;192.168.91.10 负载均衡nginxkeepalive02&#xff08;backup&am…...

一个和蔼可亲的Python库,用Gooey为你的程序添加GUI

大家好&#xff0c;你有没有遇到过这样的情况&#xff1a;你写了一个非常棒的命令行程序&#xff0c;但当你分享给朋友或同事时&#xff0c;他们却因为害怕命令行而不愿意使用&#xff1f;这时候&#xff0c;一个简洁美观的图形用户界面&#xff08;GUI&#xff09;就派上用场了…...

java抽象类,接口,枚举练习题

第一题&#xff1a; 答案&#xff1a; class Animal{//成员变量protected String name;protected int weight;//构造方法public Animal(){this.name"refer";this.weight50;}public Animal(String name,int weight){this.namename;this.weightweight;}//成员方法publ…...

探索Python技巧:零基础学习缩进与逻辑关系

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、理解Python的缩进语法 缩进规则详解 二、缩进在逻辑关系中的应用 逻辑块示例 三、实…...

【设计模式】JAVA Design Patterns——Callback(回调模式)

&#x1f50d;目的 回调是一部分被当为参数来传递给其他代码的可执行代码&#xff0c;接收方的代码可以在一些方便的时候来调用它。 &#x1f50d;解释 真实世界例子 我们需要被通知当执行的任务结束时。我们为调用者传递一个回调方法然后等它调用通知我们。 通俗描述 回调是一…...

Pandas高效数据清洗与转换技巧指南【数据预处理】

三、数据处理 1.合并数据&#xff08;join、merge、concat函数&#xff0c;append函数&#xff09; Concat()函数使用 1.concat操作可以将两个pandas表在垂直方向上进行粘合或者堆叠。 join属性为outer&#xff0c;或默认时&#xff0c;返回列名并集&#xff0c;如&#xff…...

kafka防止消息丢失配置

无消息丢失最佳实践配置&#xff1a; 不要使用 producer.send(msg)&#xff0c;而要使用 producer.send(msg, callback) API。设置 acks all。是 Producer 参数。设置成 all&#xff0c;表明所有副本 Broker 都要接收到消息&#xff0c;g该消息才算是“已提交”。设置 retrie…...

Socket CAN中ctrlmode有哪些?

在Linux中,socketcan 的 ctrlmode 是一个用于配置CAN设备控制模式的标志字段。该字段的值由一组标志位组成,这些标志位控制CAN设备的各种操作模式。以下是一些常见的 ctrlmode 标志及其含义: CAN_CTRLMODE_LOOPBACK: 描述:启用回环模式。作用:设备在发送帧的同时会接收它…...

find 几招在 Linux 中高效地查找目录

1. 介绍 在 Linux 操作系统中&#xff0c;查找目录是一项常见的任务。无论是系统管理员还是普通用户&#xff0c;都可能需要查找特定的目录以执行各种操作&#xff0c;如导航文件系统、备份数据、删除文件等。Linux 提供了多种命令和工具来帮助我们在文件系统中快速找到目标目…...

【ELK日志收集过程】

文章目录 为什么要使用ELK收集日志ELK具体应用场景ELK日志收集的流程 为什么要使用ELK收集日志 使用 ELK&#xff08;Elasticsearch, Logstash, Kibana&#xff09;进行日志收集和分析有多种原因。ELK 堆栈提供了强大、灵活且可扩展的工具集&#xff0c;能够满足现代 IT 系统对…...

设计模式—23种设计模式重点 表格梳理

设计模式的核心在于提供了相关的问题的解决方案&#xff0c;使得人们可以更加简单方便的复用成功的设计和体系结构。 按照设计模式的目的可以分为三大类。创建型模式与对象的创建有关&#xff1b;结构型模式处理类或对象的组合&#xff1b;行为型模式对类或对象怎样交互和怎样…...