条款10:令operator= 返回一个reference to *this
首先来看一个例子:
class A { public: A() { cout << "defalut constructor" << endl; } ~A() { cout << "destructor" << endl; } void operator=(const A &a); int number; }; void A::operator=(const A &a) { this->number = a.number; return; } int main() { A a; a.number = 5; A a1; a1 = a; cout << a1.number << endl; return 0; }
运行这段程序会发现程序并没有报错,并且输出了正确的结果。当然例子比较简单,只进行了一次赋值操作,但如果我们进行连等赋值时,就像这样:
int main() { A a; a.number = 5; A a1; A a2; a2 = a1 = a; cout << a2.number << endl; return 0; }
会发现程序报错了:
error: 二进制“=”: 没有找到接受“void”类型的右操作数的运算符(或没有可接受的转换)
返观上面我们实现的赋值运算符,发现当完成赋值操作时返回void。按照从右向左赋值的操作a1 = a没有毛病,但是它们结果返回了一个void,而a2 = void显然是一个非法的操作,因为它与我们赋值运算符要求的参数类型不一致。因此,我们应该给a2赋值一个A类型的对象,比如说a1。因此我们又有如下代码:
class A { public: A() { cout << "defalut constructor" << endl; } ~A() { cout << "destructor" << endl; } A operator=(const A &a); int number; }; A A::operator=(const A &a) { this->number = a.number; return *this; } int main() { A a; a.number = 5; A a1; A a2; a2 = a1 = a; cout << a2.number << endl; return 0; }
这时代码又行了,因为a1 = a执行后,返回了a1对象,将a1再给a2没有问题。那这不是解决了连等赋值的问题吗。为什么要返回引用呢?
当我们观察上面程序输出时发现:
defalut constructor defalut constructor defalut constructor destructor destructor 5 destructor destructor destructor
三个A类型的对象对应三次默认构造函数没有问题,而下面为什么析构了五次呢?让我们打印一下地址:
class A { public: A() { cout << this << " : defalut constructor" << endl; } ~A() { cout << this << " : destructor" << endl; } A operator=(const A &a); int number; };
程序输出:
000000F14274F504 : defalut constructor 000000F14274F524 : defalut constructor 000000F14274F544 : defalut constructor 000000F14274F644 : destructor 000000F14274F624 : destructor 5 000000F14274F544 : destructor 000000F14274F524 : destructor 000000F14274F504 : destructor
三个A类型对象的构造函数和析构函数,根据地址都可以对应起来,中间两个析构函数是哪里来的呢?
这是因为发生了拷贝操作,因为我们的赋值运算符返回的是值,而不是引用。
来验证一下:
class A { public: A() { cout << this << " : defalut constructor" << endl; } ~A() { cout << this << " : destructor" << endl; } A(const A& a) { this->number = a.number; cout << "&a : " << &a << endl; cout << this << " : copy constructor" << endl; } A operator=(const A &a); int number; }; A A::operator=(const A &a) { this->number = a.number; cout << "&a : " << &a << endl; cout << this << " : = operator" << endl; return *this; }
程序输出:
000000AD0AAFF7E4 : defalut constructor 000000AD0AAFF804 : defalut constructor 000000AD0AAFF824 : defalut constructor &a : 000000AD0AAFF7E4 000000AD0AAFF804 : = operator &a : 000000AD0AAFF804 000000AD0AAFF904 : copy constructor &a : 000000AD0AAFF904 000000AD0AAFF824 : = operator &a : 000000AD0AAFF824 000000AD0AAFF924 : copy constructor 000000AD0AAFF924 : destructor 000000AD0AAFF904 : destructor 5 000000AD0AAFF824 : destructor 000000AD0AAFF804 : destructor 000000AD0AAFF7E4 : destructor
仔细看一些地址变化,是不是一切都迎刃而解了。赋值运算符完成后会发生一个拷贝行为,拷贝给一个临时变量
,然后再用临时变量赋值。当赋值操作很长时,也就意味着会产生很多临时变量,那我们为何不直接返回引用呢,这也正是这条准则说的。
让我们看一下返回引用的结果:
class A { public: A() { cout << this << " : defalut constructor" << endl; } ~A() { cout << this << " : destructor" << endl; } A(const A& a) { this->number = a.number; cout << "&a : " << &a << endl; cout << this << " : copy constructor" << endl; } A &operator=(const A &a); int number; }; A &A::operator=(const A &a) { this->number = a.number; cout << "&a : " << &a << endl; cout << this << " : = operator" << endl; return *this; }
程序输出:
000000FECFFEF824 : defalut constructor 000000FECFFEF844 : defalut constructor 000000FECFFEF864 : defalut constructor &a : 000000FECFFEF824 000000FECFFEF844 : = operator &a : 000000FECFFEF844 000000FECFFEF864 : = operator 5 000000FECFFEF864 : destructor 000000FECFFEF844 : destructor 000000FECFFEF824 : destructor
可以发现中间省略了拷贝行为,提高了效率。
当然这个条款不仅适用于 = ,还有其他的赋值相关运算符比如 += 等。
条款13:以对象管理资源
场景:
void func() { Example *example = new Example(); if (条件满足) { //这里也应该回收资源 return; } delete example; return; }
上诉代码中如果if条件成立函数直接return ; 没有释放内存,导致了内存泄漏。上述例子在开发中极易遇到,尤其是逻辑较复杂时,很容易忽略这样的细节。对此有一些解决方案,比如说使用goto语句,在结尾处统一释放内存,但实际开发中并不提倡使用goto语句。还有一种do…while(0)的妙用:
void func() { Example *example = new Example(); do { if (条件满足) { break; } }while(0); delete example; return; }
这里巧妙的用到了break的特性。还有一种RAII(资源获取就是初始化)是指拿到资源后初始化,当不需要资源时,自动释放该资源。比如我们可以用一个单独的类管理资源,当出了作用域后,自动调用该类的析构函数释放资源。(以上参考《c++服务器开发精髓》 张远龙 著 一书)
方法多种多样,然而c++中提供了一种智能指针来管理资源
比如auto_ptr
//需要引入头文件#include <memory> class A { public: A() { cout << "defult constructor" << endl; } ~A() { cout << "destructor" << endl; } int number; }; int main() { std::auto_ptr<A> a(new A()); if (true) { return 0; } return 0; }
当我们用智能指针管理对象时,无论程序什么时候退出,都能正确的析构。
如上程序输出:
defult constructor destructor
auto_ptr的一个特性是,它在被销毁时会自动删除它所指之物,因此不能让多个auto_ptr同时指向同一对象,这是一种错误的行为:
int main() { A *a = new A(); std::auto_ptr<A> autoptr(a); std::auto_ptr<A> autoptr1(a); return 0; }
为了避免这个问题,auto_ptr在发生拷贝或赋值操作时,原来的指针将指向空,而复制的指针将取得资源的唯一拥有权。
看一个例子:
class A { public: A() { cout << "defult constructor" << endl; cout << "&a : " << this << endl; } ~A() { cout << "destructor" << endl; } }; int main() { A *a = new A(); std::auto_ptr<A> autoptr(a); cout << "autoptr.get() : " << autoptr.get() << endl; std::auto_ptr<A> autoptr1(autoptr); cout << "autoptr1.get() : " << autoptr1.get() << endl; cout << "autoptr.get() : " << autoptr.get() << endl; std::auto_ptr<A> autoptr2; autoptr2 = autoptr1; cout << "autoptr2.get() : " << autoptr2.get() << endl; cout << "autoptr1.get() : " << autoptr1.get() << endl; return 0; }
程序输出:
defult constructor &a : 00000215BE4A7DD0 autoptr.get() : 00000215BE4A7DD0 autoptr1.get() : 00000215BE4A7DD0 autoptr.get() : 0000000000000000 autoptr2.get() : 00000215BE4A7DD0 autoptr1.get() : 0000000000000000 destructor
非常直观吧
但是回过来想想,发生拷贝尽然是掠夺行为,这显然不太合适,因此c++中还有一种shared_ptr
它是通过引用计数的方式来管理资源,详情参考我写的关于实现一个智能指针shared_ptr
最后引用一下书中的总结:
由于tr1::shared_ptrs的复制行为“一如预期”,它们可被用于STL容器以及其他“auto_ptr之非正统复制行为并不适用“的语境上。 尽管如此,本条款并不专门针对auto_ptr,tr1::shared_ptr或任何其他智能指针,而只是强调”以对象管理资源“的重要性, auto_ptr和tr1::shared_ptr只不过是实际例子。