自上次撰写相关内容,我也算是有了一段实际的应用经验。现在,回头再次分析相关内容,有了更深的感悟。今儿个,我们再次探究一下左值、右值与引用等相关内容。


一、左值与右值的基本概念

1.1 什么是左值(lvalue)和右值(rvalue)

  • 左值(lvalue):表示具有持久存储的对象,可以获取其地址。通常是命名变量、数组元素、类成员等,可以出现在赋值语句的左侧。

  • 右值(rvalue):表示不具有持久存储的临时对象或值,通常是字面量、临时对象、表达式的结果等,不能获取其地址,通常只能出现在赋值语句的右侧。

1.2 判断左值和右值的简单方法

  • 能否取地址:如果可以对表达式取地址(&expr),则为左值;否则为右值。
  • 是否具名:具名对象通常为左值,匿名的临时对象为右值。

1.3 示例

1
2
int x = 5;      // x 是左值,5 是右值
int y = x + 3;  // y 是左值,x + 3 是右值

二、C++11 中的值类别扩展

C++11 对值类别进行了重新分类,引入了新的概念,使得表达式的分类更加精确。这些值类别包括:

  • 左值(lvalue)
  • 亡值(xvalue,eXpiring value)
  • 纯右值(prvalue,Pure rvalue)
  • 泛左值(glvalue,Generalized lvalue)
  • 右值(rvalue)

2.1 新的值类别定义

  • 左值(lvalue):表示标识实体的表达式,可以获取对象的身份(地址)。通常是具名变量或可持久化的存储。

  • 亡值(xvalue):表示即将被移动的对象,资源可以被重用,但仍然具有对象的身份。例如,函数返回的右值引用。

  • 纯右值(prvalue):表示不具有身份的纯值,用于初始化对象或计算表达式结果。例如,字面量、临时对象、算术表达式的结果。

  • 泛左值(glvalue):左值和亡值的统称,表示具有对象身份的表达式。

  • 右值(rvalue):纯右值和亡值的统称,表示没有特定存储的值或即将被移动的对象。

2.2 值类别之间的关系

值类别之间的关系可以用以下关系图表示:

1
2
3
4
5
6
7
8
9
                    表达式(Expressions)
                         |
            +------------+------------+
            |                         |
         glvalue                    rvalue
            |                         |
      +-----+-----+               +---+---+
      |           |               |       |
   lvalue       xvalue         xvalue  prvalue
  • glvalue(泛左值):包括 lvaluexvalue
  • rvalue(右值):包括 xvalueprvalue
  • xvalue(亡值):同时属于 glvaluervalue

2.3 值类别的含义

  • lvalue:具有持久存储,可以获取地址。典型的左值包括变量、函数、数组元素、解引用指针等。

  • xvalue:表示资源可被重用的对象,即将被移动。典型的亡值包括函数返回的右值引用、std::move 的结果等。

  • prvalue:不具有对象身份,仅表示一个值。典型的纯右值包括字面量、临时对象、算术表达式的结果等。

2.4 示例

1
2
3
4
5
6
7
8
class MyClass {};

MyClass createObject() {
    return MyClass(); // 返回一个临时对象,属于纯右值(prvalue)
}

MyClass&& rref = MyClass(); // MyClass() 是纯右值(prvalue),绑定到右值引用
MyClass&& x = std::move(rref); // std::move(rref) 是亡值(xvalue)
  • MyClass():纯右值(prvalue)
  • std::move(rref):亡值(xvalue)

三、左值引用与右值引用

3.1 左值引用(Lvalue Reference)

  • 语法Type& ref_name;
  • 绑定对象:只能绑定到左值。
  • 用途:为左值创建别名,常用于参数传递和返回类型。

3.2 右值引用(Rvalue Reference)

  • 语法Type&& ref_name;
  • 绑定对象:只能绑定到右值(包括纯右值和亡值)。
  • 用途:捕获右值,以实现移动语义和完美转发。

3.3 引用的绑定规则

  • 左值引用:只能绑定到左值。
  • 右值引用:只能绑定到右值。
  • 常量左值引用(const Type&:可以绑定到左值和右值,包括临时对象和字面量。

3.4 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int x = 10;
int& lref = x;       // 左值引用,绑定到左值 x

int&& rref = 20;     // 右值引用,绑定到右值 20

const int& cref = x;     // 常量左值引用,绑定到左值 x
const int& cref2 = 30;   // 常量左值引用,绑定到右值 30

// 错误示例:
// int& lref2 = 20;    // 错误,不能将左值引用绑定到右值
// int&& rref2 = x;    // 错误,不能将右值引用绑定到左值

四、右值引用的用途:移动语义与完美转发

4.1 移动语义(Move Semantics)

  • 目的:通过移动资源(如内存、文件句柄等),避免不必要的拷贝,提高程序性能。
  • 实现:提供移动构造函数和移动赋值运算符,将资源从源对象转移到目标对象。

4.2 移动构造函数与移动赋值运算符

  • 移动构造函数

    1
    
    MyClass(MyClass&& other) noexcept;
    
  • 移动赋值运算符

    1
    
    MyClass& operator=(MyClass&& other) noexcept;
    
  • 实现要点:在移动过程中,转移资源所有权,并将源对象置于可析构的安全状态。

4.3 完美转发(Perfect Forwarding)

  • 目的:在模板函数中,无论传入的是左值还是右值,都能保持其值类别,正确地转发给其他函数。

  • 实现:使用模板参数的右值引用和 std::forward

  • 示例

    1
    2
    3
    4
    
    template <typename T, typename Arg>
    std::shared_ptr<T> factory(Arg&& arg) {
        return std::make_shared<T>(std::forward<Arg>(arg));
    }
    

五、常量左值引用的特殊性

5.1 常量左值引用可以绑定右值

  • 特性const Type& 可以绑定到左值和右值,包括临时对象和字面量。
  • 作用:延长临时对象的生命周期,直到引用超出作用域。

5.2 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void printValue(const int& value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    int x = 10;
    printValue(x);    // 绑定左值
    printValue(20);   // 绑定右值
    return 0;
}
  • 在上述示例中,printValue 函数的参数是 const int&,可以绑定到左值 x,也可以绑定到右值 20

5.3 注意事项

  • 不可修改:由于引用的是常量,无法在函数内部修改其值。
  • 延长生命周期:绑定到右值时,临时对象的生命周期被延长到引用的作用域结束。

六、示例代码分析

6.1 捕获临时对象的右值引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "构造函数\n"; }
    ~MyClass() { std::cout << "析构函数\n"; }
};

MyClass createObject() {
    return MyClass(); // 返回临时对象,属于纯右值(prvalue)
}

int main() {
    MyClass&& rref = createObject(); // 右值引用,绑定到纯右值
    std::cout << "在 main 中\n";
    return 0;
}

输出:

1
2
3
构造函数
在 main 中
析构函数

分析:

  • createObject() 返回一个临时对象(纯右值)。
  • 使用右值引用 MyClass&& rref 绑定临时对象,延长其生命周期到 rref 作用域结束。
  • main 函数结束时,临时对象被析构。

6.2 移动构造函数的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>

class MyVector {
public:
    std::vector<int> data;

    MyVector() { std::cout << "默认构造函数\n"; }

    // 移动构造函数
    MyVector(MyVector&& other) noexcept : data(std::move(other.data)) {
        std::cout << "移动构造函数\n";
    }

    // 禁用拷贝构造函数
    MyVector(const MyVector&) = delete;

    void addData(int value) {
        data.push_back(value);
    }
};

MyVector createVector() {
    MyVector vec;
    vec.addData(1);
    vec.addData(2);
    return std::move(vec); // 显式调用 std::move,触发移动构造
}

int main() {
    MyVector myVec = createVector();
    std::cout << "数据大小:" << myVec.data.size() << std::endl;
    return 0;
}

输出:

1
2
3
默认构造函数
移动构造函数
数据大小:2

分析:

  • createVector() 中的 vec 是一个局部对象,返回时会触发移动构造函数,而不是拷贝构造函数(已被删除)。
  • 通过移动语义,myVec 获得了 vec 的数据,避免了不必要的深拷贝,提高了效率。

七、总结

  • C++11 对值类别进行了重新定义和扩展,引入了 左值(lvalue)亡值(xvalue)纯右值(prvalue)泛左值(glvalue)右值(rvalue)

  • 理解值类别之间的关系 对于正确使用右值引用、移动语义和完美转发非常重要。

  • 右值引用 使得我们可以捕获右值,实现资源的移动,避免不必要的拷贝。

  • 常量左值引用 的特殊性在于可以绑定到右值,延长临时对象的生命周期,但无法修改其值。

  • 移动语义和完美转发 是 C++11 中提高性能和泛型编程能力的重要特性。