文章

C++左值右值

左值指有地址可取的对象,右值指临时值或字面量,右值引用支持资源移动优化。

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::vectorstd::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

具体过程:

  1. Foo f;
    • f 是一个具名变量,是左值
  2. f.foo();
    • 调用时,编译器会查找foo()适用于左值对象的版本。
    • 匹配的是 void foo() &,所以输出 "called on lvalue"
  3. Foo().foo();
    • Foo() 创建了一个临时对象,是右值
    • 调用时,编译器查找适用于右值对象的foo()
    • 匹配的是 void foo() &&,所以输出 "called on rvalue"

用途详解

  1. 区分对左值和右值的操作

    允许对左值对象和右值对象提供不同的行为,尤其对支持移动语义的类非常重要。

    • 当操作临时对象(右值)时,可以安全地“偷走”资源(移动构造或移动赋值)。

    • 当操作具名对象(左值)时,通常不希望破坏原对象。

  2. 防止错误的调用

    比如你写一个函数只想在临时对象上调用,或者只允许在持久对象上调用,引用限定符可以帮你做到。

  3. 配合移动语义

    比如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() 是两种不同的成员函数签名。

    • 如果有一个没有限定符的版本(相当于“无限定符”,它对左值和右值都能调用),跟一个限定符版本共存,就会导致调用时不确定调用哪个(或编译错误)。

本文由作者按照 CC BY 4.0 进行授权