且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

《C++编程惯用法——高级程序员常用方法和技巧》——2.2 赋值

更新时间:2022-09-22 16:07:17

本节书摘来自异步社区出版社《C++编程惯用法——高级程序员常用方法和技巧》一书中的第2章,第2.2节,作者: 【美】Robert B. Murray ,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.2 赋值

和复制构造函数一样,如果我们没有声明赋值操作符函数,C++编译器会自动为我们合成一个。缺省的赋值操作符会为每个数据成员进行赋值。缺省的赋值操作符的适用场合和缺省的复制构造函数一样。如果缺省的复制构造函数是错误的,那么缺省的赋值操作符几乎也可以被确定为错误的(反之亦然)。

赋值操作符和复制构造函数通常都有着几乎同样的逻辑。它们之间的主要区别在于:

当对象被自赋值时,赋值操作符必须可以工作;
每次对赋值操作符的调用都会改写一个已经存在了的值。如果在被改写的对象中使用了对象外的资源,那么我们可能需要释放这些资源;
赋值操作符可以向外返回一个值。
下面是String类的复制构造函数和赋值操作符:

String::String(const String& s)
: data(new char[strlen(s.data)+1]{
   strcpy(data,s.data);
}

const String&
String::operator=(const String&s){
   if(&s!= this){
    delete [] data;
    data = new char[strlen(s.data)+1];
    strcpy(data,s.data);
   }
   return*this;
}

请注意上面的赋值操作符。其中对于&s和this的比较是为了防范如下的代码:

String s;
s=s;

如果没有上面的比较,对于String对象的自赋值,我们将会先删除String中的数据,然后又试图对这些刚被删除的数据进行拷贝。我们在编写赋值操作符时,必须时刻考虑到自赋值的可能性,并确保自己的代码在这种情况下可能做出正常的反应(通常情况下是什么都不做)。我们可能不期望其他人编写这样的代码:

s = s;

但不管是使用引用还是指针的代码,我们必须保证它们拥有同样的效果。

一旦我们确信正在处理两个不同的对象,那么与复制构造函数不同的是,赋值操作符必须将原有值中所控制的资源释放掉。(这也就是我们代码中delete的作用。)在这之后,除那个返回值之外,赋值操作符的行为通常都和构造函数中的一致。我们通常都会把那个公有的逻辑提取出来放到一个私用的成员函数中去,并在复制构造函数和赋值操作符中调用这个函数;在本例中,这个公有的逻辑只是一个简单的函数调用,所以我们就直接复制了这个调用。

2.2.1 operator=的返回值

赋值操作符应该返回一个被赋值对象的常量引用。这使得用户可以写出如下的代码:

Complex x,y;
//…
x = y = Complex(0,0);

被重载的操作符和内建的操作符有着同样的优先级和结合顺序。=是右结合的,所以下面的赋值:

y=Complex(0, 0);

将会被首先执行,它所产生的结果又将会被赋值给x。

通过返回一个常量引用,我们就可以阻止将赋值结果作为左值的做法:

(a=b)=c;

这样做不但看起来比较奇怪,它所产生的结果同样也会让人大吃一惊。例如,假设a和c都引用到同样的目的,这样做会导致什么结果呢?

让赋值操作返回一个值也有着一个缺陷:即使我们并不使用它,我们也需要用一些代码来返回这个值:

a = b;//返回值未被使用

在实践中,这种做法几乎都是可取的。那些额外的代码通常都是一条简单的机器指令(记住:我们返回的是一个引用,它实际上也就是个指针,而不是一个全新的对象)。在大部分情况下,由这一条指令带来的性能影响是可以忽略不计的。如果真的是十分注重效率的话,那么赋值操作符应该被定义为inline;在此时,任何一个还过得去的优化器都可以优化掉这条没有被使用的指令。