《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()。在这个函数中操作重复代码。

时间: 2024-10-15 11:18:02

《Effective C++》构造/析构/赋值 函数:条款10-条款12的相关文章

[Effective C++]构造/析构/赋值运算

条款05:了解C++默默编写了并调用了那些函数 请记住: 编译器可以暗自为class 创建default构造函数,copy构造函数,copy assignment 操作符,以及析构函数 class Empty { public: Empty(){...} //default constructor Empty(const Empty& rhs){...} //copy constructor ~Empty(){...} //destructor Empty& operator=(const

c++笔记:const、初始化、copy构造/析构/赋值函数

构造函数 Default构造函数:可被调用而不带任何实参的构造函数,没有参数或每个参数都有缺省值.如: class A { public: A(); }; 将构造函数声明为explicit,可阻止它们被用来执行隐式类型转换,但仍可用来进行显示类型转换.如: class B { public: explicit B(int x = 0, bool b = ture); }; copy构造函数:用于以同型对象初始化自我对象,以passed by value的方式传递对象:· copy assignm

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

每一个类中都有构造函数.析构函数.赋值操作符.这几个函数是一个类最根本的函数,它控制着创建对象并初始化.对象消亡时的清理以及摆脱旧值赋新值.这样函数如果有问题,那么影响极为严重. 条款5:了解C++默认编写并调用哪些函数 加入编写一个空类,那么经过编译之后,C++默认编写了哪些函数. class Empty{ }; 经过编译器处理后会有默认构造函数.复制构造函数.赋值操作符和析构函数.这些函数都是public且inline. class Empty{ public: Empty(){} Empt

C++构造/析构/赋值函数

在编写C++程序的时候,我们会为特定某一类对象申明类类型,几乎我们申明的每一个class都会有一个或多个构造函数.一个析构函数.一个赋值运算符重载=.以及拷贝构造函数.这些函数控制着类对象的基础操作,确保新定义的对象的初始化.完成对象撤销时的清理工作.赋予对象新值.如果这些函数的操作出错,则会导致严重的后果,所以确保这些函数的操作行为正常是非常重要的. 一.编译器默认生成的函数 如果我们编写一个空类,编译器会为我们默认生成构造函数.析构函数.赋值运算符.拷贝构造函数. 例如当我们定义 class

Effective C++ —— 构造/析构/赋值运算(二)

条款05 : 了解C++默默编写并调用哪些函数 水电费 条款02 : 尽量以const,enum,inline 替换#define 水电费 条款02 : 尽量以const,enum,inline 替换#define 水电费 条款02 : 尽量以const,enum,inline 替换#define 水电费

Effective C++ -- 构造析构赋值运算

05.了解C++默默编写并调用哪些函数 编译产生的析构函数时non-virtual,除非这个类的基类析构函数为virtual 成员变量中有引用和const成员时,无法自动生成copy assignment函数 基类将copy assignment操作符声明为private时,编译器拒绝为其derived classes生成一个copy assignment操作符. 06.若不想使用编译器自动生成的函数,就该明确拒绝 将自动生成的默认构造函数,拷贝构造函数,copy assignment声明为pr

Effective C++笔记:构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数 默认构造函数.拷贝构造函数.拷贝赋值函数.析构函数构成了一个类的脊梁,只有良好的处理这些函数的定义才能保证类的设计良好性. 当我们没有人为的定义上面的几个函数时,编译器会给我们构造默认的. 当成员变量里有const对象或引用类型时,编译器会不能合成默认的拷贝赋值函数:当一个基类把它的拷贝赋值函数定义为private时,它的派生类也不无生成默认的拷贝赋值函数,因为它无法完成基类成份的赋值. 条款06:若不想使用编译器自动生成的函数,就该明确拒绝 将拷贝构

《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(2)-读书笔记 <Effective C++>第8章 定制new和delete-读书笔记 条款09:绝不在构造和析构过程中调用virtual函数 你不该在构造和析构函数期间调用virtual函数,因为这样的调用不会带来你预期的结果. (1)在der

《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effective C++>第8章 定制new和delete-读书笔记 条款05:了解C++默默编写并调用哪些函数 当C++处理过一个空类后,编译器就会为其声明(编译器版本的):一个拷贝构造函数.一个拷贝赋值运算符和一个析构函数.如果你没有声明任何构造函数,编译器还会声明一个默认构造函数.所有这些函数都被声明为pub