【C++】类的默认成员函数(上)

在这里插入图片描述
🔥博客主页 小羊失眠啦.
🎥系列专栏《C语言》 《数据结构》 《C++》 《Linux》 《Cpolar》
❤️感谢大家点赞👍收藏⭐评论✍️


在这里插入图片描述

文章目录

  • 一、默认成员函数
  • 二、构造函数
    • 构造函数的概念及特性
  • 三、析构函数
    • 析构函数的特性
  • 四、拷贝构造函数
    • 拷贝构造函数的特性

一、默认成员函数

上一章中我们谈到,如果一个类中什么成员也没有,那么这个类就叫作空类。其实这么说是不太严谨的,因为一个类不可能什么都没有

当我们定义好一个类,不做任何处理时,编译器会自动生成以下6个默认成员函数

  • 默认成员函数:如果用户没有手动实现,则编译器会自动生成的成员函数。

在这里插入图片描述

  • 构造函数:主要完成初始化工作;
  • 析构函数:主要完成清理工作;
  • 拷贝构造:使用一个同类的对象初始化创建一个对象;
  • 赋值重载:把一个对象赋值给另一个对象;
  • 取地址重载普通对象取地址操作;
  • 取地址重载(const):const对象取地址操作;

本章我们将学习四个默认成员函数——构造函数析构函数——拷贝构造赋值重载


二、构造函数

在C语言阶段,我们实现的数据结构时,有一件事很苦恼,就是每当创建一个stack对象(之前叫作定义一个stack类型的变量)后,首先得调用它的专属初始化函数StackInit来初始化对象。

typedef int dataOfStackType;typedef struct stack
{dataOfStackType* a;int top;int capacity;
}stack;void StackInit(stack* ps);
//...int main(){stack s;StackInit(&s);//...return 0;}

这不免让人觉得有点麻烦。在C++中,构造函数为我们很好的解决了这一问题。

构造函数的概念及特性

构造函数是一个特殊的成员函数。构造函数虽然叫作构造,但是其主要作用并不是开辟空间创建对象,而是初始化对象

构造函数之所以特殊,是因为相比于其它成员函数,它具有如下特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用对应的构造函数
  4. 构造函数可以重载

举例

class Date
{
public://无参的构造函数Date(){};//带参的构造函数Date(int year,int month,int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};void TestDate()
{Date d1;//调用无参构造函数(自动调用)Date d2(2023, 3, 29);//调用带参构造函数(自动调用)
}

特别注意

  • 创建对象时编译器会自动调用构造函数,若是调用无参构造函数,则无需在对象后面使用()。否则会产生歧义:编译器无法确定你是在声明函数还是在创建对象

错误示例

//错位示例
Date d3();
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public://若用户没有显示定义,则编译器自动生成。/*Date(int year,int month,int day){_year = year;_month = month;_day = day;}*/private:int _year;int _month;int _day;
};
  1. 默认生成构造函数,对内置类型成员不作处理;对自定义类型成员,会调用它的默认构造函数
  • C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char、double…,自定义类型就是我们使用class、struct、union等自己定义的类型。

举例

默认构造函数对内置类型

class Date
{
public://此处不对构造函数做显示定义,测试默认构造函数/*Date(){}*/void print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
void TestDate1()
{Date d1;d1.print();
}

在这里插入图片描述

  • 如图所示,默认构造函数的确未对内置类型做处理。

默认构造函数对自定义类型

class stack
{
public://此处对stack构造函数做显示定义stack(){cout <<"stack()" << endl;_a = nullptr;_top = _capacity = 0;}
private:int* _a;int _top;int _capacity;
};class queue
{
public://此处不对queue构造函数做显示定义,测试默认构造函数/*queue(){}*/
private://自定义类型成员stack _s;
};void TestQueue()
{queue q;
}

在这里插入图片描述

  • 如图所示,在创建queue对象时,默认构造函数对自定义成员_s做了处理,调用了它的默认构造函数stack()

这一波蜜汁操作让很多C++使用者感到困惑与不满,为什么要针对内置类型和自定义类型做不同的处理呢?终于,在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:

  1. 内置类型成员变量在类中声明时可以给默认值

举例

class Date
{
public:
//...void print(){cout << _year << "-" << _month << "-" << _day << endl;}
private://使用默认值int _year = 0;int _month = 0;int _day = 0;
};
void TestDate2()
{Date d2;d2.print();
}

在这里插入图片描述

  • 默认值:若不对成员变量做处理,则使用默认值。
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

举例

class Date
{
public://无参的默认构造函数//Date()//{//}//全缺省的默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}void print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year = 0;int _month = 0;int _day = 0;
};

默认构造函数:

  1. 无参的构造函数
  2. 全缺省的构造函数
  3. C++编译器生成的无参的构造函数

即三种必须要有一种,如果没有默认的构造函数(写的构造函数不是无参的,也不是全缺省的)就会报错


三、析构函数

析构函数构造函数的特性相似,但功能有恰好相反。构造函数是用来初始化对象的,析构函数是用来销毁对象的。

  • 需要注意的是,析构函数并不是对对象本身进行销毁(因为局部对象出了作用域会自行销毁,由编译器来完成),而是在对象销毁时会自动调用析构函数,对对象内部的资源做清理(例如stack _s中的int* a)。

同样,有了析构函数,我们再也不用担心创建对象(或定义变量)后由于忘记释放内存而造成内存泄漏了。

举例

class Stack
{
public:Stack(){//...}void Push(int x){//...}bool Empty(){// ...}int Top(){//...}void Destory(){//...}
private:// 成员变量int* _a;int _top;int _capacity;
};void TestStack()
{Stack s;st.Push(1);st.Push(2);//过去需要手动释放st.Destroy();
}

析构函数的特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数
  3. 无返回值
  4. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  5. 析构函数不能重载

举例

class Date
{
public:Date(){cout << "Date()" << endl;}~Date(){cout << "~Date()" << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate3()
{Date d3;//d3生命周期结束时自动调用构造函数
}

在这里插入图片描述

  1. 编译器生成的默认析构函数,对自定类型成员调用它的析构函数

举例

class stack
{
public://此处对stack构造函数做显示定义stack(){cout <<"stack()" << endl;_a = nullptr;_top = _capacity = 0;}~stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:int* _a;int _top;int _capacity;
};
class queue
{
public://此处不对queue构造函数做显示定义,测试默认构造函数/*queue(){}*/
private://自定义类型成员stack _s;
};void TestQueue1()
{queue q;
}

在这里插入图片描述

  • 这里可能有小伙伴会好奇:为什么析构函数不像构造函数那样区分内置类型与自定义类型呢
    答案是:因为内置类型压根不需要我们担心清理工作,在其生命周期结束时会自动销毁。而自定义类型需要担心,因为自定义类型里可能含有申请资源(例如:malloc申请内存须手动释放)。
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如stack类。

四、拷贝构造函数

同样,拷贝构造函数也属于6个默认成员函数,而且拷贝构造函数构造函数的一种重载形式

  • 拷贝构造函数的功能就如同它的名字——拷贝。我们可以用一个已存在的对象来创建一个与已存在对象一模一样的新的对象

举例

class Date
{
public://构造函数Date(){cout << "Date()" << endl;}//拷贝构造函数Date(const Date& d){cout << "Date()" << endl;_year = d._year;_month = d._month;_day = d._day;}//析构函数~Date(){cout << "~Date()" << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate()
{Date d1;//调用拷贝构造创建对象Date d2(d1);
}

在这里插入图片描述

拷贝构造函数的特性

拷贝构造函数作为特殊的成员函数同样也有异于常人的特性:

  1. 拷贝构造函数是构造函数的重载
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用。若使用传值的方式,则编译器会报错,因为理论上这会引发无穷递归

错误示例

class Date
{
public://错误示例//如果这样写,编译器就会直接报错,但我们现在假设如果编译器不会检查,//这样的程序执行起来会发生什么Date(const Date d){_year = d._year;_month = d._month;_day = d._day;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate()
{Date d1;//调用拷贝构造创建对象Date d2(d1);
}
  • 当拷贝构造函数的参数采用传值的方式时,创建对象d2,会调用它的拷贝构造函数d1会作为实参传递给形参d。不巧的是,实参传递给形参本身又是一个拷贝,会再次调用形参的拷贝构造函数…如此便会引发无穷的递归。

在这里插入图片描述

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或者值拷贝

举例

class Date
{
public://构造函数Date(int year = 0, int month = 0, int day = 0){//cout << "Date()" << endl;_year = year;_month = month;_day = day;}//未显式定义拷贝构造函数/*Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}*/void print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate()
{Date d1(2023, 3, 31);//调用拷贝构造创建对象Date d2(d1);d2.print();
}

在这里插入图片描述

  • 有的小伙伴可能会有疑问:编译器默认生成的拷贝构造函数貌似可以很好的完成任务,那么还需要我们手动来实现吗?
    答案是:当然需要。Date类只是一个较为简单的类且类成员都是内置类型,可以不需要。但是当类中含有自定义类型时,编译器可就办不了事儿了。
  1. 类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

错误示例

class stack
{
public:stack(int defaultCapacity=10){_a = (int*)malloc(sizeof(int)*defaultCapacity);if (_a == nullptr){perror("malloc fail");exit(-1);}_top =  0;_capacity = defaultCapacity;}~stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}void push(int n){_a[_top++] = n;}void print(){for (int i = 0; i < _top; i++){cout << _a[i] << " ";}cout << endl;}
private:int* _a;int _top;int _capacity;
};void TestStack()
{stack s1;s1.push(1);s1.push(2);s1.push(3);s1.push(4);s1.print();stack s2(s1);s2.print();s2.push(5);s2.push(6);s2.push(7);s2.push(8);s2.print();
}

在这里插入图片描述

如图所示,这段程序的运行结果是程序崩溃了,且通过观察发现,是在第二次析构时出现了错误。其实出现错误的原因是在第二次析构时对野指针进行free了。

一个小tip

  • 多个对象进行析构的顺序如同一样,先创建的对象后析构,后创建的对象先析构

为什么会出现对野指针进行free呢?

  • 原因是,对象s1与对象s2中的成员_a,指向的是同一块空间。在s2析构完成后,这块空间已经被释放,此时的s1._a就是野指针。这就是浅拷贝导致的后果。

理解浅拷贝

编译器默认生成的拷贝构造函数是按字节序拷贝的,在创建s2对象时,仅仅是把s1._a的值赋值给s2._a并没有重新开辟一块与s1._a所指向的空间大小相同内容相同的空间。我们把前者的拷贝方式称为浅拷贝后者称为深拷贝

在这里插入图片描述

当开启监视窗口来观察这一过程,我们可以看到s2在进行push时,s1的内容也在跟着改变,且s1._a=s2._a

在这里插入图片描述

正确的做法

class stack
{
public:stack(int defaultCapacity=10){_a = (int*)malloc(sizeof(int)*defaultCapacity);if (_a == nullptr){perror("malloc fail");exit(-1);}_top =  0;_capacity = defaultCapacity;}//用户自己定义拷贝构造函数stack(const stack& s){_a= (int*)malloc(sizeof(int) * s._capacity);if (_a == nullptr){perror("malloc fail");exit(-1);}memcpy(_a, s._a, sizeof(int) * s._capacity);_top = s._top;_capacity = s._capacity;}~stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}void push(int n){_a[_top++] = n;}void print(){for (int i = 0; i < _top; i++){cout << _a[i] << " ";}cout << endl;}
private:int* _a;int _top;int _capacity;
};
  1. 拷贝构造函数典型调用场景
  • 使用已存在对象创建新对象;
  • 函数参数类型为类类型对象;
  • 函数返回值类型为类类型对象。

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

在这里插入图片描述

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

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

相关文章

蓝桥杯倒计时 43天 - 前缀和,单调栈

最大数组和 算法思路&#xff1a;利用前缀和化简 for 循环将 n^2 简化成 nn&#xff0c;以空间换时间。枚举每个 m&#xff0c;m是删除最小两个数&#xff0c;那k-m就是删除最大数&#xff0c;m<k&#xff0c;求和最大的值。暴力就是枚举 m-O(n)&#xff0c;计算前 n-(k-m)的…

PCSA时钟控制集成之时钟门控级别

这一部分描述了&#xff1a; • 时钟门控的级别。 • 实现最大效果的时钟门位置。 • 实现有效和高效时钟门控的集成方法。 时钟树是由时钟缓冲器构建的&#xff0c;这些缓冲器在时钟源&#xff08;时钟输入或PLL&#xff09;与时钟终端&#xff08;寄存器或RAM&#xff09…

ULN2003(COM的作用)

单路内部电路原理图 三极管多级放大电路&#xff0c;最大可达到500ma&#xff1b; 典型应用&#xff1a; ULN2003属于灌电流驱动方式&#xff1b;输入与输出电平反向&#xff0c;下拉4K电阻&#xff0c;为解决单片机上电IO瞬间不稳定&#xff1b; COM端 1.可悬空&#xff1a…

git标签操作

一.标签管理 1.理解标签 标签 tag &#xff0c;可以简单的理解为是对某次 commit 的⼀个标识&#xff0c;相当于起了⼀个别名,当我们需要回退到某个重要版本时&#xff0c;直接使⽤标签就能很快定位到 2.创建标签 ⾸先&#xff0c;切换到需要打标签的分⽀上,然后&#xf…

经典目标检测网络Yolo——原理部分

目标检测问题 分为两个子问题: 找到图片中哪些位置、哪些区域含有目标对象识别这些区域中的目标对象是什么基于CNN的目标检测算法能够很好的解决第二个问题,在一张图片仅含一个对象,且该对象占据了整张图片绝大部分面积时,基于CNN的对象识别算法具有很高的准确率。 一种定…

操作系统(1)——学习导论(Ⅱ)

目录 小程一言专栏链接: [link](http://t.csdnimg.cn/6grrU) 学习导论&#xff08;Ⅱ&#xff09;操作系统-赏前人佳作大型操作系统大型操作系统的一些特点和功能举例 服务器操作系统服务器操作系统特点和功能举例 多处理器操作系统举例 个人计算机操作系统举例 掌上计算机操作…

设计模式:什么是设计模式?①

一、什么是设计模式&#xff1f; 1. 是一类程序设计思想 2. 是在大量实践过程中摸索总结出的标准经验提炼 3. 具有多样性和丰富性&#xff0c;不同情况应用的思想不同 二、设计模式的好处 1. 代码生产力和效率的提升 2. 让代码表现更为规整&#xff0c;简洁。阅读维护管理的成本…

【竞技宝】DOTA2-梦幻联赛S22:AR命悬一线 XG确定晋级淘汰赛

北京时间2024年2月28日&#xff0c;DOTA2梦幻联赛S22的比赛在昨日进入小组赛第三个比赛日&#xff0c;本次梦幻联赛共有AR、XG、IG三支中国区的队伍参赛&#xff0c;那么经过三日激烈的比赛之后&#xff0c;目前三支队伍的积分情况以及晋级形势如何呢&#xff1f; XG XG是小组…

贪心(基础算法)--- 区间选点

905. 区间选点 思路 &#xff08;贪心&#xff09;O(nlogn) 根据右端点排序 将区间按右端点排序 遍历区间&#xff0c;如果当前区间左端点不包含在前一个区间中&#xff0c;则选取新区间&#xff0c;所选点个数加1&#xff0c;更新当前区间右端点。如果包含&#xff0c;则跳…

常见的算法

查找算法 基本查找 package MyApi.search;public class a01BasicSearchdemo01 {public static void main(String[] args) { int[] arr{131,127,147,81,103,23,7,79}; int number82;System.out.println(BasicSearch(arr,number));}public static boolean BasicSearch(int[] ar…

Java二叉树(1)

&#x1f435;本篇文章将对二叉树的相关概念、性质和遍历等知识进行讲解 一、什么是树 在讲二叉树之前&#xff0c;先了解一下什么是树&#xff1a;树是一种非线性结构&#xff0c;其由许多节点和子节点组成&#xff0c;整体形状如一颗倒挂的树&#xff0c;比如下图&#xff1…

给nginx部署https及自签名ssl证书

一、生成服务器root证书 openssl genrsa -out root.key 2048 openssl req -new -key root.key -out root.csr#Country Name (2 letter code) [XX]:---> CN#Country Name (2 letter code) [XX]:---> CN#State or Province Name (full name) []:---> Shanghai#Locality…

多层感知机 + 代码实现 - 动手学深度学习v2 | 李沐动手学深度学习课程笔记

感知机 感知机≈二分类问题 感知机和其他问题的对比 训练感知机 如果小于等于零&#xff0c;说明预测错啦 &#xff0c;其实就是同号为正&#xff0c;异号为负 举个分类的例子 增加样本&#xff0c;改变分类线 继续分类 感知机的收敛定理 XOR问题 XOR问题其实就是第1、3象限数…

【踩坑】一条指令解决torch_scatter等安装报错安装不上问题

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 背景说明 (推荐方法)解决方法一&#xff1a;使用conda安装。 解决方法二&#xff1a;指定pip的网站。 解决方法三&#xff1a;直接去下载whl文件。 (终极方法)解决方法四&#xff1a;配置MSVC 特殊情况…

构建 ESLint 内存泄露检测插件入门:提升代码质量与防范运行时风险

前言 本文目的是介绍如何创建开发一个自定义规则 ESLint 插件。利用其能力,检测一些代码中可能存在的内存泄露并及时进行提示,避免潜在的后期影响。 本文实现其中一部分功能–检测事件监听器的使用是否存在内存泄露为例来演示基本的 ESLint 自定义规则插件开发的过程。用以…

nginx笔记整理

目录 一.Nginx基础介绍 二.nginx安装配置 三.Nginx配置文件 3.1nginx主配置文件(/etc/nginx/nginx.conf) 3.2默认的网站配置文件(/etc/nginx/conf.d/default.conf) 四.创建新的虚拟主机 五.Nginx日志 5.1nginx日志格式 5.2查看日志 5.3日志缓存(了解) 5.4日志轮转(/…

COMPOSER安装使用WIN下升级PHP-V

想用TP6使用phpspreadsheet但是说我PHP版本低&#xff0c;原来是PHP7.0 composer要求至少7.4 直接修改环境变量&#xff0c;把PHP目录切换到7.4 composer升级比较简单&#xff0c;在PHP目录下CMD然后官网的命令执行下即可 下面就可以在TP根目录下执行命令安装PHPSPREADSHEET…

Domain-Wall Memory Buffer for Low-Energy NoCs

目录 Domain-Wall Memory Buffer for Low-Energy NoCs主要工作DWM&#xff1a; Domain-wall memory磁畴壁存储器磁性纳米线阵列设计 开销分析实验设计实验结果分析 参考资料 Domain-Wall Memory Buffer for Low-Energy NoCs 主要工作 我们基于SRAM在NoC中使用的头尾指针概念&a…

2024年【道路运输企业主要负责人】考试报名及道路运输企业主要负责人模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 道路运输企业主要负责人考试报名根据新道路运输企业主要负责人考试大纲要求&#xff0c;安全生产模拟考试一点通将道路运输企业主要负责人模拟考试试题进行汇编&#xff0c;组成一套道路运输企业主要负责人全真模拟考…

字符串匹配——烦人的KMP

相信很多同学看到这篇文章的时候&#xff0c;已经被KMP拿捏了吧&#xff01;KMP算法说难&#xff0c;倒也不是很难&#xff0c;手算都会&#xff0c;说不难吧&#xff0c;短短几行代码愣是看不懂&#xff0c;辗转反侧&#xff0c;翻书查阅&#xff0c;视频讲解&#xff0c;最后…