条款10和条款13

162 阅读2分钟

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