C++ 面向对象_6

拷贝赋值与移动赋值函数

  1. 拷贝赋值函数(Copy Assignment Operator):类的特殊成员函数,用于将一个已有对象的内容复制到另一个已有对象中。它通常以 operator= 的形式重载,返回类型为当前类的引用(ClassName&),参数为常量引用(const ClassName&)。

    ClassName& operator=(const ClassName& other);
    

    拷贝赋值涉及深拷贝和浅拷贝(取决于实现方式),会复制源对象的状态到目标对象中,通常需要考虑资源管理(如动态内存分配)。

  2. 移动赋值函数(Move Assignment Operator):移动赋值函数是C++11 引入的特殊成员函数,用于将一个已有对象的资源“移动”到另一个已有的对象中,而不是复制。它通常以 operator= 的形式重载,返回类型为当前类的引用(ClassName&),参数为右值引用(ClassName&&)。

    ClassName& operator=(ClassName&& other) noexcept;
    

    移动赋值不是简单复制数据,而是将资源的所有权从源对象转移到目标对象,源对象通常被置于“有效但未定义的空状态”(如指针置为nullptr)。它旨在提高性能,避免不必要的深拷贝。

为什么需要拷贝赋值和移动赋值函数?

  1. 拷贝赋值函数
    如果类没有定义拷贝赋值函数,编译器会自动为该类提供默认的拷贝赋值函数,进行逐成员拷贝(浅拷贝)。对于包含指针成员或动态分配资源的类,浅拷贝会导致两个对象指向同一块内存,后续释放可能引发双重释放或未定义行为。

    需要深拷贝或特殊逻辑来正确管理资源时,必须显示定义拷贝赋值函数。

  2. 移动赋值函数
    拷贝赋值涉及深拷贝,开销较大,尤其对于大对象(如容器)。移动赋值通过“转移”资源(如指针)避免深拷贝,提高效率。
    临时对象处理:对于右值(如函数返回的临时对象),移动赋值可以高效接管其资源,避免无意义的复制。
    C++11 引入右值引用后,移动语义成为标准库(如 std::vector)高效运行的基础,自定义类也需要支持移动赋值以融入生态。

如何使用拷贝赋值和移动赋值函数

拷贝赋值 obj1 = obj2,其中 obj2 是一个已有的对象。

一个良好的拷贝赋值函数应该:

  1. 检查自赋值(self-assignment)
  2. 释放当前对象的资源
  3. 拷贝另一个对象的资源
  4. 返回当前对象的引用

在实现上:

ClassName& ClassName::operator=(const ClassName& other) {
    // 检查自赋值
    if (this == &other) 
        return *this;
    
    // 释放当前对象的资源
    delete[] data;
    
    // 拷贝另一个对象的资源
    size = other.size;
    data = new int[size];
    for (size_t i = 0; i < size; i++) {
        data[i] = other.data[i];
    }
    
    // 返回当前对象的引用
    return *this;
}

移动赋值 obj1 = std::move(obj2) 或者赋值给右值(如函数返回值)。

一个良好的移动赋值函数应该:

  1. 检查自赋值(self-assignment)
  2. 释放当前对象的资源
  3. 从源对象浅拷贝资源
  4. 将源对象置于有效但为空的状态
  5. 标记为 noexcept 以提高性能和安全性
  6. 返回 *this 引用

在实现上:

ClassName& ClassName::operator=(ClassName&& other) noexcept {
    // 检查自赋值
    if (this == &other) 
        return *this;
    
    // 释放当前对象资源
    delete[] data;
    
    // 窃取资源(移动语义)
    data = other.data;
    size = other.size;
    
    // 将源对象置于有效但为空的状态
    other.data = nullptr;
    other.size = 0;
    
    // 返回当前对象的引用
    return *this;
}

从内存层面理解拷贝赋值和移动赋值函数

拷贝赋值执行以下操作:

  1. 保留目标对象的内存位置:赋值操作不改变目标对象在内存中的位置
  2. 释放目标对象的资源:调用适当的析构函数或释放函数(如 delete)清理目标对象当前拥有的资源。
  3. 分配新内存:为需要复制的资源分配新的内存空间。
  4. 复制数据:将源对象的数据逐字节地复制到新分配的内存中。
  5. 更新指针:更新目标对象内的指针,使其指向新分配的资源。

移动赋值执行以下操作:

  1. 保留目标对象的内存位置:同样,赋值操作不改变目标对象在内存中的位置。
  2. 释放目标对象的资源:释放目标对象当前拥有的资源。
  3. 窃取指针:直接复制源对象的指针值,而不是复制指针指向的数据。
  4. 避免额外内存分配:不需要为资源分配新的内存。
  5. 重置源对象:将源对象的指针设置为 nullptr 或其他安全值,防止析构时释放被窃取的资源。

为何不能使用初始化列表?

赋值函数与构造函数有本质区别:

  • 构造函数用于初始化新的对象,可以使用初始化列表
  • 赋值函数用于重载赋值操作符,用于修改已存在对象的状态,此时对象的成员已经被构造

因此,赋值函数中不能使用初始化列表,只能在函数体内执行赋值操作。初始化列表只能在构造阶段使用,而赋值函数操作的对象已经完成了初始化的阶段。

赋值函数为何返回引用?

赋值函数通常返回当前对象的引用(ClassName&),这样做的好处包括:

  1. 支持链式赋值表达式,比如 a = b = c
  2. 避免不必要的对象复制

链式赋值从右到左求值,例如 a = b = c 首先执行 b = c,然后执行 a = (b = c)。此处 b = c 需要返回引用才能继续用于后续的赋值。

处理自赋值的情况

自赋值是指对象赋值给自身的情况(例如 a = a),如果不正确处理,可能导致严重问题,比如,如果先释放自身资源,在复制,而源数据已经被销毁,可能导致内存泄漏或访问无效内存。

// 自赋值检查
    if (this == &other) {
        return *this;  // 直接返回,不做任何操作
    }

编译器合成的赋值函数

在某些情况下,编译器会自动合成赋值函数:

默认拷贝赋值函数:如果类没有自定义拷贝赋值函数,编译器会生成一个执行成员逐个浅拷贝的默认版本

默认移动赋值函数:如果类满足以下所有条件,编译器会生成移动赋值函数

  • 没有用户定义的拷贝构造函数
  • 没有用户定义的移动构造函数
  • 没有用户定义的拷贝赋值函数
  • 没有用户定义的移动赋值函数
  • 没有用户定义的析构函数

当类中有指针成员或管理其他资源时,编译器合成的赋值运算符通常是不够的,因为它们只执行浅拷贝。