重载运算符=返回值应该是值还是引用?

4

1)以下两者有何不同:

complex& operator = (const complex& c);

并且

complex operator = (complex c);

2) 如果我在第二种情况下定义了复制构造函数,它们是否相同?

complex::complex (const complex& c){

    //code
    }

3) const的作用是什么?

5个回答

5

它们非常不同

您想要链接赋值吗?返回* this

a = b = c; // Chained assignment

C++中的默认赋值支持链式操作,因此保持这个规则是一个好主意。

为了支持这种类型的赋值,每个操作都必须返回对 *this 的引用或返回一个副本:

complex& operator = (const complex& c)
{
  // do assignment

   return *this;
}

或者

complex operator = (const complex& c)
{
  // do assignment

   return *this;
}

但是,这个简单的工作应该是轻便高效的。如果没有RVO或移动可用,则更喜欢返回引用而不是副本。

 

我喜欢返回引用的另一个原因是为了摆脱我心中的困惑。看看以下两种情况:

// Returning a reference
a = 1;
b = 2;
c = 3;
(a = b) = c;

在赋值后,a=3b=2c=3。因为首先b分配给a并返回一个对a的引用,然后c分配给该引用并再次更改a

// Returning a copy
a = 1;
b = 2;
c = 3;
(a = b) = c;

在赋值后,a=2 b=2 c=3。因为首先b分配给a并返回一个临时副本,然后c分配给该临时对象(对ab没有影响)。
如您所见,结果是不同的。无论发生了什么和哪个更好,您都应该选择一种对程序员来说普遍和易于预期的方式。我相信通过观看(a=b)=c,C++程序员会期望得到第一个结果。
因此,您应该使用第一个结果。
关于const关键字:
当您通过引用将对象传递给函数时,函数可以修改它。您可以使用const来保护不受意外更改的对象。

1

1) 第一个版本是惯用的C++版本。它表示它接受参数的引用(而不是复制),并将返回对一个复杂对象的引用(而不是复制),通常是正在分配的对象(即*this)。

第二个版本可以工作,但它会产生不必要的副本,并且您无法调用被分配对象上的函数,例如:(a = b).dosth(),因为它返回了一个不同的对象。我的意思是它会编译,但它会在一个临时对象上调用dosth

2) 它们从未相同,但是如果您没有可访问的复制构造函数(或者有时是移动构造函数),第二个版本将无法工作。

3) const是一个承诺,表明您不会修改参数。因此,在a = b中,您不希望修改b,并且const强制执行该规则。在第二个版本(按值传递)中,您不需要它,因为您无论如何都不能修改它,因为它只是一份副本。


@kal 为什么你说它“总是一样快”?这取决于你能否很好地完成交换。如果你的函数被内联,那么编译器在需要时仍然可以执行移动语义,即使在引用情况下也一样。 - rabensky
@cluracan 如果你之后会直接复制对象,为什么要通过引用传递呢?这与按值传递相比没有任何优势,而且多了一行代码,效率(轻微)降低。 (请注意,我甚至没有谈论移动语义。) - Konrad Rudolph
@KonradRudolph 这里有一个例子:我有向量。它们都恰好是大小为100的。operator=会看到它有足够的内存来存储新值,因此它不需要分配内存。而你的复制-交换语义将进行分配(为新对象),复制,交换和释放内存。这要慢得多! - rabensky
我想你必须根据每个类别来考虑。如果您的类别可以便宜地交换,而且在特殊情况下可能无法进行优化,则复制+交换是可以的。如果您有瓶颈或者它不便宜交换或者可以进行优化,则使用传统方法。 - Kal
@Kal 你的代码是正确的。但那不是这段代码。函数输入的规则是不同的。一个const reference输入参数可以被绑定到一个临时变量上,而且如果代码被内联,它会被简单地替换为该临时变量或输入的非临时变量。 - rabensky
显示剩余14条评论

0
第二种形式采用复制而不是引用来传递和返回值,这通常是低效的。
complex operator=(complex c);

参数c作为原始副本传入。对于像int这样的简单数据类型,这并不重要,但如果是complex,那么这可能会导致不必要的分配/释放和影响性能。

通过引用传递,如下:

complex& operator=(complex& c);

这意味着传递给运算符的对象并没有被复制。

第二个区别在于它们返回的内容。运算符的正确形式是返回对自身的引用。你提供的第一种形式实际上返回了对象的副本,这可能会导致效率低下。

最后,再次强调,运算符的正确形式应该将输入对象作为一个const引用。这更多是一个风格问题,而不是性能问题——但人们不应该合理地期望赋值运算符修改输入对象,指定它为const可以防止这种情况发生。

因此,总结一下,

complex& operator=(const complex& c);

是正确的形式。


复制并不总是更低效。 - Kal
与其他答案一样,建议查找“复制并交换”惯用语。在operator=的情况下,按值传递通常是完全可以的,特别是如果对象很大(但可以便宜地交换)。然而,在complex类的情况下,可能不是这种情况。 - Konrad Rudolph

0

它们并不相同,第一个传递引用,因此对象在内存中保持不变,而第二个将需要复制构造函数,因为complex对象将在函数内部复制进出。第一个是规范的,也是你想要的,第二个没有被使用,因为它是不必要的浪费。


0

const 是一种表达“我保证不会改变 c!所以不用担心它!”的方式。这允许用户通过引用提供实际类(在您的情况下是 complex),而不必担心它会意外更改。

“引用” - 通过添加 & 符号选择 - 意味着此函数实际上使用用户提供的相同对象,来自相同的内存。由于它正在使用相同的对象,因此重要的是您承诺不更改该对象。

没有 & - 您的第二个示例 - 您不传递相同的对象,而是传递该对象的 副本。因此,const 是不必要的(尽管仍然合法),因为用户不关心您是否更改了副本。

区别?时间(对于大型类还有内存)。如果将对象的引用传递给函数 - 您无需复制该对象。这可以节省您的时间(复制对象所需的时间)和内存(对象需要的内存)。


有时候复制(即不使用引用)可能会更快,但通常不是这样。我认为只适用于小类。 - Kal
@kal 是的,你说得对。使用引用意味着你仍然需要复制指针本身 - 因此它确实需要一些时间和一些内存。如果你的对象比指针小,那么引用实际上会占用更多的内存。此外,在函数执行期间,引用会增加一个间接级别,因此它比将对象放在堆栈上要花费更长的时间。但这些是更高级需求的考虑因素 :) - rabensky
“operator=” 的整个意义就是进行复制。因此,按值传递就可以了(与 Kal 的答案相同的评论:查找“复制并交换”惯用语)。 - Konrad Rudolph
@KonradRudolph - 这需要一个有效的交换,而并非所有对象都具备或能够具备。此外 - 这仍然需要一个单独的交换,因此速度略慢。最后,如果该类的每个实例不论如何都需要大量数据块(没有共享的原因),则将具有更大的内存印记。所以...不,这不是一种优越的方法。 - rabensky
@KonradRudolph 如果您确实想只编写一次副本,请仅编写 operator= 并在复制构造函数中使用它(通过执行 *this=other)。这比另一种方式要好得多(因为它不需要高效的交换,并且不会创建3个不同的对象)。 - rabensky
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接