首页 > 代码库 > 《Effective C++》构造/析构/赋值 函数:条款10-条款12

《Effective C++》构造/析构/赋值 函数:条款10-条款12

条款10:令operator=返回一个reference to *this

赋值操作符运算是由右向左运算的。例如一个连锁赋值
<span style="font-size:14px;">int x, y, z;
x=y=z=15;</span>

编译器解释时时这样的:
x=(y=(z=15));
先给z赋值,用赋值后的z再给y赋值,用赋值后的y再给x赋值。
为了实现连锁赋值,操作符必须返回一个reference指向操作符左侧的实参。
其实,如果operator=不返回一个引用,返回一个临时对象,照样可以实现连锁赋值。但这个临时对象的构建会调用拷贝构造函数。看下面这个例子:
#include<iostream>
using namespace std;

class Widget
{

public:
	Widget()
	{
		cout<<"Default Ctor"<<endl;
	}
	Widget(const Widget& rhs)
	{
		cout<<"Copy Ctor"<<endl;
	}
	Widget& operator=(const Widget& rhs)
	{
		cout<<"operator="<<endl;
		return *this;
	}
};
int main()
{
	Widget a,b,c;
	a=b=c;
	return 0;
}
这样输出为:
Default Ctor
Default Ctor
Default Ctor
operator=
operator=
如果把operator=返回的引用去掉,改为Widget operator=(const Widget& rhs)
则会输出:
Default Ctor
Default Ctor
Default Ctor
operator=
Copy Ctor
operator=
Copy Ctor
返回临时对象,临时对象再给左侧变量赋值。多出了一步,浪费资源。

operator=是改变左侧操作数的,与其类似的operator+=、operator-=等改变左侧操作符的运算,都应该返回引用。这是一个协议,应该去遵守。

条款11:在operator=中实现“自我赋值”

自我赋值是指对象给自己赋值。
Widget w;
w=w;
这样看起来有点愚蠢,但是它合法。上面这个例子很容易发现自我赋值。但有时候就不那么容易了,例如:数组a
a[i]=a[j];
当i和j相等时,就是自我赋值。
例如,两个指针px和py
*px=*py;
如果两个指针指向同一个对象,这也是自我赋值。
除此之外,还有引用。更加隐晦的自我赋值发生在基类和派生类层次中,不同类型的指针或引用之间的赋值都有可能发生自我赋值。

如果遵循条款13和条款14,运用对象来管理资源,确定“资源管理对象”在copy发生时有正确的举措,这样自我赋值是安全的。如果自己管理资源,可能会“在停止使用资源之前意外释放了它”。
例如使用一个class管理一个指针。
class Widget
{
public:
	Widget& operator=(const Widget& rhs)
	{
		delete p;
		p=new int(ths.p);
		return *this;
	}
	int *p;
};
如果上面代码自我赋值,在使用指针p之前已经将其释放掉了。
防止这种问题发生的办法是“证同测试”,在删除前判断是不是自我赋值
class Widget
{
public:
	Widget& operator=(const Widget& rhs)
	{
		if(this==&rhs)//证同测试
			return *this;
		delete p;
		p=new int(rhs.p);
		return *this;
	}
	int *p;
};
这个版本的operator=可以解决自我赋值的问题。但是还有个问题:异常安全。如果delete p成功,而p=new int(rhs.)失败会发生什么?
这时,widget对象会持有一个指针,这个指针指向了被释放的内存。下面方法可以实现异常安全。
class Widget
{
public:
	Widget& operator=(const Widget& rhs)
	{
		int tmp=p;//记录原先内存
		p=new int(rhs.p);
		delete tmp;//释放原先内存
		return *this;
	}
	int *p;
};
在实现异常安全的同时,其实也获取了自我赋值的安全。如果p=new int(ths.p)发生异常,后面的delete tmp就不会执行。
如果你很关心效率,可以把“证同测试”放到函数起始处。但是“自我赋值”发生的频率有多高?因为“证同测试”也需要成本,因为它加入了新的控制分支。
还有一个替代方案是:copy and swap技术。这个技术和异常安全关系密切,条款29详细说明。下面看它怎么实现
class Widget
{
public:
	void swap(const Widget& rhs);//交换rhs和this
	Widget& operator=(const Widget& rhs)
	{
		Widget tmp(rhs);//赋值一份数据
		swap(tmp)//交换
		return *this;//临时变量会自动销毁
	}
	int *p;
};
如果赋值操作符参数是值传递,那么就不需要新建临时变量,直接使用函数参数即可。
class Widget
{
public:
	void swap(const Widget& rhs);//交换rhs和this
	Widget& operator=(const Widget rhs)
	{
		swap(rhs)
		return *this;
	}
	int *p;
};
这个做法代码可读性比较差,但是将“copying动作”从函数体内移到“函数参数构造阶段”,编译器有时会生成效率更高的代码(by moving the copying operation from the body of the function to construction of the parameter, it‘s fact that compiler can sometimes generate more efficient code.


条款12:复制对象时勿忘其每一部分

在一个类中,有两个函数可以给复制对象:复制构造函数和赋值操作符,统称为copying函数。在条款5中讲到,如果我们自己不编写者两个函数,编译器会帮我们实现这两个函数,编译器生成的版本会将对象的所有成员变量做一份拷贝。编译器生成的copying函数的做法通常是浅拷贝。可以参考这里。
如果我们自己实现了copying函数,编译器就不再帮我们实现。但是编译器不会帮我们检查copying函数是否给对象的每一个变量都赋值。
下面有一个消费者的类
class Cutsomer
{
public:
	Cutsomer()
	{
		name="nobody";
	}
	Cutsomer(const Cutsomer& rhs)
		:name(rhs.name)
	{
		cout<<"Customer Copy Ctor"<<endl;
	}
	Cutsomer& operator=(const Cutsomer& rhs)
	{
		cout<<"assign operator"<<endl;
		name=rhs.name;
		return *this;
	}
private:
	string name;
};
这样的copying函数没有问题,但是如果再给类添加变量,例如添加一个电话号码
class Cutsomer
{
……
private:
	string name;
	string telphone;
};
这时copying函数不做更改,即便是在最高警告级别,编译器也不会报错,但是我们的确少拷贝了内容。
由此可以得出结论:一旦给类添加变量,那么自己编写的copying函数也要修改,因为编译器不会提醒你。
在派生类层次中,这样的bug更难发现。假如有优先级的客户类,它继承自Customer
class PriorityCustomer:public Cutsomer
{
public:
	PriorityCustomer()
	{
		cout<<"PriorityCustomer Ctor"<<endl;
	}
	PriorityCustomer(const PriorityCustomer& rhs)
		:priority(rhs.priority)
	{
		cout<<"PriorityCustomer Copy Ctor"<<endl;
	}
	PriorityCustomer& operator=(const PriorityCustomer& rhs)
	{
		cout<<"PriorityCustomer assign operator"<<endl;
		priority=rhs.priority;
		return *this;
	}
private:
	int priority;
};
在PriorityCustomer的copying函数中,只是复制了PriorityCustomer部分的内容,基类内容被忽略了。那么其基类内容部分怎么初始化的呢?
在派生类中构造函数没有初始化的基类部分是通过基类默认构造函数初始化的(没有默认构造函数就会报错)。
但是在copy assignment操作符中,不会调用基类的默认构造函数,因为copy assignment只是给对象重新赋值,不是初始化,因此不会调用基类的构造函数,除非我们显示调用。
正确的PriorityCustomer的copying函数应该这样写:
PriorityCustomer(const PriorityCustomer& rhs)
		:Cutsomer(rhs),priority(rhs.priority)
	{
		cout<<"PriorityCustomer Copy Ctor"<<endl;
	}
	PriorityCustomer& operator=(const PriorityCustomer& rhs)
	{
		cout<<"PriorityCustomer assign operator"<<endl;
		Cutsomer::operator=(rhs);
		priority=rhs.priority;
		return *this;
	}

可以发现复制构造函数和赋值操作符有类似的代码。但是者两个函数不能相互调用。复制构造函数是构造一个不存在的对象,而赋值操作符是给一个存在的对象重新赋值。消除重复代码的方法编写一个private方法,例如void Init()。在这个函数中操作重复代码。

《Effective C++》构造/析构/赋值 函数:条款10-条款12