【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;
}

📖总结:

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


🎁结语:

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

相关推荐
‘’林花谢了春红‘’40 分钟前
C++ list (链表)容器
c++·链表·list
----云烟----1 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024061 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic2 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it2 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康2 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神2 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
机器视觉知识推荐、就业指导2 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海3 小时前
scala String
大数据·开发语言·scala
qq_327342733 小时前
Java实现离线身份证号码OCR识别
java·开发语言