C++左值右值
左值指有地址可取的对象,右值指临时值或字面量,右值引用支持资源移动优化。
C++ 左值右值
概念
概念 | 简明解释 |
---|---|
左值(lvalue) | 有名字、有地址的对象,能出现在赋值号左边 |
右值(rvalue) | 没有名字、临时存在的值,只能出现在赋值号右边 |
举例:
1
2
int a = 10;
int b = a + 5;
表达式 | 类型 | 说明 |
---|---|---|
a | 左值 | 有名字,可以 &a 取地址 |
10 | 右值 | 临时值,不能取地址 |
a + 5 | 右值 | 表达式结果是临时值 |
b | 左值 | 是变量 |
如何判断
能否被赋值? | 能否取地址? | 类型 |
---|---|---|
✅ | ✅ | 左值 |
❌ | ❌(大部分情况) | 右值 |
1
2
3
int x = 42;
&x; // ✅ 左值能取地址
&(x + 1); // ❌ 编译错误,右值不能取地址
常见的左值右值
示例 | 类型 | 说明 |
---|---|---|
变量 x | 左值 | 有地址、可以赋值 |
字面量 10 | 右值 | 没地址、不能取地址 |
x + 1 | 右值 | 运算结果是一个临时值 |
"abc" | 右值 | 字符串字面量是临时值 |
函数返回值(返回非引用) | 右值 | |
函数返回引用 | 左值 |
与引用的关系
引用类型 | 能绑定右值 | 能绑定左值 | 用途 |
---|---|---|---|
T& | ❌ | ✅ | 修改已有变量 |
const T& | ✅ | ✅ | 安全地读取,不可修改 |
T&& | ✅ | ❌ | 专门操作右值,进行移动 |
1
2
3
4
5
6
int a = 10;
int& r1 = a; // ✅ 左值引用绑定左值
int& r2 = 10; // ❌ 错误,不能绑定右值
const int& r3 = 10; // ✅ 常量引用可以绑定右值
int&& r4 = 10; // ✅ 右值引用绑定右值
右值引用
语法是 T&&
,只允许绑定右值(不能绑定有名字的变量)。右值引用让你可以“接住”临时对象的资源,然后偷走它们,而不是复制。
移动语义
1
2
3
4
5
6
7
std::string a = "hello";
std::string b = std::move(a); // move 后,a 的资源转移给 b
std::string s1 = "abc";
std::string s2 = s1; // 拷贝构造(复制内容)
std::string s3 = std::move(s1); // 移动构造(直接窃取 s1 的资源)
移动构造函数/赋值运算符
1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 把 other 的资源“偷”过来
}
MyClass& operator=(MyClass&& other) noexcept {
// 先释放自己资源,再接管 other's
return *this;
}
};
强烈推荐给移动构造函数加上 noexcept
:
标准库容器(如 std::vector
、std::string
) 在内部做扩容时:
- 优先使用移动构造函数(比拷贝效率高)
- 但前提是:这个移动构造是
noexcept
的
否则就退回使用拷贝构造,导致性能变差。
引用限定符
引用限定符(Ref-qualifiers)是C++11引入的一种语法,用于区分成员函数对对象引用类型的限定,常见于成员函数声明后面,表示该成员函数只能被左值对象或右值对象调用。
主要用来控制成员函数的调用权限,尤其对重载成员函数行为和资源管理(比如移动语义)非常重要。
基本概念
- 左值引用限定符
&
:表示该成员函数只能被左值对象调用。 - 右值引用限定符
&&
:表示该成员函数只能被右值对象调用。 - 无引用限定符:该成员函数可以被左值和右值调用。
示例
1
2
3
4
5
6
7
8
9
10
11
struct Foo {
void foo() & { std::cout << "called on lvalue\n"; }
void foo() && { std::cout << "called on rvalue\n"; }
};
int main() {
Foo f;
f.foo(); // 调用左值版本
Foo().foo(); // 调用右值版本
}
输出:
1
2
called on lvalue
called on rvalue
具体过程:
Foo f;
f
是一个具名变量,是左值。
f.foo();
- 调用时,编译器会查找
foo()
适用于左值对象的版本。 - 匹配的是
void foo() &
,所以输出"called on lvalue"
。
- 调用时,编译器会查找
Foo().foo();
Foo()
创建了一个临时对象,是右值。- 调用时,编译器查找适用于右值对象的
foo()
。 - 匹配的是
void foo() &&
,所以输出"called on rvalue"
。
用途详解
区分对左值和右值的操作
允许对左值对象和右值对象提供不同的行为,尤其对支持移动语义的类非常重要。
当操作临时对象(右值)时,可以安全地“偷走”资源(移动构造或移动赋值)。
当操作具名对象(左值)时,通常不希望破坏原对象。
防止错误的调用
比如你写一个函数只想在临时对象上调用,或者只允许在持久对象上调用,引用限定符可以帮你做到。
配合移动语义
比如
std::string
中的operator+
,返回的临时字符串可以调用带&&
限定符的成员函数,避免不必要的复制。
结合const
使用
引用限定符可以和const
一起用:
1
2
void foo() const &; // const左值对象调用
void foo() const &&; // const右值对象调用
函数签名 | 调用对象类型限制 | 备注 |
---|---|---|
void f() | 左值和右值均可 | 默认 |
void f() & | 只能左值调用 | 用于修改左值的成员函数 |
void f() && | 只能右值调用 | 用于移动语义或优化 |
void f() const & | 只能常量左值调用 | 不修改对象的左值 |
void f() const && | 只能常量右值调用 | 不修改对象的右值 |
注意点
只用于非静态成员函数声明末尾,表示该成员函数只能被特定类型的对象调用。
如果一个成员函数写了引用限定符(比如
&
或&&
),那么所有该函数同名、参数列表完全相同的重载版本,也都建议写上引用限定符。1 2 3 4 5 6
struct A { void foo() & { std::cout << "lvalue\n"; } // void foo() && { std::cout << "rvalue\n"; } // 如果这里没写 foo() &&,对右值调用 foo() 会找不到匹配版本 // 要么都写上引用限定符,覆盖所有情况;要么都不写,保持传统行为 };
引用限定符是函数签名的一部分,
foo() &
和foo()
是两种不同的成员函数签名。如果有一个没有限定符的版本(相当于“无限定符”,它对左值和右值都能调用),跟一个限定符版本共存,就会导致调用时不确定调用哪个(或编译错误)。