【C++】走进 ⌈ 类和对象 ⌋ 的核心 - 感受C++的精华 _ 剖析默认成员函数 | 构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载

💛 前情提要💛

本章节是C++六个默认成员函数的相关知识~

接下来我们即将进入一个全新的空间,对代码有一个全新的视角~

以下的内容一定会让你对C++有一个颠覆性的认识哦!!!

以下内容干货满满,跟上步伐吧~


作者介绍:

🎓 作者: 热爱编程不起眼的小人物🐐
🔎作者的Gitee:代码仓库
📌系列文章&专栏推荐: 《刷题特辑》《C语言学习专栏》《数据结构_初阶》《C++轻松学_深度剖析_由0至1》

📒我和大家一样都是初次踏入这个美妙的“元”宇宙🌏 希望在输出知识的同时,也能与大家共同进步、无限进步🌟
🌐这里为大家推荐一款很好用的刷题网站呀👉点击跳转



💡本章重点

在这里插入图片描述

  • 认识并实现C++中类的默认成员函数:

    • 构造函数

    • 析构函数

    • 拷贝构造函数

    • 赋值运算符重载

  • 🔥了解C++的魅力:运算符重载


🍞一.类的默认成员函数

💡默认成员函数:

  • 简单来说:就是在任何一个类(空类、日期类……)中编译器会自动生产六个默认成员函数,来辅助这个类更好的实现

  • 虽然编译器会自动生成,我们也可以显示的去定义

👉接下来我们就深入分析默认成员函数究竟是什么吧~


🥐Ⅰ.构造函数

💡构造函数:

  • 函数形式:类名() {};

  • 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次

  • 本质: 辅助完成对象的成员变量的初始化,将对象的成员变量初始化函数内嵌化【相当于顶替了我们以前实现数据结构的Init函数】

➡️构造函数的特征:

  • 函数名字与类名相同

  • 函数没有返回值

  • 在对象进行实例化的时候,编译器自动调用其构造函数

  • 构造函数支持重载

👉那我们就以日期类为例子,来剖析构造函数吧:

class Date
{
public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
  • 上述的日期类中,我们虽然看不见默认构造函数,但其实它是存在的,只是没有显示的调用,函数为:Date() {}【无参构造函数】

1️⃣在上述的了解中,我们得知默认构造函数会帮助我们初始化成员变量,那初始化后的值是多少呢?

在这里插入图片描述

  • 上述的检验便可以得知,编译器自动生成的默认构造函数,对成员变量初始化的值为随机值,这样一来我们就可以知道:

  • 编译器生成的默认构造函数对成员变量进行初始化了,但并没有处理【即初始化成随机值了,但并没有真正像Init函数一样处理成我们想要的默认值】

  • 正是因为C++认为没有初始化就使用对象会存在一些使用风险,于是引入了构造函数,这样就不会忘记调用对应的函数对成员变量初始化了

  • 👆综上: “默认构造函数初始化了,但没完全初始化”【因为并没有将初始化后的值处理成0等其它数值】

2️⃣此时为了达到能将初始化后的值处理成我们想要的值,可以有如下种操作:

  1. 无参构造函数:即在对象实例化的过程中,编译器自动调用此构造函数进行成员变量的初始化
Date(int year, int month, int day)
{
	_year = 0;
	_month = 1;
	_day = 1;
}

在这里插入图片描述

  1. 带参构造函数: 即在实例化的过程中,传参实例化对象,显示定义构造函数
Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

在这里插入图片描述

  • 特别注意: 在我们显示定义构造函数后,编译器便不会再生成默认的构造函数,而是采用我们自己的构造函数进行使用
  1. 全缺省构造函数:在第一步的基础上,我们可以进一步优化,写成全缺省的构造函数【这样就可以在不带参的情况下,也处理成我们想要的值】
Date(int year = 1, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}

在这里插入图片描述

  • 特别注意: 在这种情况下,若无参进行构造函数的初始化时,是不需要带上括号的【Date d();】,否则就会被看成是关于d的函数声明
  1. 构造函数的函数重载:也可以用于构造函数的多种初始化方式中,但容易与缺省函数构成歧义,程序在执行的时候不知道应该调用哪个,所以不推荐

🔥重点:

  • 默认构造函数:并不单单指我们不写,编译器自动生成的构造函数

  • 也指我们不需要传参,即可默认被调用的构造函数,Eg:全缺省构造函数无参构造函数

class Date
{
private:
	// 基本类型(内置类型)
 	int _year;
	int _month;
 	int _day;
 	
	// 自定义类型
 	Time _t;
};
  • 默认构造函数不仅会对成员变量中的内置类型(int、char……类型)进行初始化,也会对自定义类型(class、struct……类型)的成员变量去调用它们自己的构造函数去初始化自己(Eg:Time为自定义类型),从而达到这个类(eg:Date)中的成员变量被全部初始化

  • 简单来说:默认构造函数仅仅对自定义类型成员的初始化执行了调用的操作,即调用自定义类型成员自己的默认构造函数去初始化【调用的默认构造函数为哪一种,具体看用户自己怎么样实现】

综上:

  • 细节很多,但我们实际中的构造函数多半是需要自己去定义的,且推荐写全缺省的构造函数,因为可以适用于各种场景

  • 上述就是默认构造函数的概念啦~


🥐 Ⅱ.析构函数

💡析构函数:

  • 函数形式:~类名() {};

  • 析构函数是指:对象在销毁时,自动调用析构函数,完成类中的资源清理工作

  • 其中这里的资源,并不是指对象的销毁,因为对象为局部对象,是存在栈区的,其销毁工作是由编译器去完成的

  • 而是指一些类中(Eg:vector、string……),有向堆区申请空间的,在对象被销毁时,编译器会自动调用其析构函数区释放申请的空间,避免内存泄漏【替代了以往自己实现数据结构的destroy函数】

特别注意:

  • 其中编译器仅仅是帮忙执行了调用析构函数的操作

  • 但具体的释放内存的操作,还得用户自己去显示定义、实现,并不代表着编译器也自动生成释放内存的代码

➡️析构函数的特征:

  • 无参数无返回值

  • 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数

  • 对象生命周期结束时,C++编译系统系统自动调用析构函数

❓那如果同时存在多个对象,析构的顺序是怎么样的呢

在这里插入图片描述

  • 综上:我们可以看见,析构的顺序是和对象实例化的顺序刚好相反

  • 这就涉及到函数栈帧的知识啦:

    • 对象在实例化的过程在栈帧中是压栈的操作

    • 在生命周期结束后,因为是遵循FILO(先进后出)的原则,所以最后压栈的对象先出栈

    • 这也就是为什么最后实例化出来的对象先被析构啦~

综上:

  • 在实际中,我们需要针对不同的场景去定义相对应的析构函数

  • 以上就是析构函数的概念啦~


🥐 Ⅲ.拷贝构造函数

💡拷贝构造函数:

  • 函数形式:类名(&要拷贝的对象) {};

  • 只有单个形参【该形参是对本类类型对象的引用(一般常用const修饰)】,在用已存在的类类型对象创建新对象时由编译器自动调用

  • 简单来说:就是在创建一个新的对象的时候,创建一个与已构造的对象一模一样的对象

➡️拷贝构造函数的特征:

  • 拷贝构造函数是构造函数的一个重载形式,与构造函数构成重载

  • 拷贝构造函数的参数只有一个且必须使用引用传参

❓为什么特别注意要用引用传参

  • 这是因为,如果使用传值传参的话,会引发无穷递归的调用

  • Eg:

在这里插入图片描述

  • 因为是传值传参,所以会在形参接收的时候,形参是实参的临时拷贝

  • 即如上:传值传参后,需要构建一个对象d去接收对象d1,而此操作不正相当于对象d调用拷贝构造函数去拷贝构造一个与对象d1相同值的对象吗

  • 而此时的拷贝构造函数都没还进入到实现部分,就在传参上陷入了无限的递归调用了

  • 所以只有引用传参可以解决这个问题

特别注意:

  • 如果不显示定义拷贝构造函数的函数实现,系统自己也会默认生成一个:按照按内存存储按字节序完成拷贝,即实现了浅拷贝(值拷贝)【如下所示】

在这里插入图片描述

  • 但这里会涉及拷贝的问题【后续会提到】,假如有两个堆类,一个是通过另外一个而拷贝构造出来的,但又因为是值拷贝,所以仅仅单纯的将值拷贝过去,这就会产生如下的问题:它们都指向同一块空间了

在这里插入图片描述

  • 即对其中一个对象插入删除数据时,都会导致另外一个对象也插入删除数据

  • 且在对象生命周期结束,要销毁的时候,会自动调用析构函数,于是会出现对一块空间释放两次,导致程序崩溃的错误

综上:

  • Date这样的类,只需要浅拷贝的,那么默认生成的拷贝构造函数就足够 了,不需要我们自己写

  • 但对于Stack这样的类,需要更深层次的拷贝,需要的就是深拷贝

  • 上述就是拷贝构造函数的概念啦~


🔥二.运算符重载

💡运算符重载:

  • 函数形式: 返回类型operator需要重载的运算符符号(参数列表){};

  • C++中为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似

  • 简单来说:因为类和对象的引入,C++中专门引入运算符重载解决原本在语言层面上运算符就支持内置类型,而不支持自定义类型的情况

  • 即支持我们自己针对自定义类型【Eg:Date d1 + Date d2,此时程序是不支持两个类相加的】的运算符操作,进行赋予新的意思,去告诉编译器针对这种某种类型下的运算符是如何操作的,自己给自定义类型写一个对应运算符的使用规则

特别注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@【需要在编译器原有的操作符上进行重载】

  • 重载操作符必须有一个类型或者枚举类型的操作数【如果操作数都是内置类型的话,重载就没意义了】

  • 有五个运算符是不支持重载的:*::sizeof?:.

➡️运算符重载的两种场景:

  • 1️⃣全局的operator
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year;
	&& d1._month == d2._month
	&& d1._day == d2._day;
}
  • 这里有一个问题:因为要外面访问到成员变量,则需要将成员变量设为公有的,那封装性就无法保证

  • 为了有效解决这个问题,可以将重载函数变为成员函数

  • 2️⃣类中的operator

bool operator==(const Date& d2)
{
	return _year == d2._year;
	&& _month == d2._month
	&& _day == d2._day;
}

//d1 == d2; -> d1.operator==(&d1, d2);
bool operator==(const Date& d2)
{
	return this->_year == d2._year;
	&& this->_month == d2._month
	&& this->_day == d2._day;
}

综上:

  • 就是运算符重载的概念啦~

  • 有了以上的相关概念,那我们就来看一下接下来的赋值运算符重载


🥐Ⅰ.赋值运算符重载

💡赋值运算符重载:

  • 函数形式:类名& operator=(const 类名& d) {};

  • 赋值运算符重载与拷贝构造得目的都是:拷贝一个数值一样的类出来

特别注意:

  • 与拷贝构造不同的是:拷贝构造是在对象实例化的时候,拿另外一个对象初始化自己【在对象实例化阶段】

  • 但赋值运算符重载是:在已创建的对象上,拿另外一个对象的数据对自我进行拷贝【在对象已存在阶段】

  • 对于Date d1; Date d2 = d1;这种情况,d2此时还没有被实例化出来,处于对象实例化的阶段,所以这种情况是属于拷贝构造而非赋值运算符重载

👉示例:

在这里插入图片描述

👆不难发现:

  • 和拷贝构造函数一样,对于内置类型会完成浅(值)拷贝

  • 但对于自定义类型,我们还是需要看情况去是否显示定义一个深拷贝

🔥重点: 我们应该如何实现一个赋值运算符重载呢?

void operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
  • 一般我们想到的会是如上的形式,但是此时返回类型为void,如果碰到连续赋值d1 = d2 = d3的情况,就会编译不过

  • 所以,我们的返回值的类型为类对象的引用返回

    • 所以是return *this

    • 此时相当于返回一个对象的别名(即相当于对象本身),而在连续赋值的情况下,依然可以进行赋值运算符重载,形成链式访问

    • 正也因为出了函数后对象依然存在,我们便可以利用其引用返回

  • 还有以防自己给自己赋值的情况,我们也可以增加一个判断【注意:我们这里是利用对象的地址进行判断】

  • 正确写法如下:

Date& operator=(const Date& d)
{
	if(this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
	return *this; 
}

综上:

  • 就是运算符重载的概念啦~

  • 有了以上的了解,想必大家都感悟到C++的精华了吧


🍞三.总结

综上:

  • 对于内置类型来说:

    • 编译器自动生成的构造函数(显示定义一个全缺省的构造函数)和析构函数就已经足够了

    • 编译器自动生成的拷贝构造函数赋值运算符重载会实现浅拷贝

  • 对于自定义类型来说:

    • 编译器会自动调用其默认成员函数实现相关的初始化、拷贝赋值、析构等操作

    • 自定义类型的成员涉及深拷贝等操作,我们则需要对拷贝构造函数赋值运算符重载显示实现深拷贝

    • 若涉及到内存的释放,析构函数需要显示定义去释放空间


🍞四.const成员

💡const修饰类的成员函数:

  • 将const修饰的类成员函数称之为const成员函数

  • const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

  • 简单来说:就是保护对象中的成员变量

👉该如何操作呢?

  • 我们知道,this指针是隐藏的,无法加const

  • 此时,我们就可以在成员函数的函数名后面➕const,以表示给this指针被const修饰

bool operator==(const Date& d); 
//bool operator==(Date* this, const Date& d);

bool operator==(const Date& d) const; 
//bool operator==(const Date* this, const Date& d);

特别注意:

  • 此处如果是普通对象调用const成员函数,是可以调用且类成员是收到保护的,因为这属于权限缩小

  • 但如果是const对象调用const成员函数,则调不动,因为这属于权限放大,不符合规则

  • 【对于权限的放大or缩小,同学们可点击>跳转<食用呀】

综上:

  • const*之前修饰的是指针指向的对象

  • const*之后修饰的是指针本身


🫓结尾

综上,我们基本了解了C++中的 “类和对象 - 默认成员函数” 🍭 的知识啦~

恭喜你的内功又双叒叕得到了提高!!!

感谢你们的阅读😆

后续还会继续更新💓,欢迎持续关注📌哟~

💫如果有错误❌,欢迎指正呀💫

✨如果觉得收获满满,可以点点赞👍支持一下哟~✨

在这里插入图片描述

  • 140
    点赞
  • 109
    收藏
  • 打赏
    打赏
  • 239
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:代码科技 设计师:Amelia_0503 返回首页
评论 239

打赏作者

Dream-Y.ocean

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值