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

C++初阶——类和对象(四) 拷贝构造函数、赋值运算符重载函数

C++初阶——类和对象(四)

上期内容中,我们详细讲解了类的六个默认成员函数中的构造函数析构函数,有了构造函数,我们在创建类对象的时候就可以将其初始化成需要的信息;有了析构函数,我们可以在对象生命周期结束时自动销毁开辟的内存空间,有效的避免了内存泄漏。从这几期的内容中,我们逐渐感受到C++在C语言的基础上,做出了很多的优化。本期内容,将为大家介绍拷贝构造函数以及赋值运算符重载函数

一、拷贝构造函数

1.背景引入

我们还是先以日期类为例:
示例1
这样的一个日期类中,我们重载了两个构造函数,一个无参,一个带有参数,在创建类对象时,用法也有所不同:对于带有参数的构造函数,创建类对象时将需要的信息填在后面的括号里;对于无参的构造,创建的对象后面不能加括号,因为会被误以为是函数的声明
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
在这里我们已经知道了d2的具体信息,因此可以通过Date d3(2025, 3, 15);创建一个和d2一样的对象d3,但是这种显然是不方便的,那么,我们就需要了解一下拷贝构造函数了!

2.什么是拷贝构造函数?

我们来看这样一段代码:
示例2
在这里,我们先用Date类实例化出一个对象d1,在对d2实例化时,就有所不同了,从字面意思上也很容易理解,就是按照d1的内容再初始化一个对象d2。不过,这里的构造和前面的有一定的区别,因为前面的毕竟是直接传了具体的数值,可以直接调用构造函数初始化;而这里的参数是一个类对象,肯定不能传参给我们之前写的那些构造函数,那么到底应该调用哪个函数呢?——拷贝构造函数应运而生。通过拷贝构造,我们把类对象里的值拷贝一份,再赋给这个新对象d2,完成它的初始化。拷贝构造函数的调用,发生在传递的参数是一个类对象时,因为它是一个类对象,就一定要先把成员的值拷贝出来,这种调用是一定会发生的。至于拷贝构造函数是什么样的,这里没有显式地实现,因为这里都是内置类型,是简单的值拷贝编译器已经帮我们生成了默认的拷贝构造函数,但是,我们可以自己写一个拷贝构造,来进一步了解一下。

3.拷贝构造函数的特性

  • 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
    示例3
    首先,和构造函数类似,拷贝构造函数的函数名与类名相同,在传参为类对象时会自动调用,形参为类对象的引用。函数体的定义中,也是简单明了的,this指针指向的是需要被初始化的对象,d是已经存在的对象的别名,需要注意的是,一定要清楚是把谁赋值给谁,这里是很容易出错的,我们看一下调试:
    示例4
    按F11,转入调用的函数中:
    示例5
    示例6
    我们再来探讨一下为什么需要传引用,无穷递归是怎么产生的呢?
    如图所示:
    示例7
    只要是传递类对象,就会调用它的拷贝构造函数,这是语法规定。由于形参是实参的一份临时拷贝,那么对于Date d2(d1);传给函数Date(Date d),首先需要拷贝实参d1,既然要拷贝,就要调用拷贝构造函数,而拷贝构造函数的传参又需要将实参拷贝给形参,既然要拷贝,又要调用拷贝构造……也就是说拷贝构造函数自己在调用自己,引发无穷递归。因此,形参不能是传值类型,而应该是传引用,Date(const Date& d)意思就是d是实参d1的一个别名,这样就可以避免传值需要拷贝的问题。

4.默认拷贝构造函数

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。比如:
示例8
这里就没有写拷贝构造函数,编译器默认生成了一个,由于这里的成员变量都是内置类型,默认的拷贝构造函数是足够使用的,但是对于 自定义类型,默认生成的就不一定够用了,需要自己实现,我们来看下面一段代码:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);

	return 0;
}

运行时,程序崩溃了。因为这里我们没有自己写拷贝构造函数,默认生成的只能完成简单的值拷贝(浅拷贝),而Stack类中有一个成员变量是指针类型,指向的是动态开辟的内存空间,浅拷贝使得对象s2的成员——指针变量也指向同一块空间,然而程序退出时,由于析构函数的存在s1s2动态开辟的内存空间要销毁,因此同一块空间被销毁了两次,程序崩溃,如图所示:
示例9
所以,类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5.拷贝构造函数典型调用场景

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
    示例10
    为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

二、赋值运算符重载

接下来,就到了另一个非常重要的板块——运算符重载!本期内容,我们先为大家介绍赋值运算符重载。

1.背景引入

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

  • 函数名:关键字operator后面接需要重载的运算符符号。
  • 函数原型:返回值类型 operator 操作符 (参数列表)

2.赋值运算符重载

赋值运算符重载格式:

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值(易错)
  • 返回*this:要复合连续赋值的含义

我们以日期类为例,先来简单的实现一下:
示例11
示例12

  • 先创建一个日期类对象d1,并且初始化想要的数据,然后再创建一个对象d2,我们没有给出具体的数值,那么它将调用默认构造函数,也就是不需要传参的构造函数,在这里,我们自己实现了一个无参的构造函数,因此会出现Date()打印到屏幕上,然后,我们将d1的值赋给d2。那么我们不难发现,赋值运算符重载和构造函数是有区别的,构造函数是在创建的时候初始化,对象是新创建的,而赋值是在两个已经存在的对象之间,这就是区别所在。

当然,上面的赋值运算符重载函数实现得比较粗糙,我们稍加改进,如图所示:
示例13

  • 引用返回是为了减少拷贝次数,这里还加了一个检查是否是自己给自己赋值,如果是自己给自己赋值,直接返回this解引用即可。

和前面的几个默认成员函数一样,用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
if语句块里面的this指针是可以省略的,这里是为了方便大家理解是把谁赋给谁。

3.注意事项

  • 赋值运算符只能重载成类的成员函数不能重载成全局函数,赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
  • 如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现

本期总结+下期预告

本期内容,我们详细介绍了拷贝构造函数已经赋值运算符重载函数,然而赋值运算符重载只是运算符重载之一,下期内容将为大家带来一系列的运算符重载的实现!

感谢大家的关注,我们下期再见!
在这里插入图片描述


http://www.kler.cn/a/589545.html

相关文章:

  • 单元测试、注解
  • EasyExcel动态拆分非固定列Excel表格
  • ZVA-Z90,罗德与施瓦茨毫米波变换器
  • 边缘端设备开发流程全解
  • uniapp-x web 开发警告提示以及解决方法
  • Rust + WebAssembly 开发环境搭建指南
  • 蓝桥杯 第五天 2021 国赛 第 5 题 最小权值
  • 使用BLSTM自动评估句子级构音障碍的可理解性
  • ssh命令
  • QVariant:Qt中万能类型的使用与理解
  • python中多重继承和泛型 作为模板让子类实现具体业务逻辑
  • Linux错误(2)程序触发SIGBUS信号分析
  • 基于Springboot+Typst的PDF生成方案,适用于报告打印/标签打印/二维码打印等
  • 开源文档管理系统 Paperless-ngx
  • 【后端开发面试题】每日 3 题(十三)
  • 利用golang embed特性嵌入前端资源问题解决
  • 【经验分享】SpringBoot集成WebSocket开发-03 使用WebSocketSession为每个对话存储独立信息
  • Vue3中正确解析RefImpl对象
  • Hyperlane:轻量、高效、安全的 Rust Web 框架新选择
  • Java 大视界 -- Java 大数据机器学习模型的对抗攻击与防御技术研究(137)