C++:类(二)—— 拷贝控制(浅拷贝,深拷贝,浅赋值,深赋值)
一、拷贝构造函数
浅拷贝:
1、如果类未定义自己的拷贝构造函数,编译器会为它合成一个默认拷贝构造函数(默认合成的是public的)。
拷贝构造函数从来不显式调用,而是由编译器隐式地调用。在以下三种情况:
(1)定义对象
Object a;
Object b(a); // 或写成 Object b = a;
(2)动态创建对象
Object a;
Object* p = new Object(a);
(3)函数的传值调用
void Test(Object obj);
2、区分构造与赋值:
构造:
Object a;
Object b = a; // 或写作 Object b(a);
// 此为“构造”,在创建对象的时候给初值,拷贝构造函数被调用
赋值:
Object a(1, 2);
Object b;
b = a; // 此为“赋值”,不会调用拷贝构造函数,调用的是拷贝赋值运算符
3、注意:除非深拷贝(即类中含有指针成员时),否则不要人为定义拷贝构造函数,使用编译器合成的默认拷贝构造函数即可。
一旦你决定了要添加拷贝构造函数,请仔细检查:
(1)所有的成员变量,要依次拷贝,所有成员变量,不能遗漏
(2)记得调用父类的拷贝构造函数
深拷贝(类中有指针成员):
当类有指针成员的时候,需要人为定义拷贝构造函数,而不能继续使用编译器默认合成的。
因为默认的拷贝构造函数只会单纯的复制指针,而不会把指针指向的对象的值保存到一块新开辟的内存中。
看如下例子:
1 #include <iostream> 2 3 using namespace std; 4 5 class Text 6 { 7 public: 8 Text(string s) 9 { 10 size = s.length(); 11 p = new string(s); 12 } 13 ~Text() 14 { 15 delete p; //默认析构函数只会销毁成员指针,不会delete成员指针指向的对象,所以此处要人为定义一个析构函数。 16 } 17 private: 18 int size; 19 string* p; 20 }; 21 22 int main() 23 { 24 Text t1("hello"); 25 Text t2(t1); //调用拷贝构造函数 26 }
此程序运行时崩溃,主要原因是:
// 对象创建
t1 的指针对象p,指向一块内存
t2 拷贝了 t1, 此时 t2.p 和 t1.p 指向了同一块内存
// 对象析构
对象t1析构, t1.p 被delete,释放其指向对象的内存;
对象t2析构,t2.p被delete,但因为 t2.p 和 t1.p 指向了同一块内存,此块内存已经被delete,所以崩溃。(同一块内存被重复delete)
解决办法:人为定义自己的拷贝构造函数(深拷贝),程序如下:
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 class Text 6 { 7 public: 8 Text() = default; //默认构造 9 Text(string s) //构造函数 10 { 11 size = s.length(); 12 p = new string(s); 13 } 14 Text(const Text& t) //需要人为定义自己的深拷贝构造函数 15 { 16 this -> size = t.size; 17 this -> p = new string(*t.p); 18 } 19 ~Text() 20 { 21 delete p; //默认析构函数只会销毁成员指针,不会delete成员指针指向的对象,所以此处要人为定义一个析构函数。 22 } 23 private: 24 int size; 25 string* p; 26 }; 27 28 int main() 29 { 30 Text t1("hello"); 31 Text t2 = t1; //调用拷贝构造函数 32 }
三、拷贝赋值运算符
1、与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个默认的拷贝赋值运算符(默认合成的是public的)。 (浅赋值)
2、除非类中含有指针成员时,否则不要人为定义拷贝赋值运算符,使用编译器默认合成的即可。 (深赋值)
看如下例子:
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 class Text 6 { 7 public: 8 Text() = default; //默认构造 9 Text(string s) //构造函数 10 { 11 size = s.length(); 12 p = new string(s); 13 } 14 15 ~Text() 16 { 17 delete p; //默认析构函数只会销毁成员指针,不会delete成员指针指向的对象,所以此处要人为定义一个析构函数。 18 } 19 private: 20 int size; 21 string* p; 22 }; 23 24 int main() 25 { 26 Text t1("hello"); 27 Text t2; 28 t2 = t1; //调用赋值运算符 29 }
运行时会崩溃,原因与深拷贝原因一样,编译器合成的默认赋值运算符只是拷贝了指针,没有新开辟内存,导致同一对象的内存重复被delete。
解决方法:深赋值(与深拷贝类似),人为定义一个拷贝赋值运算符
注意: (1) 要记得释放原左侧对象的内存,之后再把右侧对象赋值给左侧对象。
(2) 返回值为类类型的引用,即返回本对象
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 class Text 6 { 7 public: 8 Text() = default; //默认构造 9 Text(string s) //构造函数 10 { 11 this->size = s.length(); 12 p = new string(s); 13 } 14 Text& operator = (const Text& t) 15 { 16 auto newp = new string(*t.p); //拷贝底层的string 17 delete this->p; //释放旧内存 18 this->p = newp; //从右侧运算对象拷贝数据到本对象 19 this->size = t.size; 20 return *this; //返回本对象 21 } 22 23 ~Text() 24 { 25 delete p; //默认析构函数只会销毁成员指针,不会delete成员指针指向的对象,所以此处要人为定义一个析构函数。 26 } 27 private: 28 int size; 29 string* p; 30 }; 31 32 int main() 33 { 34 Text t1("hello"); 35 Text t2; 36 t2 = t1; //调用赋值运算符 37 }