c++ 11左值和右值

C++11 系列(二):右值引用与移动语义完全指南

前言

C++11 引入了右值引用(T&&)和移动语义,这是 C++11 最重要的特性之一。它们解决了 C++98 中临时对象拷贝效率低下的问题,为现代 C++ 的高性能编程奠定了基础。

本文将从左值与右值的基本概念出发,系统讲解右值引用、移动构造/移动赋值、引用折叠、完美转发等核心知识。


一、左值与右值

1.1 基本概念

在 C++ 中,表达式可以分为左值和右值:

类型 含义 特点 示例
左值(lvalue) 有持久状态、可取地址 可以出现在赋值左边 变量名、*ps[0]
右值(rvalue) 临时值、不可取地址 只能出现在赋值右边 字面量、x+y、临时对象

现代 C++ 中,lvalue 被解释为 locator value (有地址的值),rvalue 被解释为 read value(仅可读的值)。

1.2 代码示例

cpp 复制代码
int main() {
    // 左值:可以取地址
    int* p = new int(0);
    int b = 1;
    const int c = b;
    string s("hello");
    s[0] = 'x';

    cout << &c << endl;      // 可以取地址
    cout << (void*)&s[0] << endl;

    // 右值:不能取地址
    double x = 1.1, y = 2.2;
    10;                      // 字面量
    x + y;                   // 表达式结果
    string("temp");          // 临时对象

    // cout << &10 << endl;  // 错误:不能取地址
    return 0;
}

二、左值引用与右值引用

2.1 基本语法

cpp 复制代码
int a = 10;
int& lref = a;      // 左值引用:绑定到左值
int&& rref = 20;    // 右值引用:绑定到右值

2.2 引用绑定规则

引用类型 能否绑定左值 能否绑定右值
T&(左值引用)
const T&(const 左值引用)
T&&(右值引用)

2.3 std::move:将左值转为右值

cpp 复制代码
int a = 10;
int&& rref = std::move(a);   // 将左值 a 转为右值引用

std::move 的本质是一个强制类型转换:

cpp 复制代码
template<class T>
remove_reference_t<T>&& move(T&& arg) {
    return static_cast<remove_reference_t<T>&&>(arg);
}

2.4 重要:右值引用变量本身是左值

cpp 复制代码
int&& rref = 10;   // rref 绑定到右值 10
int& lref = rref;  // 正确:rref 本身是左值
// int&& rref2 = rref;  // 错误:不能将右值引用绑定到左值
int&& rref2 = std::move(rref);  // 正确:需要再次 move

⚠️ 关键理解:右值引用变量在表达式中被视为左值,因为它有名称、可取地址。


三、移动语义

3.1 为什么需要移动语义?

在 C++98 中,返回局部对象或插入临时对象时,会发生深拷贝,效率低下:

cpp 复制代码
string addStrings(string num1, string num2) {
    string str;
    // ... 拼接逻辑
    return str;  // 需要拷贝,效率低
}

左值引用虽然能减少拷贝,但无法解决 返回局部对象 的拷贝问题。移动语义允许我们 窃取 临时对象的资源,而不是拷贝。

3.2 移动构造与移动赋值

cpp 复制代码
class string {
public:
    // 拷贝构造:深拷贝
    string(const string& s) {
        // 分配新内存,复制数据
    }

    // 移动构造:窃取资源
    string(string&& s) noexcept {
        _str = s._str;      // 直接接管指针
        s._str = nullptr;   // 将源对象置空
    }

    // 移动赋值:窃取资源
    string& operator=(string&& s) noexcept {
        if (this != &s) {
            delete[] _str;
            _str = s._str;
            s._str = nullptr;
        }
        return *this;
    }

private:
    char* _str;
};

3.3 使用场景

cpp 复制代码
int main() {
    bit::string s1("hello");

    // 拷贝构造(s1 是左值)
    bit::string s2 = s1;

    // 移动构造(匿名对象是右值)
    bit::string s3 = bit::string("world");

    // 移动构造(move 将左值转为右值)
    bit::string s4 = std::move(s1);

    return 0;
}

3.4 移动语义在容器中的提效

C++11 后,STL 容器的 push_backinsert 等接口增加了右值引用版本:

cpp 复制代码
list<string> lt;
string s1("hello");

lt.push_back(s1);                    // 拷贝构造
lt.push_back(string("world"));       // 移动构造
lt.push_back("hello world");         // 移动构造
lt.push_back(std::move(s1));         // 移动构造

四、引用折叠

4.1 问题引入

C++ 不允许直接定义引用的引用,但在模板或类型别名中会出现:

cpp 复制代码
typedef int& lref;
lref& r1 = n;   // r1 的类型是什么?

4.2 折叠规则

C++11 定义了引用折叠规则:

原始类型 折叠后
T& & T&
T& && T&
T&& & T&
T&& && T&&

总结:只有右值引用的右值引用折叠成右值引用,其他全部折叠成左值引用。

4.3 万能引用(Universal Reference)

在模板中,T&& 可以根据实参推导为左值引用或右值引用:

cpp 复制代码
template<class T>
void Function(T&& t) {   // 万能引用
    // t 的类型取决于实参
}

int main() {
    int a = 10;
    Function(10);    // T = int,参数类型:int&&(右值引用)
    Function(a);     // T = int&,参数类型:int&(左值引用)
    return 0;
}

五、完美转发

5.1 问题:参数类型丢失

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }

template<class T>
void Function(T&& t) {
    Fun(t);   // t 是变量表达式,永远是左值!
}

Function(10);   // 输出:左值引用(但预期应该是右值引用)

5.2 解决方案:std::forward

cpp 复制代码
template<class T>
void Function(T&& t) {
    Fun(std::forward<T>(t));   // 保持 t 的原始类型
}

5.3 forward 的实现原理

cpp 复制代码
template<class T>
T&& forward(remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
}
  • 如果 Tint&,则返回 int&
  • 如果 Tint,则返回 int&&

六、值类别(补充知识)

C++11 进一步细化了值类别:

复制代码
        expression
        /        \
    glvalue      rvalue
    /    \       /    \
lvalue   xvalue prvalue
类别 说明 示例
lvalue 左值 变量名、*p
xvalue(将亡值) 即将被移动的对象 std::move(x)T&& 函数返回值
prvalue(纯右值) 字面量、临时对象 42a+bstring("tmp")

七、总结

特性 作用
左值引用 T& 减少拷贝,可修改实参
右值引用 T&& 绑定临时对象,实现移动语义
std::move 将左值转换为右值引用
移动构造/赋值 窃取临时对象资源,避免深拷贝
引用折叠 模板中 T&& 的推导规则
完美转发 std::forward 保持参数在传递过程中的原始类型

核心要点

  1. 右值引用变量本身是左值(有名称、可取地址)
  2. 移动语义只对深拷贝类型有意义 (如 stringvector
  3. std::move 只是类型转换,不移动任何东西
  4. std::forward 用于模板中保持参数的原始类型
  5. 移动构造/赋值通常应标记为 noexcept,以便 STL 容器优先使用

相关推荐
Hical_W1 小时前
C++ Web 框架性能实测(Benchmark)
c++·开源
lzh200409192 小时前
手撕线程池:巩固Linux线程知识
linux·c++
basketball6162 小时前
C++ 命名空间知识点总结:从入门到合理设计
开发语言·c++
handler012 小时前
【C++ 算法竞赛基础】数论篇:核心公式、经典例题与高频模板
开发语言·c++·算法·蓝桥杯·数论·最大公约数·最小公倍数
fpcc2 小时前
并行编程实战——CUDA编程的打印输出
c++·cuda
程序leo源3 小时前
Qt信号与槽深度详解
c语言·开发语言·数据库·c++·qt·c#
水云桐程序员3 小时前
C++数组详细介绍
开发语言·c++
z200509303 小时前
今日算法(二叉树)
数据结构·c++·算法
故事和你913 小时前
洛谷-【图论2-2】最短路1
开发语言·数据结构·c++·算法·动态规划·图论