1.概念
1)“三”是指拷贝构造函数、拷贝赋值运算符、析构函数这三者之间关系,“五”是在前面三个的基础之上再加上移动构造函数、移动赋值运算符这两个
2.法则一:需要自定义析构函数的类也需要自定义拷贝和赋值操作
1)当类含有指针成员,构造函数new了一块动态内存并让这个指针成员指向它,这个类析构时就需要执行delete去释放指针指向的内存,但是合成析构函数不会delete这个指针,所以这种类需要自定义析构函数
2)同时,这种类在发生拷贝或者赋值时,如果执行合成拷贝构造函数和合成拷贝赋值运算符,只会进行简单的浅拷贝,会使得多个类对象的指针成员指向同一块内存;这样的话,当某个类对象结束了它的生命周期,会执行它自定义的析构函数,delete它的指针成员,于是这块内存便被释放了,那么此时其他的类对象的指针成员将成为空悬指针,或者多个类对象同时被销毁时,这块内存会被delete多次,这都是会发生错误的
3)浅拷贝:在进行类对象拷贝时,如果类对象含有指针成员,浅拷贝是仅仅拷贝了一个指针,让这个指针也指向原来已经存在的内存
4)深拷贝:深拷贝是生成一个指针,并新开辟了一块内存,把原来那块内存上的内容拷贝过去,并让新生成的指针指向这块新内存
5)默认拷贝构造函数,进行的是浅拷贝,会发生上述所说的错误,所以要自定义拷贝构造函数和拷贝赋值运算符,进行深拷贝
class HasPtr { public: HasPtr(const string& s = string()) : i(0) { ps = new string(s); } //自定义拷贝构造函数 HasPtr(const HasPtr& hp) : i(hp.i) { ps = new string(*hp.ps); } //自定义拷贝赋值运算符 HasPtr& operator = (const HasPtr& rhs_hp) { if (this != &rhs_hp)
{ string *temp_ps = new string(*rhs_hp.ps);//new在delete前,为了保证安全性,如果new不出来,那么异常就在这发生了,下面那行delete就不不会发生了 delete ps; ps = temp_ps; i = rhs_hp.i; }//temp_ps离开了它的作用域,指针本身被销毁,但是它指向的动态内存不会被销毁,因为没有执行delete return *this; } //自定义折构函数 ~HasPtr() { delete ps; } private: string *ps; int i; };
6)基类的析构函数可以不遵循此法则:一个基类总是需要虚折构函数来防止内存泄露
- 首先要明确的是,每个析构函数只会清理自己所在层级创造的成员
- 当delete一个指向派生类对象的基类指针时,如果基类的析构函数没有定义成虚函数,则编译器实现静态绑定,在delete这个基类的指针时,只会执行基类的析构函数,释放基类成员,派生类成员得不到释放,此时会导致释放内存不完全,导致内存泄露
3.法则二:需要自定义拷贝操作的类也需要自定义赋值操作,反之亦然
1)一个类有一个数据成员代表编号,当需要这个类的每一个类对象都有一个唯一的编号,此时如果仅仅使用合成拷贝构造函数的话,发生拷贝时,拷贝出来的对象的编号肯定就一样了,这个时候就需要自定义拷贝构造函数
2)这种情况下需要自定义拷贝构造函数,那肯定也要自定义拷贝赋值运算符,反过来也是一样的道理
4.法则三:一般来说,如果一个类定义了任何一个拷贝控制成员,它就应该定义所有五个拷贝控制成员
1)如前所述的类必须定义析构函数、拷贝构造函数、拷贝赋值运算符才能正常工作,这些类通常拥有一个资源,拷贝成员会拷贝资源,一般来说,拷贝资源会导致一些额外开销,在某些情况下,移动构造函数和移动赋值运算符能不开辟新内存完成对对象的移动,减少开销,所以定义它俩有必要