文章

C++左值右值

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

C++左值右值

C++ 左值右值

  • 左值(lvalue):有名字、有地址的对象,能出现在赋值号左边
  • 右值(rvalue):没有名字、临时存在的值,只能出现在赋值号右边

如何判断

能否被赋值?能否取地址?类型
左值
不能不能(大部分情况)右值
1
2
3
int x = 42;
&x;         // 左值能取地址
&(x + 1);   // 编译错误,右值不能取地址

常见示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int a = 10;      // a 是左值(有名字,可以 &a 取地址)
                 // 10 是右值(字面量常量,不能取地址)

int b = a + 5;   // b 是左值(变量,有名字,可以取地址)
                 // a + 5 是右值(表达式结果的临时值)

// ------------------- 一些容易混淆的情况 -------------------

int c = (a);     // (a) 仍然是左值(加括号不影响)

int d = a++;     // a++ 返回右值(自增前的临时值)
                 // a 本身依旧是左值

int e = ++a;     // ++a 返回左值(自增后的 a,本身可以继续取地址)

int f = a * 2;   // a * 2 是右值(临时结果)
                 // f 是左值(变量)

int& foo();      // 假设 foo 返回 int& (引用)
int  bar();      // 假设 bar 返回 int (值)

int& r = foo();  // foo() 返回左值(引用,可取地址)

int x = bar();   // bar() 返回右值(纯值,临时对象)
示例类型说明
变量 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. 区分对左值和右值的操作

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

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

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

  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 进行授权