拷贝构造函数

389 阅读3分钟

「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战

💦 概念

在这里插入图片描述

在现实生活中有双胞胎的存在,那么我们的对象也可以有。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;	
	}
	//Date d3(d2);
	//Date(Date d)
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}
	
	//Date d3(d2);
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
	//Date d3(d2);
	Date(const Date* p)
	{
		_year = p->_year;
		_month = p->_month;
		_day = p->_day;
	}	
	void Print()
	{
		cout << _year << "/"<< _month << "/" << _day <<	endl;	
	} 
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2022, 1, 1);
	d1.Print();
	d2.Print();
	//拷贝复制一个d2对象
	Date d3(d2);
	d3.Print();
	//指针也可以,但不好
	Date d4(&p2);
	d4.Print();
	return 0;
}

📝 说明

在这里插入图片描述

error C2652:非法的复制构造函数,第一个参数不应是 "Date"。

这里会引发一个无穷递归的现象,只是语法进行了强制检查,所以它由运行时错误转向了编译时错误。

在这里插入图片描述

怎么解决 ❓

1、这里用引用解决了问题,因为形参是实参的别名,即 d 是 d2 的别名

   注意如果引用传参,不是做输出型参数,最好加 const 在这里插入图片描述

2、当然也能使用指针解决,但并没有引用好用 在这里插入图片描述

💦 特性

拷贝构造函数也是特殊的成员函数,其特征如下:

1️⃣ 拷贝构造函数是构造函数的一个重载形式。

2️⃣ 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

❓ 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝 ❔

class AA
{
public:
	AA()//下面的拷贝构造也是构造,写了就不会生成,所以这里要写一个默认构造函数出来才能通过
	{}
	AA(const AA& a)	
	{
		cout << "AA(const AA& a)" << endl;
	}
};
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;

	AA _aa;
};
int main()
{
	Date d1;
	Date d2(2022, 1, 1);
	d1.Print();
	d2.Print();
	//拷贝复制一个d2对象
	Date d3(d2);
	d3.Print();

	return 0;
}

📝 说明

在这里插入图片描述 我们不写,编译器默认生成拷贝构造,跟构造和析构不太一样,它不会去区分内置类型和自定义类型,都会处理。

1、内置类型,字节序的浅拷贝 (一个字节一个字节的拷贝)。

   AA(const AA& a)

2、自定义类型,会去调用它的拷贝构造完成拷贝。

   2022/1/1

❓ 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了 (内置类型和自定义类型),是否意味着编译器自己生成的就行呢。我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试 ❔

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if(_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

📝 说明

经调试发现虽然已经完成了拷贝,但是程序崩了 在这里插入图片描述 在这里插入图片描述

为什么会崩 ❓

在这里插入图片描述 此时 st1 和 st2 指向同一块空间,那么问题来了,谁先被析构呢 —— 根据栈的特性后进先出,所以这里先被析构的是 st2。st2 被析构后再析构 st1 时程序就崩溃了,因为同一块空间不能释放两次。

编译器默认生成的拷贝构造并不能解决所有问题,像 Stack 这样的类,编译器默认生成拷贝构造完成的就是浅拷贝。解决方案就是自己实现深拷贝,对于深拷贝,因为比较复杂,所以后面要另写一篇文章。

❓ 拷贝 | 拷贝构造 ❔

int p()
{
	int ret = 0;
	return ret;	
}
Date q()
{
	Date ret;
	return ret;
}

📝 说明

我们说了,在传值返回时,它会先拷贝 (实际上是两次拷贝,但有些编译器会优化成一次)。 在这里插入图片描述

为什么需要拷贝 ❓

当函数调用结束,函数栈帧会被销毁,ret 是属于这个栈帧的局部变量,所以拷贝是为了解决局部变量被销毁的问题。使用引用可以解决拷贝消耗,但要注意如果出了作用域返回对象不在了就不能用引用。注意有些编译器在函数结束时不会清理栈帧,所以能返回正确返回值 (如果不是正确返回值,那么说明栈空间被清理了),但那是非法的。