条款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只不过是实际例子。