C++ 左值与右值

Posted by KalosAner on March 27, 2025

一、引言

  • 左值(lvalue):表示具名对象或内存中已有的资源,通常可以在表达式中多次使用。例如,一个变量 a 或数组元素等都属于左值。
  • 右值(rvalue):通常指临时对象或表达式求值结果,它们没有持久化存储,往往在使用后即被销毁。例如,函数返回的临时对象或字面常量。

  • 拷贝:在拷贝构造和拷贝赋值中,程序会分配新的内存并将源对象中的数据完整地复制到目标对象中。这样做确保了两个对象相互独立,但对于拥有动态资源(如堆内存、文件句柄、网络连接等)的对象,可能会造成性能开销或资源重复管理(需要深拷贝以防双重释放)。
  • 移动:移动语义的引入(从 C++11 开始)目的在于减少不必要的数据拷贝,尤其是在处理临时对象时。移动操作不会创建数据副本,而是直接“窃取”源对象的内部资源,然后将源对象置于一个有效但未指定的状态。这样可以显著提高性能,尤其是在容器、智能指针等类中效果明显。

二、构造和赋值

1. 左值引用的拷贝构造和拷贝赋值

拷贝构造函数

  • 原型:通常写为

    Foo(const Foo& other);
    

    使用常量引用主要有两方面原因:

    1. 避免在拷贝过程中修改源对象。
    2. 可以接受常量对象作为参数。
  • 实现要点

    • 为目标对象分配新的内存或资源。
    • 将源对象内的各个数据成员逐个拷贝(深拷贝或按值拷贝)。
    • 如果类中有指针等资源,通常需要进行深拷贝以确保两个对象不共享同一资源,从而避免后续的资源释放问题。

拷贝赋值运算符

  • 原型:通常写为

    1
    
    Foo& operator=(const Foo& other);
    
  • 实现要点

    • 自我赋值检查(例如:if (this == &other) return *this;)。
    • 释放目标对象当前占有的资源(如果有)。
    • 分配新的资源或复制数据,使得目标对象与源对象内容相同。
    • 返回当前对象的引用以支持链式赋值。

示例代码

class Foo {
private:
    int* data;
public:
    // 拷贝构造函数
    Foo(const Foo& other) {
        data = new int(*other.data);  // 对动态分配内存的深拷贝
    }

    // 拷贝赋值运算符
    Foo& operator=(const Foo& other) {
        if (this != &other) {       // 自我赋值检查
            delete data;            // 释放已有资源
            data = new int(*other.data);
        }
        return *this;
    }

    ~Foo() {
        delete data;
    }
};
2. 右值引用的移动构造和移动赋值

移动构造函数

  • 原型:通常写为

    1
    
    Foo(Foo&& other) noexcept;
    

    移动构造函数接受一个右值引用参数(Foo&&),表明这个参数可以“被窃取”。

  • 实现要点

    • 直接获取源对象的资源指针,而不分配新的内存。
    • 将源对象的资源指针置空或置于一个安全状态,防止析构时重复释放资源。
    • 标记为 noexcept 能使容器在发生异常时更安全高效地转移资源。

移动赋值运算符

  • 原型:通常写为

    1
    
    Foo& operator=(Foo&& other) noexcept;
    
  • 实现要点

    • 同样需要进行自我赋值检查(尽管自我移动较为罕见,但有时依然需要)。
    • 释放目标对象当前的资源。
    • 将源对象的内部资源转移给目标对象。
    • 将源对象置于有效状态(一般将内部指针置为 nullptr)。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo {
private:
    int* data;
public:
    // 移动构造函数
    Foo(Foo&& other) noexcept : data(other.data) {
        other.data = nullptr;   // 使 other 处于一个可析构状态
    }

    // 移动赋值运算符
    Foo& operator=(Foo&& other) noexcept {
        if (this != &other) {        // 自我赋值检查
            delete data;             // 释放当前资源
            data = other.data;       // 接管资源
            other.data = nullptr;    // 重置 other
        }
        return *this;
    }

    ~Foo() {
        delete data;
    }
};