C++赋值运算符重载

前言

C++规定,赋值运算符重载只能定义为成员函数。

本文代码在 Linux 系统下用 gcc 测试,并且需要关闭编译器的“返回值优化”,也就是需要加上编译参数 -fno-elide-constructors

编译命令:

g++ -std=c++11 -fno-elide-constructors test.cc

C++赋值运算符重载时,总是返回一个引用,为什么?

我们分以下几种情况进行讨论:返回“void”、返回“值类型”、返回“引用类型”、返回“常量引用类型”。

返回“void”

// test.cc
#include <bits/stdc++.h> // Linux 系统
using namespace std;

#define __FILENAME__ \
  (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)

struct A {
  int x_;

  A() { cout << this << "()" << endl; }

  A(int x) { cout << this << "(" << x << ")" << endl; }

  A(const A& rhs) {
    x_ = rhs.x_;
    cout << this << "(" << &rhs << ")" << endl;
  }
  ~A() { cout << "~" << this << endl; }

  // 返回 void
  void operator=(const A& rhs) {
    x_ = rhs.x_;   // 改变成员变量
  }
};

int main() {
  A a(1);
  A b(2);

  b = a; // 等价于 b.operator=(a);
  // c = b = a; // 连续赋值,会报错!
}

运算符函数与普通成员函数没有区别,b = ab.operator=(a) 是等价的。也就是传入 aconst A& 类型的参数,然后用 a 的成员更新 b 的成员。

可以看到,返回值其实没有作用,因为我们需要的“赋值”操作已经完成了。

返回“值类型”

如果返回 void ,那么如果一条语句存在连续赋值,就会出现问题:

c = b = a;

这条语句等价于:

c = (b = a);

即:

c.operator=(b.operator=(a));

先执行 b.operator=(a) ,返回值是 void,然后将 void 作为参数传递给 c 的成员函数 void operator=(const A&) ,显然参数类型不匹配,找不到这样的函数,所以会报错。

那么,我们尝试返回一个 A 类型的对象,那么返回值就可以作为赋值运算符函数的参数继续调用赋值运算符函数了。

我们给出完整测试程序:

#include <bits/stdc++.h> // Linux 系统
using namespace std;

#define __FILENAME__                                                           \
  (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)

struct A
{
  int x_;

  A()
  {
    cout << this << "()" << endl;
  }

  A(int x)
  {
    cout << this << "(" << x << ")" << endl;
  }

  A(const A &rhs)
  {
    x_ = rhs.x_;
    cout << this << "(" << &rhs << ")" << endl;
  }
  ~A()
  {
    cout << "~" << this << endl;
  }

  // 返回 A 的值类型
  A operator=(const A &rhs)
  {
    x_ = rhs.x_; // 改变成员变量
    // return A(*this);
    return *this; // 用自身复制构造一个匿名对象返回
  }
};

int main()
{
  A a(1);
  A b(2);
  A c(3);
  getchar();

  b = a; // 等价于 b.operator=(a);
  getchar();

  c = b = a; // 连续赋值
  getchar(); // 暂停,方便观察
}

运行代码,打印如下:

0x7ffcd9143be4(1)
0x7ffcd9143be8(2)
0x7ffcd9143bec(3)

0x7ffcd9143bf0(0x7ffcd9143be8)
~0x7ffcd9143bf0

0x7ffcd9143bf0(0x7ffcd9143be8)
0x7ffcd9143bf4(0x7ffcd9143bec)
~0x7ffcd9143bf4
~0x7ffcd9143bf0

~0x7ffcd9143bec
~0x7ffcd9143be8
~0x7ffcd9143be4

打印结果分为 4 段。其中,第 2 段为 b = a 的执行结果;第 3 段为 c = b = a 的执行结果。

  • 分析 b = a 的执行过程:

我们还是转化成成员函数调用的方式以便于理解:

b.operator=(a);

(1)以参数 a 调用 b 的赋值运算符成员函数,执行 x_ = rhs.x_ 更新 b 的成员;
(2)执行 return *this ,等价于 return A(*this),即用 b 对象自身复制构造一个匿名对象作为返回值。
(3)离开 b 的赋值运算符成员函数的作用域。
(4)语句 b.operator=(a) 执行结束,此时之前赋值运算符函数返回的匿名对象析构。
综上,一共 1 次复制构造、1 次析构。

  • 分析 c = b = a 的执行过程

等价语句:

c.operator=(b.operator=(a));

(1)执行 b.operator=(a),函数返回时复制构造一个对象,该对象是一个临时的匿名对象 Anonymous1。
(2)执行 c.operator=(Anonymous1) ,函数返回时,再次复制构造一个临时对象 Anonymous2。
(3)语句 c.operator=(b.operator=(a)) 执行完毕,之前的临时匿名对象依次析构:先析构Anonymous2,再析构 Anonymous1。因为局部变量是放在栈上的,所以析构顺序与变量定义顺序相反。
综上,一共 2 次复制构造、2 次析构。

  • 问题:
    当我们执行 b = a,由于 ab 都是已经定义好的对象,不应该有构造和析构过程。但当我们返回值类型的对象时,却凭空多了 1 次构造和 1 次析构的开销!

返回“引用类型”

为了避免返回匿名对象造成的构造和析构开销,我们将返回类型改成引用类型。

测试程序:

#include <bits/stdc++.h> // Linux 系统
using namespace std;

#define __FILENAME__                                                           \
  (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)

struct A
{
  int x_;

  A()
  {
    cout << this << "()" << endl;
  }

  A(int x)
  {
    cout << this << "(" << x << ")" << endl;
  }

  A(const A &rhs)
  {
    x_ = rhs.x_;
    cout << this << "(" << &rhs << ")" << endl;
  }
  ~A()
  {
    cout << "~" << this << endl;
  }

  // 返回引用类型
  A& operator=(const A &rhs)
  {
    x_ = rhs.x_; // 改变成员变量
    // return A(*this);
    return *this;
  }
};

int main()
{
  A a(1);
  A b(2);
  A c(3);
  getchar();

  b = a; // 等价于 b.operator=(a);
  getchar();

  c = b = a; // 连续赋值
  getchar(); // 暂停,方便观察
}

测试结果:

0x7fffb86d362c(1)
0x7fffb86d3630(2)
0x7fffb86d3634(3)



~0x7fffb86d3634
~0x7fffb86d3630
~0x7fffb86d362c

可见,调用赋值运算符函数的构造和析构过程都消失了。这是符合预期的。

返回“常量引用类型”

那么,我们再提出一个问题:如果返回常量引用类型,可以吗?
答:可以。可以看到,我们之所以返回对象或对象的引用,是为了连续赋值时,将其作为新的参数以再次调用赋值运算符函数,而赋值运算符函数的参数就是一个常量引用,即使传入一个引用(也就是返回引用类型),也和传入常量引用(也就是返回常量引用)没有什么区别。

测试程序:

#include <bits/stdc++.h> // Linux 系统
using namespace std;

#define __FILENAME__                                                           \
  (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)

struct A
{
  int x_;

  A()
  {
    cout << this << "()" << endl;
  }

  A(int x)
  {
    cout << this << "(" << x << ")" << endl;
  }

  A(const A &rhs)
  {
    x_ = rhs.x_;
    cout << this << "(" << &rhs << ")" << endl;
  }
  ~A()
  {
    cout << "~" << this << endl;
  }

  // 返回常量引用
  const A& operator=(const A &rhs)
  {
    x_ = rhs.x_; // 改变成员变量
    // return A(*this);
    return *this;
  }
};

int main()
{
  A a(1);
  A b(2);
  A c(3);
  getchar();

  b = a; // 等价于 b.operator=(a);
  getchar();

  c = b = a; // 连续赋值
  getchar(); // 暂停,方便观察
}

打印结果:

0x7ffdda7c80ac(1)
0x7ffdda7c80b0(2)
0x7ffdda7c80b4(3)



~0x7ffdda7c80b4
~0x7ffdda7c80b0
~0x7ffdda7c80ac

这和返回引用类型没有什么不同。

但是,以下调用方式就会报错:

// 由运算符优先级,括号不加也可以。
A& ra = (b = a); // 报错:(b = a) 返回一个 const 引用,显然不能将其绑定到引用类型。
const A& ra1 = (b = a); // 正确

但是,一般理解上,b = a 应该返回的是还是 b ,我们将 b 绑定到一个引用上,应该是可行的。所以,为了适应一般性理解,我们重载赋值运算符时一般返回的是引用,而不是常量引用。其实,这也同时解释了为什么我们总是直接返回引用,而不是一个新的对象的原因,因为我们会认为 b = a 返回的应该是 b 本身。

<think>我们正在讨论C++中的赋值运算符重载,特别是与深拷贝和浅拷贝相关的内容。用户可能希望了解如何正确重载赋值运算符,避免浅拷贝带来的问题(如双重释放、内存泄漏等),并理解深拷贝与浅拷贝的区别。同时,用户引用的资料中提到了拷贝构造函数和禁止拷贝的方法,这些内容与赋值运算符重载紧密相关。回答结构:1.赋值运算符重载的基本概念和作用2.深拷贝与浅拷贝的区别及问题3.赋值运算符重载的典型写法(包括处理自赋值和释放原有资源)4.拷贝构造函数和赋值运算符的协同(拷贝交换技法)5.何时需要自定义赋值运算符重载6.如何禁止拷贝和赋值(使用delete)7.代码示例8.注意事项9.相关问题注意:根据用户提供的引用引用[2]展示了深拷贝的拷贝构造函数,引用[3]展示了如何禁止拷贝,引用[4]说明了赋值运算符重载必须是成员函数以及默认行为(浅拷贝)。下面开始回答:</think> ### C++赋值运算符重载(Operator Overloading)使用指南 赋值运算符重载(`operator=`)是控制类对象拷贝行为的核心机制。正确实现可避免**浅拷贝**导致的资源泄漏或数据损坏,尤其涉及动态内存管理时[^1][^4]。 --- #### 一、核心概念 | **类型** | **特点** | **风险** | |----------------|--------------------------------------------------------------------------|------------------------------| | **浅拷贝** | 逐字节复制指针地址(编译器默认生成) | 多个对象共享资源 → 双重释放崩溃 | | **深拷贝** | 创建资源的新副本(需手动实现) | 无共享资源 → 安全但消耗更大 | | **禁止拷贝** | 用 `= delete` 显式删除拷贝操作 | 完全阻止拷贝行为 | --- #### 二、赋值运算符重载规范写法 ##### 1. 基本结构 ```cpp class MyClass { public: // 深拷贝赋值运算符 MyClass& operator=(const MyClass& other) { if (this != &other) { // 关键:检查自赋值! // 1. 释放原有资源 delete[] data; // 2. 深拷贝新资源 data = new int[other.size]; size = other.size; std::copy(other.data, other.data + size, data); } return *this; // 支持链式赋值 (a=b=c) } private: int* data; size_t size; }; ``` ##### 2. 处理自赋值与异常安全 ```cpp MyClass& operator=(const MyClass& other) { if (this != &other) { int* new_data = new int[other.size]; // 先创建新资源 std::copy(other.data, other.data + other.size, new_data); delete[] data; // 再释放旧资源 (异常安全) data = new_data; // 无异常再赋值 size = other.size; } return *this; } ``` > **为什么检查自赋值?** > 防止 `a=a` 时误释放自身资源 → 访问已释放内存崩溃[^4]。 --- #### 三、深度拷贝完整示例 ```cpp #include <iostream> #include <algorithm> class Vector { public: // 构造函数 Vector(size_t n) : size(n), data(new int[n]{}) {} // 深拷贝赋值 Vector& operator=(const Vector& other) { if (this != &other) { int* new_data = new int[other.size]; std::copy(other.data, other.data + other.size, new_data); delete[] data; data = new_data; size = other.size; } return *this; } // 析构函数 ~Vector() { delete[] data; } void print() { for (size_t i = 0; i < size; ++i) std::cout << data[i] << " "; std::cout << "\n"; } private: int* data; size_t size; }; int main() { Vector v1(3); // 初始化为 [0,0,0] Vector v2(2); // 初始化为 [0,0] v1 = v2; // 深拷贝赋值 v2 = v2; // 自赋值安全 } ``` --- #### 四、禁止拷贝的实现 当类不应被拷贝时(如管理唯一资源),显式删除拷贝操作: ```cpp class UniqueResource { public: UniqueResource() = default; // 删除拷贝构造和赋值 UniqueResource(const UniqueResource&) = delete; // [^3] UniqueResource& operator=(const UniqueResource&) = delete; // [^3] // 允许移动语义 UniqueResource(UniqueResource&&) noexcept = default; }; ``` --- #### 五、关键注意事项 1. **返回类型**:必须返回 `Class&`(支持链式赋值 `a=b=c`)[^4] 2. **自赋值检查**:避免 `if (this == &other) return *this;` 逻辑错误 3. **异常安全**:先分配新资源再释放旧资源 4. **协同拷贝构造**:深拷贝类需同时定义 `拷贝构造函数` 和 `operator=`[^2] 5. **移动语义**:C++11+ 中优先实现移动赋值 (`operator=&&`) 提升性能 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值