拷贝构造函数和赋值函数
什么是拷贝构造
是一种特殊构造函数,如果没有显式的实现,编译器就会自动生成。
class 类名 { public: // 拷贝构造 类名(const 类名& that) { } };
什么时候会调用拷贝构造
当使用一个类对象给另一个新的类对象初始化时,就会自动调用拷贝构造。
#include <iostream> using namespace std; class Test { public: Test(void) { cout << "调用了普通的构造函数" << endl; } Test(const Test& that) { cout << "调用了拷贝构造" << endl; } }; void func(Test t) { } int main(int argc,const char* argv[]) { Test t1; // 调用的是普通构造 Test t2 = t1; // 调用的是拷贝构造 func(t1); // 调用的是拷贝构造 return 0; }
拷贝构造的任务是什么
拷贝构造参数对象的所有成员变量挨个赋值给新对象的成员变量,一般情况下编译器自动生成的拷贝构造就能完全满足我们使用需求。
什么时候需要显式实现拷贝构造
当成员变量中有指针成员且指向了堆内存,就需要显式实现拷贝构造。
编译器自动生成的拷贝构造,只会对成员变量挨个赋值,如果成员变量中有指针变量且指向堆内存,结果就两个对象的指针变量同时指向一份堆内存,当它们执行析构函数时,会把这块堆内存释放两次,产生 double free or corruption 的错误。
正确的做法应该是先给新对象的指针变量重新申请一份堆内存,然后把旧对象的指针变量所指向的内存拷贝到新对象的指针变量所指向的内存。
#include <iostream> using namespace std; class Test { int* ptr; public: Test(int num) { ptr = new int; cout << "new:" << ptr << endl; *ptr = num; } ~Test(void) { cout << "delete:" << ptr << endl; delete ptr; } /* 编译器生成的拷贝构造,会造成 double free Test(const Test& that) { ptr = that.ptr; } */ Test(const Test& that) { // 给新对象的指针变量重新申请堆内存 ptr = new int(*that.ptr); // 把旧对象的指针变量所指向的内存拷贝给新对象的指针变量所指向的内存,如果不方便解引用时可以使用memcpy函数 } void show(void) { cout << "val:" << *ptr << " addr:" << ptr << endl; } }; int main(int argc,const char* argv[]) { Test t1(12345); Test t2 = t1; t1.show(); t2.show(); return 0; }
什么是赋值函数
是一种特殊的成员函数,如果没有显式实现,编译器会自动生成。
class 类名 { public: // 赋值函数 const 类名& operator=(const 类名& that) { } };
什么时候会调用赋值函数
当一个旧对象给另一个旧对象赋值时会自动调用赋值函数。
当一个旧对象给另一个新对象初始化时会自动调用拷贝构造函数。
#include <iostream> using namespace std; class Test { public: Test(const Test& that) { cout << "调用了拷贝构造" << endl; } void operator=(const Test& that) { cout << "调用了赋值函数" << endl; } }; int main(int argc,const char* argv[]) { Test t1; // 调用了普通的构造函数 Test t2 = t1; // 调用了拷贝构造 t1 = t2; // 调用的是赋值函数 return 0; }
赋值函数的任务是什么
赋值函数与拷贝构造的任务几乎相同,都是挨个给成员变量赋值,但如果需要显式实现时,它的业务逻辑不同。
什么时候需要显式实现赋值函数
当需要显式实现拷贝构造时,就需要显式实现赋值函数,它们两个面临问题是一样的。
赋值函数不应该对成员指针变量赋值,而应该对象成员指针变量所指向的内存进行拷贝。
#include <iostream> using namespace std; class Test { int* ptr; public: Test(int num) { ptr = new int; cout << "new " << ptr << endl; *ptr = num; } ~Test(void) { cout << "delete " << ptr << endl; // delete ptr; } Test(const Test& that) { ptr = new int; // 如果不方便解引用,可以调用memcpy函数进行拷贝 *ptr = *that.ptr; cout << "new " << ptr << "调用了拷贝构造" << endl; } const Test& operator=(const Test& that) { // 当ptr和that.ptr指向的内存块大小一样,可以直接进行内存拷贝 *ptr = *that.ptr; cout << "调用了赋值函数" << endl; return *this; /* 当对象的ptr指向的内存与与that.ptr指向的内存块不一样大 先释放旧的ptr 再分配新的,要与that.ptr的内存块一样大 然后再拷贝 */ } }; int main(int argc,const char* argv[]) { Test t1(1234); // 调用了普通的构造函数 Test t2 = t1; // 调用了拷贝构造 t1 = t2; // 调用的是赋值函数 return 0; }
浅拷贝与深拷贝
拷贝就是一个对象给另一个对象赋值,编译器自动生成的拷贝构造和赋值函数执行的业务逻辑就是浅拷贝(成员指针给成员指针赋值),深拷贝就是把成员指针所指向的内存拷贝给另一个成员指针所指向的内存。
浅拷贝就是指针给指针赋值,深拷贝就内存给内存赋值。
注意:如果成员变量中没有成员指针,则浅拷贝就可以满足需求,如果如果成员变量中有成员指针且指向堆内存,则必须手动实现深拷贝,否则就会出现 double free or corruption 的错误。