C++ 中的左值和右值

  • 左值 (lvalue, locator value): 可以放在赋值运算符=左边的表达式。它代表一个有身份 (identity) 的、持久的对象或内存位置。你可以对它取地址(使用 &)。
  • 右值 (rvalue, read value): 只能放在赋值运算符=右边的表达式。它代表一个没有身份的、临时的值。你通常不能对它取地址。

更现代、更准确的理解是基于“身份”:

  • 左值:有一个持久的身份(identity),在表达式结束后依然存在。就像一个有门牌号的房子。
  • 右值:是一个即将被销毁的临时值,没有持久的身份。就像你刚算出来的 2+3 的结果 5,这个 5 只是一个临时值。

左值 (lvalue)

左值是指定了内存中某个位置的表达式。

特征:

  1. 有持久的内存地址
  2. 可以通过地址访问
  3. 可以被修改(除非被 const 修饰)。

常见的左值示例:

int x = 10; // x 是一个左值

std::string s = "hello"; // s 是一个左值

int arr[5];
arr[0] = 1; // 数组的元素 arr[0] 是一个左值

int* p = &x;
*p = 20; // 指针解引用的结果 *p 是一个左值

class MyClass {
public:
    int value;
};
MyClass obj;
obj.value = 5; // 成员访问的结果 obj.value 是一个左值

// 函数返回一个引用,也是左值
int global_var = 100;
int& get_global() {
    return global_var;
}
get_global() = 50; // get_global() 的调用结果是一个左值

右值 (rvalue)

右值是不表示任何特定内存位置的临时值。它们是表达式计算过程中产生的中间结果。

特征:

  1. 通常是临时的,表达式结束时就会被销毁。
  2. 没有可识别的内存地址(不能对它安全地使用 &)。

常见的右值示例:

int x = 10;         // 10 是一个右值(字面量 literal)
std::string s = "hello"; // "hello" 是一个右值

int y = x + 5;      // (x + 5) 的计算结果是一个右值

int get_value() {
    return 42;
}
int z = get_value(); // 函数按值返回的结果 get_value() 是一个右值

MyClass mc;
MyClass another = MyClass(); // MyClass() 创建的临时对象是一个右值

移动语义 (Move Semantics)

在 C++11 之前,左值和右值的区别主要用于语法检查。但 C++11 引入了移动语义右值引用 (rvalue reference),彻底改变了游戏规则,极大地提升了性能。

1. 问题:不必要的拷贝

想象一个持有大量内存的类,比如一个字符串或向量:

std::string create_a_big_string() {
    // 假设这里创建了一个非常大的字符串
    return "some very very very long string...";
}

int main() {
    std::string my_str = create_a_big_string(); // (1)
}

在 C++11 之前,第 (1) 行会发生什么:

  1. create_a_big_string 函数内部创建一个字符串对象(我们称之为 temp_str)。
  2. 函数返回时,会创建一个 temp_str拷贝,作为函数调用的返回值(一个临时右值)。
  3. my_str 通过拷贝构造函数,再次拷贝这个临时的右值。
  4. 临时的右值被销毁。

这里有两次昂贵的拷贝(涉及到堆内存的重新分配和大量字符的复制)。这完全是浪费,因为那个临时对象马上就要被销毁了,我们为什么不直接“偷”走它的资源呢?

2. 解决方案:右值引用和移动语义

C++11 引入了右值引用,用 && 表示。它专门用于“绑定”到一个右值上。

// 只能绑定到右值
std::string&& rvalue_ref = create_a_big_string();

// 不能绑定到左值
std::string my_str = "abc";
// std::string&& rvalue_ref_err = my_str; // 编译错误!

有了右值引用,我们就可以为类创建移动构造函数 (Move Constructor)移动赋值运算符 (Move Assignment Operator)

class MyString {
public:
    // ... 其他构造函数 ...

    // 移动构造函数,参数是右值引用
    MyString(MyString&& other) noexcept {
        // "偷"走 other 的资源,而不是拷贝
        this->data = other.data;
        this->size = other.size;

        // 将 other 置为空状态,防止它在析构时释放我们刚偷来的资源
        other.data = nullptr;
        other.size = 0;
    }
    // ...
private:
    char* data;
    size_t size;
};

现在,当编译器看到 my_str = create_a_big_string() 这样的代码时,它发现函数的返回值是一个右值,于是它会自动选择调用移动构造函数,而不是拷贝构造函数。

这个“移动”操作仅仅是交换了几个指针和变量的值,没有进行任何深拷贝,速度极快。这就是移动语义的核心。


std::move:请求“移动”的授权

有时候,我们想从一个左值中“偷”走资源。比如,我们确定一个左值对象在后续代码中不会再被使用。这时可以用 std::move

std::move 本身什么也不移动,它只是一个类型转换,它将一个左值强制转换为一个右值引用,告诉编译器:“嘿,你可以把这个对象当成右值来处理了,可以安全地移动它的资源了。”

std::string str1 = "hello";
std::string str2 = "world";

// str1 是左值,所以这里会调用拷贝赋值运算符
// str2 会把 str1 的内容拷贝一份
str2 = str1;

std::string str3 = "goodbye";
// std::move(str1) 将左值 str1 转换为右值引用
// 这会触发移动赋值运算符
// str3 的资源被“偷”走,str1 的内容被转移到 str3
// 操作之后,str1 处于“有效但未指定的状态”,不能再使用它的值
str3 = std::move(str1);

总结

特性 左值 (lvalue) 右值 (rvalue)
含义 指向特定内存位置的持久对象 不指向特定内存位置的临时值
生命周期 较长,直到离开作用域 短暂,通常在表达式结束后销毁
取地址 & 可以 通常不可以
出现位置 可在 = 的左边或右边 只能在 = 的右边
绑定引用 可被左值引用 & 绑定 可被右值引用 && 绑定
const & const 左值引用可以绑定到所有类型的值 const 左值引用可以绑定到所有类型的值
核心用途 标识对象 实现移动语义,避免不必要的拷贝