您的位置:首页 > 编程语言 > C语言/C++

Effective C++ rule 10-11 赋值运算符注意点

2017-08-01 10:40 134 查看

前言

条款10和11都是在讲赋值操作符的一些注意事项,尤其是operator=在处理“自我赋值”时候应该注意的一些东西。

operator= 返回一个指向*this的引用

Operator返回引用,只要是因为,我们习惯看到连等的使用。比如:

Float x,y,z;

我们希望能够:x=y=z=10,因为这样子很方便,也很符合我们的数学中的常识。

x=y=z=10,等效于:

z=10;
y=z;
x=y;


X=Y=Z=10,这条语句能够以上面的逻辑正确执行的基础是:

1. 赋值运算具有返回值,返回值是赋值符号右边表达式的值。

2. 赋值运算是右结合的,X=Y=Z=10,其实是:X=(Y=(Z=10))的缩写。

因此,如何我们想为我们的类重载赋值运算符,那么我们最好也实现这个连等的特性,这个只是约定俗成,并不强制性。为了实现连等,我们需要让赋值运算符具有返回值。

例如:Code1.0

#include <stdio.h>
#include <iostream>
class complexy
{
public:
complexy(double _real=0,double _img=0)
{
real = _real, img =_img;
}
complexy operator=(const complexy& rhs)
{
this->real = rhs.real;
this->img = rhs.img;
return *this;
}
public:
double real;
double img;
};

int main()
{
complexy x, y, z(1,1);
x = y = z;
printf("X:%lf,%lf.\n", x.real, x.img);
printf("Y:%lf,%lf.\n", y.real, y.img);
printf("Y:%lf,%lf.\n", z.real, z.img);
system("pause");
return 0;
}


输出:

X:1.000000,1.000000.
Y:1.000000,1.000000.
Y:1.000000,1.000000.


Ok,这样是达成了我们的连等的目标,但是,这样的效率不高。主要是因为每次返回的是一个对象,这将调用对象的复制构造函数。在上面的基础上,我们在复制构造函数里面加上东西。

Code1.2:

#include <stdio.h>
#include <iostream>
class complexy
{
public:
complexy(double _real=0,double _img=0)
{
real = _real, img =_img;
}
complexy& operator=(const complexy& rhs)
{
this->real = rhs.real;
this->img = rhs.img;
return *this;
}
complexy(complexy&rhs)
{
printf("call copy construct func.\n");
this->img = rhs.img;
this->real = rhs.real;
}
public:
double real;
double img;
};

int main()
{
complexy x, y, z(1,1);
x = y = z;
printf("X:%lf,%lf.\n", x.real, x.img);
printf("Y:%lf,%lf.\n", y.real, y.img);
printf("Y:%lf,%lf.\n", z.real, z.img);
system("pause");
return 0;
}


输出:

call copy construct func.
call copy construct func.
X:1.000000,1.000000. Y:1.000000,1.000000. Y:1.000000,1.000000.


这主要是在:y=z时,执行完return*this,返回时生成一个匿名对象,这个匿名对象是由复制构造函数初始化的,同理x=y返回时又发生了一次。

出于C++对效率的追求,于是 我们在赋值运算符加上返回引用。

Code 1.3

#include <stdio.h>
#include <iostream>
class complexy
{
public:
complexy(double _real=0,double _img=0)
{
real = _real, img =_img;
}
complexy & operator=(const complexy& rhs)
{
this->real = rhs.real;
this->img = rhs.img;
return *this;
}
complexy(complexy&rhs)
{
printf("call copy construct func.\n");
this->img = rhs.img;
this->real = rhs.real;
}
public:
double real;
double img;
};

int main()
{
complexy x, y, z(1,1);
x = y = z;
printf("X:%lf,%lf.\n", x.real, x.img);
printf("Y:%lf,%lf.\n", y.real, y.img);
printf("Y:%lf,%lf.\n", z.real, z.img);
system("pause");
return 0;
}


输出:

X:1.000000,1.000000.
Y:1.000000,1.000000.
Y:1.000000,1.000000.


这是因为,返回引用,返回的是指向*this的引用,期间并没有匿名对象的生成。

所以,为了提高效率及实现连等,operator=一般设计为返回一个指向*this的引用。

在operator=中处理“自我赋值”

自我赋值,顾名思意就是自己给自己赋值,这通常不能引起注意,我们经常会很容易忽视自我赋值这一特殊情况,而遇到一些奇妙的错误。例如下面的函数:


void swap(int &x, int &y)
{
x = x + y;
y = x - y;
x = x - y;
}


这个函数的本意是交换两个数的内容,一种无需临时变量的比较装逼的写法。

假设x+y的和为s

则:

x = x + y;=>x等于s
y = x - y;=>y等于s减去y,也即等于用x+y的和减去y,其实就是x的值,这里y变量已经是x的值了
x = x - y;=>x等于s减去y,用他们的和减去变量y的值,而y又是原来x的值,所以这里x等于y的值。


上面以十分巧妙地方式完成了x,y的值互换,但是这种写法潜在着一种危险。

假设存在这样的调用语句:

T=10;
Swap(T,T);


也就是swap的两个参数是指向同一个变量的引用,那就GG了。

此时:

x = x + y;=>T变成原来的两倍,所以T=2T,又因为x,y指向T,所以x是2T,y也是2T.

y = x - y;=>因为x指向T,y也指向T,等效于y=T-T,所以此时,运算后y变成了0,那么x,T都是0了。

x = x - y;=>等效于x=0-0


虽然,一般我们不会写出swap(T,T)的这么2的调用来,可是,万一是两个指向T的指针呢?我们以为这两个指针不同,但两个指针是很有可能会指向同一个变量。而且,swap函数也的确存在这个Bug,不应该因为没人写出那样的调用语句就假装这个bug不存在。

解决这个自我复制的问题的一个很简单粗暴的方式就是在函数的最开始加上判断两个对象是否相同的测试,查看他们的地址是否一致。

例如:

void swap(int &x, int &y)
{
if (&x == &y) return;
x = x + y;
y = x - y;
x = x - y;
}


即可。

再举一个例子:

class Bitmap{···};
Class Widget{
···
Private:
Bitmap *pb;//动态内存对象
};


其中有一个赋值函数:

Widget & Widget::operator=(const Widget& rhs)
{
Delete this->pb;
This->pb=new Bitmap(*rhs.pb);
Return * this;
}


大家可以想想 这个函数会有什么问题。这个例子当当加上证同测试还不足够,任有所谓的“异常安全性”问题。

总结

我们需要确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象是,其行为任然是正确的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: