【C++杂货铺】拷贝构造函数

📖定义

拷贝构造函数是构造函数的一个重载 ,它的本质还是构造函数,那就意味着,只有在创建对象的时候,编译器才会自动调用它,那他和普通的构造函数有什么区别呢?

拷贝构造函数,是创建对象的时候,用一个已存在的对象,去初始化待创建的对象 。简单来说,就是在我们创建对象的时候,希望创建出来的对象,和一个已存在的对象一模一样,此时就应该用拷贝构造函数,而不是普通的构造函数。拷贝构造函数有一点类似于克隆技术。

cpp 复制代码
Data d1(2023, 7, 20);//定义一个日期类对象d1
Data d2(d1);//会去调用拷贝构造函数

int a = 10;
int b = a;//不会调用拷贝构造

上面代码,首先定义了一个日期类对象d1,接着想创建第二个日期类对象d2,并且希望d2d1一模一样,也就是用d1去克隆出d2d2相当于是d1的一份拷贝。所以在创建d2对象的时候,参数列表直接传递了d1

小Tips :拷贝构造函数是针对自定义类型的,自定义类型的对象在拷贝的时候,C++规定必须要调用拷贝构造函数。内置类型不涉及拷贝构造函数,如上,用a去创建b,是由编译器直接把a所表示的空间中的内容直接拷贝到b所表示的空间,并不涉及拷贝构造函数。

📖拷贝构造函数的错误写法

有了上面的分析,可能很多朋友会觉得,那我直接在类里面再写一个构造函数,把它的形参设置成日期类对象,不就行了嘛,于是便得到了下面的代码:

cpp 复制代码
Data(Data d)//错误的拷贝构造
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

是不是觉得很简单?创建d2对象的时候,实参把d1传过来,然后用d接收,最后再把d的所有值赋值给this指针(当前this指针就指向d2),这一切堪称完美,但是我想告诉你,这种写法是大错特错的。

📖为什么是错的

问题出现在传参,就是实参d1传递给形参d的时候,上面代码中的形参d,既不是指针也不是引用,说明是值传递,值传递就意味着,形参d是实参d1的一份拷贝,注意:是拷贝 ,就是说,形参d要和实参d1一模一样,怎么才能让dd1一摸一样?调用拷贝构造函数呀。

形参d在接收实参d1的时候,又要去调用拷贝构造来创建d,这次调用拷贝构造,又会有一个形参d,这个形参d又需要调用拷贝构造才能创建,相信到这里,小伙伴们已经看出问题所在了---------无穷递归 ,形参在接收的时候,会无穷无尽的去调用拷贝构造函数,就像套娃一样。

为了避免出现这种无穷递归,编译器会自行检查,如果拷贝构造函数的形参是值传递,编译时会直接报错。

📖必须是引用

为了打破上面的魔咒,拷贝构造函数的形参只能有一个 ,并且必须是类类型对象的引用。下面才是正确的拷贝构造函数:

cpp 复制代码
Data(Data& d)//正确的拷贝构造
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
Data d1(2023, 7, 20);//定义一个日期类对象d1
Data d2(d1);//

此时创建d2的时候,传递d1调用拷贝构造函数,形参d是一个日期类的引用,因为引用时区别名,意味着dd1的一个别名,此时就不会再去无穷无尽的调用拷贝构造啦。

📖建议加const

因为存在用一个const对象去初始化创建一个新对象这种场景,所以建议在拷贝构造函数的形参前面加上const,此时普通的对象能用,const对象也能用。

cpp 复制代码
Data(const Data& d)//正确的拷贝构造
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
const Data d1(2023, 7, 20);//定义一个日期类对象d1
Data d2(d1);

📖编译器生成的拷贝构造干了什么?

上一节提到,拷贝构造是一种默认成员函数,我们不写编译器会自动生成。编译器生成的默认拷贝构造函数,对内置类型按照字节方式直接拷贝(也叫值拷贝浅拷贝),对自定义类型是调用其拷贝构造函数完成拷贝

cpp 复制代码
class Time//定义时间类
{
public:
	Time()//普通构造函数
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)//拷贝构造函数
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private://成员变量
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

📖什么是浅拷贝

上面提到,编译器生成的拷贝构造函数,会对内置类型完成浅拷贝,浅拷贝就是以字节的方式,把一个字节里的内容直接拷贝到另一个字节中。

📖拷贝构造函数可以不写嘛?

通过上面的分析可以得出:编译器自己生成的构造函数对内置类型和自定义类型都做了处理。那是不是意味着我们就可以不写拷贝构造函数了呢?答案是否定的,对于日期类,我们确实可以不写,用编译器自己生成的,但是对于一些需要深拷贝的对象,构造函数是非写不可的。栈就是一个典型的需要我们自己写构造函数的例子

cpp 复制代码
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,我们没有写它的拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,栈中的成员变量都是内置类型,默认的拷贝构造函数会对这三个成员变量都完成值拷贝(浅拷贝)。

此时浅拷贝的问题在于 :对象s1和对象s2中的_array存的是同一块空间的地址,他俩指向了同一块空间,当程序退出,往s1s2中的任意一个对象push值,另一个也会跟着改变。s1s2要销毁,s2先销毁,s2销毁时调用析构函数,已经将0X11223344这块空间释放了,但是s1并不知道,到s1销毁的时候,会将0X11223344这块空间再释放一次,一块内存空间多次释放,最终就会导致程序崩溃。

📖深拷贝

通过上面的分析可以看出,简单的浅拷贝不能满足栈的需求,因此,对于栈,我们需要自己写一个拷贝构造函数,来实现深拷贝,深拷贝就是去堆上重新申请一块空间,把s1_array指向的空间中的内容,拷贝到新申请的空间,再让s2中的_array指向该空间。

cpp 复制代码
//自己写的拷贝构造函数,实现深拷贝
Stack(const Stack& st)
{
	DataType* tmp = (DataType*)malloc(sizeof(DataType) * st._capacity);
	if (nullptr == tmp)
	{
		perror("malloc申请空间失败");
		return;
	}
	memcpy(tmp, st._array, sizeof(DataType) * st._size);
	_array = tmp;
	_size = st._size;
	_capacity = st._capacity;
}

📖总结:

类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,拷贝构造函数是一定要写的,否则就是浅拷贝,最终析构的时候,就会释放多次,造成程序崩溃。

📖拷贝构造函数典型的调用场景:

  • 使用已存在对象创建新对象。
  • 函数参数类型为类类型对象。
  • 函数返回值为类类型对象。
cpp 复制代码
class Data
{
public:
	Data(int year = 1, int month = 1, int day = 1)
	{
		cout << "调用构造函数:" << this << endl;
		cout << endl;
		_year = year;
		_month = month;
		_day = day;
	}

	Data(const Data& d)
	{
		cout << "调用拷贝构造:" << this << endl;
		cout << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	~Data()
	{
		cout << "~Data()" << this << endl;
		cout << endl;
	}
private:
	int _year;
	int _month;
	int _day;

	//可以不用写析构,因为全是自定义类型,并且没有动态申请的空间,这三个成员变量会随着对象生命周期的结束而自动销毁
};
Data Text(Data x)
{
	Data tmp;
	return tmp;
}

int main()
{
	Data d1(2023, 4, 29);
	Text(d1);
	return 0;
}

📖总结:

自定义类型在传参的时候,形参最好用引用来接收,这样可以避免调用拷贝构造函数,尤其是深拷贝的时候,会大大的提高效率,函数返回时,如果返回的对象在函数栈帧销毁后还在,最好也用引用返回。


🎁结语:

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!

相关推荐
西猫雷婶12 分钟前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila12 分钟前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
小_太_阳38 分钟前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾41 分钟前
scala借阅图书保存记录(三)
开发语言·后端·scala
唐 城1 小时前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
码银3 小时前
【python】银行客户流失预测预处理部分,独热编码·标签编码·数据离散化处理·数据筛选·数据分割
开发语言·python
从善若水3 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust
lqqjuly3 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
冰红茶兑滴水4 小时前
云备份项目--工具类编写
linux·c++
刘好念4 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl