拷贝赋值运算符

拷贝赋值运算符 是一个特殊的运算符重载函数,用于将一个已存在对象的值赋予另一个已存在的对象。通俗地说,它规定了当使用 obj2 = obj1; 时,应该如何用 obj1 的内容来覆盖 obj2 的内容。


语法形式

拷贝赋值运算符的典型声明形式如下:

cpp

class MyClass {
public:
    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& other); // 最常见的形式
};

它有以下几个关键特点:

  1. 函数名是 operator=

  2. 参数通常是对本类类型的常量引用,即 const ClassName&

  3. 返回值通常是对当前对象(*this)的引用。这主要是为了支持链式赋值,例如 a = b = c;

  4. 它是一个成员函数


什么时候会被调用?

拷贝赋值运算符在以下场景中被调用:

cpp

MyClass obj1, obj2, obj3;
obj1 = obj2;              // 调用拷贝赋值运算符 (场景1:直接赋值)
obj1 = obj2 = obj3;       // 调用拷贝赋值运算符 (场景2:链式赋值)

关键区别:拷贝构造函数 vs. 拷贝赋值运算符

这是最容易混淆的地方,请务必理解:

拷贝构造函数拷贝赋值运算符
目的创建一个新对象,并用另一个对象初始化它。已存在的对象的值赋给另一个已存在的对象。
调用时机MyClass obj2(obj1);
MyClass obj2 = obj1; (初始化!)
obj2 = obj1; (赋值!)
目标对象状态目标对象正在被创建,之前不存在。目标对象已经存在,可能持有需要管理的资源。

默认拷贝赋值运算符

如果你没有在自己的类中定义拷贝赋值运算符,编译器会自动为你生成一个

和默认拷贝构造函数一样,这个默认的拷贝赋值运算符执行的是 “浅拷贝” 或 “按成员赋值”

  • 对于基本数据类型,直接复制其值。

  • 对于类成员对象,会调用该成员对象自身的拷贝赋值运算符。


为什么需要自定义拷贝赋值运算符?

当类中含有动态分配的资源(如指针指向堆内存) 时,默认的浅拷贝赋值会带来两个严重问题:

  1. 内存泄漏:目标对象原有的资源没有被释放。

  2. 悬空指针/双重释放:两个对象的指针指向了同一块内存。

问题示例:浅拷贝赋值的陷阱

cpp

#include <iostream>
#include <cstring>

class BadString {
private:
    char* m_data;
    int m_size;
public:
    // 普通构造函数
    BadString(const char* str = "") {
        m_size = strlen(str);
        m_data = new char[m_size + 1];
        strcpy(m_data, str);
    }

    // 析构函数
    ~BadString() {
        delete[] m_data;
    }

    // 注意:这里没有定义拷贝赋值运算符!
    // 编译器会生成一个默认的(浅拷贝)版本。
};

int main() {
    BadString str1("Hello");
    BadString str2("World");

    str2 = str1; // 灾难的开始!浅拷贝赋值

    // 此时,str2.m_data 原来的内存("World")没有被释放 -> 内存泄漏
    // 同时,str1.m_data 和 str2.m_data 指向了同一块内存("Hello")

    return 0;
} // 离开作用域时:
  // 1. str2 被析构,释放了 "Hello" 的内存。
  // 2. str1 被析构,尝试再次释放同一块 "Hello" 的内存 -> 程序崩溃(双重释放)
解决方案:自定义拷贝赋值运算符

一个正确、安全的拷贝赋值运算符通常需要完成以下步骤:

  1. 自我赋值检查:防止 a = a; 这种操作导致资源被意外释放。

  2. 释放自身原有资源:避免内存泄漏。

  3. 分配新资源并复制数据:进行深拷贝。

  4. 返回 *this:以支持链式赋值。

cpp

class GoodString {
private:
    char* m_data;
    int m_size;
public:
    // ... 构造函数、析构函数与之前相同 ...

    // 【核心】自定义拷贝赋值运算符
    GoodString& operator=(const GoodString& other) {
        // 1. 自我赋值检查 (非常重要!)
        if (this == &other) {
            return *this; // 如果是自己给自己赋值,直接返回
        }

        // 2. 释放自己原有的内存,避免泄漏
        delete[] m_data;

        // 3. 分配新内存,并复制数据 (深拷贝)
        m_size = other.m_size;
        m_data = new char[m_size + 1];
        strcpy(m_data, other.m_data);

        // 4. 返回当前对象的引用,以支持链式赋值
        return *this;
    }
};

int main() {
    GoodString str1("Hello");
    GoodString str2("World");

    str2 = str1; // 安全!调用自定义的拷贝赋值运算符
    str1 = str1; // 安全!自我赋值检查会处理这种情况

    // 现在 str1 和 str2 拥有各自独立的内存块
    return 0;
}

拷贝赋值运算符的“拷贝-交换”技法

一种更优雅、更安全且能自动提供异常安全性的实现方式是 “拷贝-交换”技法。它通常需要一个能正常工作的拷贝构造函数和一个交换成员函数。

cpp

#include <utility> // for std::swap

class StringWithSwap {
private:
    char* m_data;
    int m_size;
public:
    // ... 其他成员 ...

    // 友元交换函数
    friend void swap(StringWithSwap& first, StringWithSwap& second) noexcept {
        using std::swap;
        swap(first.m_data, second.m_data);
        swap(first.m_size, second.m_size);
    }

    // 拷贝赋值运算符 (使用拷贝-交换技法)
    StringWithSwap& operator=(StringWithSwap other) { // 注意!这里是传值,不是传引用
        // 参数 `other` 是通过拷贝构造函数创建的副本
        swap(*this, other); // 将 *this 的内容与副本交换
        return *this;
        // 当函数返回时,参数 `other` 被析构,会释放掉 *this 原来的资源
    }
};

“拷贝-交换”技法的优点:

  • 自动处理自我赋值:因为参数是传值,a = a; 会先创建一个临时副本,再交换,是安全的。

  • 强异常安全性:如果拷贝构造失败(内存不足抛出异常),不会影响 *this 的原始状态。

  • 代码复用:无需手动写释放和分配的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值