- 左值 (lvalue, locator value): 可以放在赋值运算符
=
左边的表达式。它代表一个有身份 (identity) 的、持久的对象或内存位置。你可以对它取地址(使用&
)。 - 右值 (rvalue, read value): 只能放在赋值运算符
=
右边的表达式。它代表一个没有身份的、临时的值。你通常不能对它取地址。
更现代、更准确的理解是基于“身份”:
- 左值:有一个持久的身份(identity),在表达式结束后依然存在。就像一个有门牌号的房子。
- 右值:是一个即将被销毁的临时值,没有持久的身份。就像你刚算出来的
2+3
的结果5
,这个5
只是一个临时值。
左值 (lvalue)
左值是指定了内存中某个位置的表达式。
特征:
- 有持久的内存地址。
- 可以通过地址访问。
- 可以被修改(除非被
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)
右值是不表示任何特定内存位置的临时值。它们是表达式计算过程中产生的中间结果。
特征:
- 通常是临时的,表达式结束时就会被销毁。
- 没有可识别的内存地址(不能对它安全地使用
&
)。
常见的右值示例:
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)
行会发生什么:
create_a_big_string
函数内部创建一个字符串对象(我们称之为temp_str
)。- 函数返回时,会创建一个
temp_str
的拷贝,作为函数调用的返回值(一个临时右值)。 my_str
通过拷贝构造函数,再次拷贝这个临时的右值。- 临时的右值被销毁。
这里有两次昂贵的拷贝(涉及到堆内存的重新分配和大量字符的复制)。这完全是浪费,因为那个临时对象马上就要被销毁了,我们为什么不直接“偷”走它的资源呢?
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 左值引用可以绑定到所有类型的值 |
核心用途 | 标识对象 | 实现移动语义,避免不必要的拷贝 |