Harris
发布于 2026-03-18 / 5 阅读
0
0

深入理解 C++ 值类别 (Value Categories)

深入理解 C++ 值类别 (Value Categories)


在学习现代 C++(C++11 及以后)时,值类别 (Value Categories) 是一个绕不开且容易让人困惑的核心概念。理解它,是掌握移动语义 (Move Semantics)完美转发 (Perfect Forwarding) 的基础。


本文将带你从 C++ 早期简单的“左值 / 右值”概念,平滑过渡到现代 C++ 复杂的“五大值类别”体系。


1. 为什么需要值类别?


在 C++ 表达式中,每个表达式都有两个独立的核心属性:


1. 类型 (Type):例如 int, std::string, const double& 等。


2. 值类别 (Value Category):决定了该表达式能否被取地址、能否被修改、能否将其资源“偷走”(移动)。


在 C++11 引入移动语义之前,C++ 的值类别非常简单:


左值 (lvalue):可以在赋值号左边(通常有名字,能取地址)。
右值 (rvalue):只能在赋值号右边(通常没有名字,不能取地址的临时变量)。

但为了支持移动语义(把即将销毁的对象的资源直接“转移”给新对象,避免深拷贝开销),C++11 对值类别进行了重新分类。


2. 现代 C++ 的值类别树


现代 C++ 将表达式分为三大基础类别和两大组合类别。可以用两个维度的属性来划分它们:


拥有身份 (has identity):是否可以通过指针或引用安全地指向它(即能否取地址)。
可被移动 (can be moved from):是否可以安全地将其内部资源“偷”走。

基于这两个维度,诞生了以下分类树:


表达式 (Expression)
          /                 \
  泛左值 (glvalue)        右值 (rvalue)
     /         \           /         \
左值 (lvalue)  将亡值 (xvalue)  纯右值 (prvalue)

核心属性一览表


| 类别 | 拥有身份 (有内存地址) | 可被移动 (资源可转移) | 含义简述 |

| --- | --- | --- | --- |

| lvalue (左值) | ✅ | ❌ | 具名对象,持久存在 |

| prvalue (纯右值) | ❌ | ✅ | 临时值,不占实际数据内存 |

| xvalue (将亡值) | ✅ | ✅ | 即将销毁的具名对象 (可转移) |


3. 三大基础类别详解


3.1 左值 (lvalue - Left Value)


左值是指在内存中有确定存储地址、有名字(或通过引用/指针可定位)的对象。它们的生命周期超出了单个表达式的范围。
典型特征:可以对其使用取地址符 &
int a = 10;          // 'a' 是左值
int* p = &a;         // 正确:可以对左值取地址
a = 20;              // 正确:可以给非 const 左值赋值

std::string str = "hello"; // 'str' 是左值
str[0] = 'H';              // 'str[0]' 也是左值

int& getRef() { return a; }
getRef() = 30;             // 返回左值引用的函数调用是左值

3.2 纯右值 (prvalue - Pure Right Value)


纯右值是传统的右值,通常是用于初始化对象或计算操作数的值。它们没有名字,也没有持久的内存地址。
典型特征:字面量、返回非引用类型的函数调用、临时对象。不能对其取地址。
int a = 10 + 20;     // '10 + 20' 是纯右值
// int* p = &(10);   // 错误:不能对纯右值取地址

std::string getName() { return "PKM"; }
getName();           // 函数返回的值(非引用)是纯右值

int x = 5;
int y = x++;         // 'x++' 是纯右值(先返回原值的临时拷贝)
                     // 注意:'++x' 是左值

3.3 将亡值 (xvalue - eXpiring Value)


将亡值是 C++11 引入的最关键概念。它表示一个拥有身份(有地址),但其生命周期即将结束,其内部资源可以被安全移动(窃取)的对象
典型特征:通常与右值引用 && 绑定。
std::string str = "hello";
// std::move(str) 将左值 str 显式转换为右值引用,
// 此时 std::move(str) 的返回值就是一个将亡值 (xvalue)。
std::string str2 = std::move(str); 

// 强转为右值引用也是将亡值
static_cast<std::string&&>(str);

4. 两大组合类别


为了在编译器规范中方便描述,C++ 标准委员会定义了两个泛化的组合类别:


4.1 泛左值 (glvalue = lvalue + xvalue)


泛左值 (Generalized lvalue) 是指所有拥有身份(内存地址)的表达式。 无论是普通的左值,还是即将被移动的将亡值,它们在内存中都有确切的位置。


4.2 右值 (rvalue = prvalue + xvalue)


现代 C++ 中的右值,是指所有可以被移动(资源可窃取)的表达式。 包括纯粹的临时变量(纯右值),以及被标记为即将销毁的对象(将亡值)。


5. 为什么这很重要?(实际应用场景)


理解值类别主要为了理解函数的重载决议 (Overload Resolution),特别是拷贝构造函数与移动构造函数。


class MyVector {
public:
    // 拷贝构造函数:接受左值 (lvalue),执行深拷贝
    MyVector(const MyVector& other) {
        std::cout << "Copy Construct (深拷贝)\n";
    }

    // 移动构造函数:接受右值 (rvalue = xvalue/prvalue),执行指针转移
    MyVector(MyVector&& other) noexcept {
        std::cout << "Move Construct (资源窃取)\n";
    }
};

int main() {
    MyVector v1; 
    
    // 1. v1 是左值,调用拷贝构造
    MyVector v2(v1); 
    
    // 2. MyVector() 是纯右值 (prvalue),调用移动构造
    MyVector v3(MyVector()); 
    
    // 3. std::move(v1) 是将亡值 (xvalue),属于右值范畴,调用移动构造
    MyVector v4(std::move(v1)); 
}

总结


左值 (lvalue):能取地址,能修改,不愿交出资源(持久存在)。
纯右值 (prvalue):不能取地址,临时产生的数据计算结果。
将亡值 (xvalue):能取地址,但即将销毁,愿意交出资源供他人“榨干”。
C++11 引入将亡值,桥接了左值的“有内存身份”和右值的“可被移动”特性,从而完美支撑了提升性能的移动语义

评论