四、拷贝构造函数

如果一个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。

拷贝构造使用于当前类型的对象去初始化当前类型的另一个要创建的对象,换句话来说就是当你完成的是自身这个类型的拷贝初始化就会用拷贝构造,拷贝构造函数是构造函数的一个重载。

拷贝构造函数的特点:

  1. 拷贝构造函数是构造函数的⼀个重载。
  2. 拷贝构造函数的第⼀个参数必须是当前类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值。
  3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

既然拷贝构造函数是构造函数的一个重载,接下来,我们来写一个拷贝构造函数:

代码语言:javascript

AI代码解释

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day =day;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025,8,4);
	Date d2(d1);//拷贝构造
	d2.Print();
	return 0;
}

我们看到,在拷贝构造特点中的第二点中写到:拷贝构造函数的第⼀个参数必须是类类型对象的引用。

那为什么要用引用传参?为什么不能用传值传参?

有人会说,func(d1)不是在调用函数吗?怎么跑到拷贝构造上面去了 ? 在调用一个函数前,编译器会先完成传参(传值传参就是拷贝,对于自定义类型的拷贝,都是调用拷贝构造来完成拷贝的),然后再去调用这个函数

总结: C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

如果拷贝构造函数使用传值调用,会形成无穷递归:

每次要调用靠别构造函数之前要进行传值传参,传值传参是一种拷贝,又形成一个新的拷贝构造,调用新的拷贝构造之前要进行传值传参,传值传参是一种拷贝,又形成一个新的拷贝构造,以此下去,就形成了无穷递归,但是当我们使用引用传参时,就不需要进行拷贝构造,这样就不会形成无穷递归了,所以我们使用引用传参。

当我们使用拷贝构造函数的时候,引用传参建议使用const:

代码语言:javascript

AI代码解释

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day =day;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
	//加上const可以避免权限放大
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void func(Date d)
{

}
int main()
{
	Date d1(2025,8,4);
	func(d1);
	return 0;
}

通过上面的学习,我们知道拷贝构造函数是适用于当前类型的对象去初始化当前类型的另一个要创建的对象,比如上面的Date类,那如果说拷贝构造仅仅适用于这种一一拷贝,那拷贝构造是不是有点太简单了。接下来,我们来看一下稍微难点的应用:

代码语言:javascript

AI代码解释

class Stack
{
public:
	Stack(int n=4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		_top = 0;
		_capacity = n;
	}
	~Stack()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_top = 0;
			_capacity = 0;
		}
	}
	Stack(const Stack& s)
	{
		_a = s._a;
		_top = s._top;
		_capacity = s._capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

ok,我们创建了一个栈,然后使用拷贝构造函数给s2进行初始化,在这个初始化的过程中会发生什么不可预期的东西呢?

我们看到红方框内的地址是一样的。那就有同学会问了,地址一样会有什么问题吗?ok,当你使用编译器进行调试的时候,就会发现编译器崩了:

这是什么原因呢?当我们调试编译器的时候,返现拷贝构造时没有发生任何错误,但是当我们执行到第二次析构函数的时候,此时发生了报错,只是因为编译器对空间进行了两次释放(同一块空间)

那为什么会对空间进行两次释放呢?

那我们该如何解决呢? 对于这种情况,我们要使用深拷贝:不仅仅是对成员拷贝,还是对指向资源空间数据进行拷贝

代码语言:javascript

AI代码解释

class Stack
{
public:
	Stack(int n=4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		_top = 0;
		_capacity = n;
	}
	~Stack()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_top = 0;
			_capacity = 0;
		}
	}
	//深拷贝
	Stack(const Stack& s)
	{
		_a = (int*)malloc(sizeof(int) * s._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			exit(1);
		}
        //将数据拷贝过去
        memcpy(_a,s._a,sizeof(int)* s._capacity);
		_top = s._top;
		_capacity = s._capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

前面我们学习的构造函数和析构函数,当我们不写时,编译器自动生成的构造函数和析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。但是拷贝构造有点不同:若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐