拷贝构造函数
拷贝构造函数
拷贝构造是构造函数的一个重载,用于从其他对象拷贝初始化。
class A {
public:
// 构造函数
A() :i_(0) {}
// 拷贝构造
A(const A &a) i_(a.i_) {}
private:
int i_;
};
拷贝构造的调用
拷贝初始化
当对象通过拷贝其他对象来初始化时,会调用拷贝构造函数。
下面对象 a2
通过拷贝 a1
来完成自身的初始化。
A a1;
A a2(a1);
同样的,可以通过下面的代码看出拷贝构造是如何调用的。
class A {
public:
// 构造函数
A() :i_(0) {}
// 拷贝构造
A(const A &a) :i_(a.i_) {
cout << "A(const A&)" << endl;
}
private:
int i_;
};
int main() {
A a1;
A a2(a1);
return 0;
}
函数的传参
只要对象发生了拷贝初始化,就必须调用拷贝构造,包括函数传参的过程。
void fun(A a) {} // 对象a1传参给a时,会调用一次拷贝构造
int main() {
A a1;
fun(a1); // 函数调用,传参
return 0;
}
由于拷贝构造函数也是函数,所以在传参时,对象必须以引用的形式传参。
如果以非引用的形式传参,会发生拷贝构造的调用,在拷贝构造的参数列表中,发生拷贝构造函数的调用,即对拷贝构造自身的调用,会造成无限递归。
非引用传参的危害
假设拷贝函数的参数 a
不使用引用传参
A(A other) :i_(other.i_) {}
对象 a2
调用拷贝构造函数,试图对对象 a1
进行拷贝
int main() {
A a1;
A a2(a1);
return 0;
}
在调用 A a2(a1)
时,对象 a1
被作为参数传递给拷贝构造函数 A(A other)
中的对象 other
,由于 other
不是一个引用,所以此时会发生新的拷贝:从 a1
到 other
的拷贝。而调用拷贝函数时,又会发生拷贝式的传参,造成无限递归。
因此,在定义拷贝构造时必须使用引用传参。
深拷贝和浅拷贝
为什么需要自定义拷贝构造
在上面的示例中,拷贝构造对成员变量进行了拷贝,即直接对对象的内存空间进行了复制,这种拷贝称为浅拷贝。这种情况下,直接使用默认生成的拷贝构造函数也达成目的。
如果对象中管理了其他的资源,而拷贝时需要对这些额外的资源进行拷贝,就会涉及到对象的深拷贝。比如成员变量中含有其他资源的指针,就不得不自己定义拷贝构造。
编译器默认生成的拷贝构造都是浅拷贝。
拷贝构造的实现
以之前Stack类举例。
int main() {
Stack s1;
Stack s2(s1);
return 0;
}
如果使用编译器默认生成的拷贝构造,在对Stack对象进行拷贝时,_data
指针会被拷贝到新对象,而其指向的数据没有被拷贝。这样会造成诸多问题,比如:
- 由于两个对象的
_data
指针指向同一块空间,在原对象中对_data
指向的空间做修改时,会影响新对象中_data
指向的空间。 - 在调用析构函数时,
_data
指向的空间会被释放两次,而导致程序异常。
此时就需要自己定义深拷贝的逻辑了,不能只拷贝 _data
指针,而是要拷贝其指向的空间。
Stack(const Stack& s) :_capacity(s._capacity), _top(s._top), _data(nullptr) {
// 申请新的空间
_data = (int*)malloc(_capacity * sizeof(int));
assert(_data != nullptr);
// 对空间进行拷贝
memcpy(_data, s._data, _capacity * sizeof(int));
}
这样就能正确地完成Stack类的深拷贝了。