为什么 C++ 中的赋值运算符重载必须返回引用

2025年2月11日 | 阅读 7 分钟

在 C++ 中,**运算符重载** 是为用户定义的类型(如类和结构体)上的内置运算符定义新含义的过程。通过重载运算符,我们可以设计出更自然、更易于理解的代码,使其在处理 +、-、=、== 等运算符时,其行为与标准类型类似。

例如,假设存在一个表示复数的类。如果未实现运算符重载,则在想对两个复数进行相加时,您可能需要编写类似 add(c1, c2) 的代码。但如果重载了 + 运算符,那么 c1 + c2 的运算就更加直观,并且符合数学中的加法运算。这种语法糖的好处首先是使代码更易于理解,因为这些运算看起来很熟悉。

运算符重载并非定义新运算符,而是允许现有运算符用于用户定义的类型。重载运算符时,应避免扭曲运算符的实际含义和用途。过度使用运算符重载可能会产生一些负面影响,导致长且不清晰的运算符序列,因此在重载运算符时,必须确保其逻辑清晰。

理解赋值运算符

**赋值运算符 (=)** 可能是 C++ 语言中最常用的运算符之一。它用于将一个对象的值复制到另一个对象。对于整数、浮点数等内置类型,赋值运算符仅通过将运算符右侧的操作数复制到运算符左侧来工作。例如,在语句 int a = 5; int b = a; 中,a 的值被赋给了 b。

  • 当处理类等用户定义类型时,如果编译器提供的默认行为不满足要求,则必须重载赋值运算符。编译器自动提供的赋值运算符执行的是浅拷贝,它会将一个对象的所有成员变量复制到另一个对象。
  • 尽管对于简单的类来说,这种默认行为可能有效,但当类包含动态内存、资源和指针等特性时,可能会出现问题。
  • 假设定义了一个可以处理动态数组的类。如果您依赖于默认的赋值运算符,可能会出现双重释放或实际内存损坏等问题,因为实际对象可能指向相同的内存地址。
  • 为了正确处理这种情况,必须使用运算符重载,特别是赋值运算符,其操作应该是创建一个数据的全新副本,而不是复制链接数据的地址。

返回引用的重要性

重载赋值运算符时,必须返回当前对象的引用 (*this) 或指向当前对象的 const 引用。这不仅仅是一个约定俗成的习惯,更是满足关键的基本语言操作以及防范 C++ 中常见陷阱的必要条件。

  • 支持链式赋值
    • 返回引用的一项目的是支持链式赋值。例如,STL 容器返回迭代器,以便将新值传递给程序的下一部分。链式赋值是指如 a = b = c 这样的表达式,只有当表达式 b = c 的输出应用于 a 时才能使用。
    • 换句话说,为了给赋值运算符添加此功能,它必须返回对赋值表达式左侧对象的引用。当运算符返回 *this 时,当前对象的引用会沿着链传递,从而使整个表达式能够被正确评估。
    • 如果赋值运算符不返回引用,则在链式赋值的情况下,赋值将无法按预期进行。操作将失败或产生不正确的行为,因为没有办法将链中多个赋值的结果关联起来。
  • 与内置类型的保持一致
    • 在可能的情况下,内置类型会从赋值运算符返回引用,在重载运算符时复制相同的行为可以使代码整洁,并与 C++ 的其他部分保持一致。您的类的用户期望它像内置类型一样工作;因此,赋值运算符也应该有类似的操作。当您重载的赋值运算符返回引用(它应该这样做)时,它就可以满足这些期望,而不会令使用该类的用户感到意外。
  • 这样做是因为 int 类型的赋值运算符会返回对赋值表达式左侧的引用。通过重载类的赋值运算符来尽可能地模仿这种行为,可以使其行为尽可能地可预测和直观。
  • 安全处理自赋值
    • 自赋值发生在将一个对象赋给自己时,例如 a = a; 这种情况可能会导致资源使用或数据错误,如内存泄漏或损坏。在一个正确实现的赋值运算符中,应该检测到自赋值并进行处理,以避免这些问题。
    • 赋值运算符应返回 *this,因为这允许它包含一种方法来防止自赋值,并安全地处理它,以确保对象的连贯状态。
  • 避免不必要的对象副本
    • 在 C++ 中重载赋值运算符时,有几个重要的考虑因素,其中之一是效率问题,更具体地说,是需要最小化对象副本的数量。在 C++ 中,如果您按值返回一个对象(例如,通过将其作为函数的值),则会创建一个对象的副本。虽然这种行为有时是有益的,但在重载赋值运算符时可能会产生不利影响。
    • 如果赋值是按值进行的,那么每次赋值都会涉及生成一个临时对象。对于大型对象或拥有动态内存、文件句柄、网络连接等资源的对象,这会增加大量开销,导致速度变慢和内存使用量增加。此外,在这种情况下,像指针这样的资源可能会引起深拷贝问题、内存泄漏以及在处理副本不当时的其他小问题。
    • 返回当前对象的引用 (*this) 也意味着不需要创建任何这些临时副本。它提供了一个返回对象引用的选项,以便可以直接操作对象而不是创建新对象,从而节省时间。特别是在性能高度敏感的应用程序中,底层细节往往很重要。

示例代码:不带引用的赋值运算符

输出

 
Hello 
Hello 
Hello    

示例代码:带引用的正确实现

输出

 
Hello 
Hello 
Hello     

结论

在 C++ 中,将赋值运算符重载为指向当前对象的引用 (*this) 在以下几个方面具有重要意义。首先,它支持链式赋值,这是大多数其他语言不支持的;像 a = b = c 这样的表达式是可能的,其中引用会沿着链传递,并且每次赋值都能得到正确的评估。其次,它们与内置类型保持一致,这意味着它们将以有序且预期的方式执行功能。第三,它使得所需的自赋值操作是安全的,而在没有它时,可能会导致内存损坏等问题。最后,返回引用不需要创建额外的对象副本,从而提高了程序的效率,尤其是在使用大型对象或管理动态内存等资源时。这种方法符合 C++ 的设计原则,并且在性能上是适用的,因为它避免了对象赋值相关的几个问题。