头文件<utility>


一、什么是:左值 & 右值

在 C++ 中,表达式根据能否取地址、是否持久存在,分为左值右值,这是理解后续知识点的基础。

1. 左值(lvalue)

  • 定义:有名字、可以取地址的表达式,程序运行中内存持久存在。
  • 常见例子:变量、数组元素、引用。
int a = 10;   // a 是左值,有变量名,可以 &a 取地址
int b = a;    // a 依旧是左值
int arr[3] = {1,2,3};
arr[0];       // 数组元素也是左值
  • 特点:能出现在赋值运算符 = 左边,所以叫左值。
    a = 20; 合法,a 是左值。

2. 右值(rvalue)

  • 定义:没有名字、不能取地址的临时数据,用完就销毁,生命周期极短。
  • 常见例子:字面量、表达式运算结果、函数返回的临时对象。
100;          // 字面量,右值,无法 &100 取地址
a + b;        // 加法运算结果,临时值,右值
std::string("test"); // 临时字符串对象,右值
  • 特点:只能出现在赋值运算符 = 右边不能取地址。
    100 = a; 编译报错,右值不能被赋值。

简易记忆口诀

有名字、能取地址 → 左值
无名字、临时值、不能取地址 → 右值


二、左值引用 & C++11 新增:右值引用

引用就是给变量起别名,C++98 只有左值引用,C++11 补充了右值引用

1. 回顾:C++98 左值引用 &

语法:类型 & 引用名 = 左值;

  • 只能绑定左值,不能直接绑定纯右值。
int a = 10;
int &ra = a;    // 合法:左值引用 绑定 左值

// int &rb = 20; // 编译报错!左值引用不能直接绑定字面量(右值)
const int &rc = 20; // 特殊:const 左值引用可以绑定右值(C++98 特性)//这么做会延长右值的生命周期

2. C++11 右值引用 &&

语法:类型 && 引用名 = 右值;

  • 作用:专门用来绑定右值(临时对象)
  • 符号:两个 &&,和左值引用 & 区分开
基础示例
// 1. 绑定字面量(右值)
int &&r1 = 100;
r1 = 200;        // 右值引用本质也是别名,可以修改
cout << r1;      // 输出 200
//普通右值引用 T&& 可以修改;const T&& 不能修改(几乎不用)。

// 2. 绑定表达式临时结果(右值)
int x = 1, y = 2;
int &&r2 = x + y;
cout << r2;      // 输出 3
关键特性
  1. 右值引用只能绑定右值不能直接绑定普通左值
    int a = 10;
    // int &&r = a;  // 编译报错:左值不能绑定到右值引用
    
  2. 一旦右值引用绑定了临时右值,这个引用本身就变成了左值(有名字、可寻址)。

三、为什么需要右值引用?—— 解决拷贝浪费

1. 传统拷贝的问题

对于自定义类、字符串、容器这类占用堆内存的对象,普通拷贝会做深拷贝
完整复制一份内存数据,临时对象产生 → 拷贝 → 临时对象销毁,产生大量不必要的内存开销和性能损耗

举个例子:自定义字符串类(模拟 std::string

#include <iostream>
#include <cstring>
using namespace std;

class MyString
{
private:
    char *data;
public:
    // 构造函数
    MyString(const char *str)
    {
        cout << "构造函数" << endl;
        int len = strlen(str);
        data = new char[len + 1];
        strcpy(data, str);
    }

    // 拷贝构造函数(深拷贝)
    MyString(const MyString &other)
    {
        cout << "拷贝构造函数(深拷贝)" << endl;
        int len = strlen(other.data);
        data = new char[len + 1]; // 重新开辟内存
        strcpy(data, other.data);
    }

    // 析构函数
    ~MyString()
    {
        cout << "析构函数" << endl;
        delete[] data;
    }
};

// 返回临时 MyString 对象(右值)
MyString getTempStr()
{
    return MyString("hello c++11");
}

int main()
{
    MyString s = getTempStr(); 
    return 0;
}
执行流程(C++98 模式)
  1. getTempStr() 内部创建临时对象 → 调用构造函数
  2. 临时对象赋值给 s → 调用拷贝构造函数(完整复制内存)
  3. 临时对象生命周期结束 → 调用析构函数释放内存

问题:临时对象马上就要销毁,我们明明可以直接把临时对象的内存“抢过来”用,却还要完整拷贝一遍,纯纯资源浪费。

2. 解决方案:移动语义

移动语义:借助右值引用,把即将销毁的临时对象(右值) 的内存资源,直接转移给新对象,而非拷贝。

  • 动作:转移所有权,不是复制数据
  • 效率:几乎零开销,比深拷贝快得多

四、移动构造函数(移动语义核心)

C++11 允许我们重载移动构造函数,语法基于右值引用

1. 语法格式

类名(类名 && 临时对象)
{
    // 转移资源,不拷贝
}

2. 给上面的MyString 添加移动构造函数

// 移动构造函数:参数是 右值引用
MyString(MyString &&other)
{
    cout << "移动构造函数(转移资源)" << endl;
    // 1. 直接接管对方的堆内存指针
    data = other.data;
    // 2. 把原对象指针置空,防止原对象析构时重复释放内存
    other.data = nullptr;
}

3. 完整运行 & 效果对比

再次运行 MyString s = getTempStr();

C++11 执行流程:
  1. 创建临时对象 → 构造函数
  2. 临时对象是右值,编译器自动匹配 移动构造函数(转移指针,无内存拷贝)
  3. 临时对象析构(指针已置空,不会释放有效内存)

总结:有了移动构造,临时对象不再深拷贝,直接移交资源,性能大幅提升。

补充规则

  1. 编译器匹配规则:
    • 传入左值 → 调用 拷贝构造(const 类&
    • 传入右值 → 调用 移动构造(类&&
  2. 移动构造函数中,必须把源对象的资源指针置空,否则两个对象指向同一块堆内存,析构时会重复释放,程序崩溃。

五、std::move 函数(强制转为右值)

1. 作用

std::move 定义在 <utility> 头文件中,功能只有一个

强制把一个左值,转换成右值

场景:明明是有名字的左值对象(不会马上销毁),但我们确定不再使用它,希望把它的资源移动给其他对象,而非拷贝。

2. 语法

std::move(左值变量)

3. 示例演示

沿用 MyString 类:

int main()
{
    MyString s1("test move");  // s1 是普通左值
    // 直接赋值:左值 → 调用拷贝构造
    MyString s2 = s1;          

    // 使用 std::move:把左值 s1 强制转为右值
    MyString s3 = std::move(s1); 

    return 0;
}

运行结果:

  • s2 = s1:调用 拷贝构造
  • s3 = std::move(s1)s1 被转为右值,调用 移动构造

4. 重要使用提醒

执行 std::move(s1) 之后:

  1. s1 的堆内存资源已经被转移走,内部指针变为 nullptr
  2. 不要再正常使用 s1(相当于一个“空壳对象”),强行读写会出问题;
  3. std::move 本身不移动任何数据,它只是做类型转换,真正转移资源的是移动构造/移动赋值函数

六、移动赋值运算符

和拷贝赋值对应,C++11 也支持移动赋值,同样使用右值引用。

1. 语法

类名 & operator=(类名 &&other)
{
    cout << "移动赋值运算符" << endl;
    // 1. 释放自身原有资源
    delete[] data;
    // 2. 接管对方资源
    data = other.data;
    // 3. 原对象置空
    other.data = nullptr;
    return *this;
}

2. 使用场景

MyString s4("aaa");
MyString s5("bbb");
s5 = std::move(s4); // 移动赋值,而非拷贝赋值

七、四大构造/赋值函数总结

C++11 之后,一个常规类默认存在 6 个特殊成员函数,这里区分核心 4 个:

函数类型 参数形式 触发场景 行为
普通构造函数 类名(参数) 创建新对象时 初始化资源
拷贝构造函数 类名(const 类&) 左值对象初始化新对象 深拷贝数据
移动构造函数 类名(类&&) 右值对象初始化新对象 转移资源
拷贝赋值运算符 operator=(const 类&) 左值对象赋值 深拷贝数据
移动赋值运算符 operator=(类&&) 右值对象赋值 转移资源

八、常见使用场景 & 实战建议

1. 什么时候会自动触发移动语义?

函数返回临时对象、字面量构造的临时对象 → 编译器自动识别为右值,调用移动构造。

2. 什么时候手动用 std::move?

明确某个左值对象后续不再使用,想把它的资源转移出去,手动转成右值。
例:容器元素转移、局部对象移交。

3. 注意事项

  1. 右值引用 && 只能绑定右值,普通左值不能直接赋值给右值引用;
  2. std::move 只是类型转换,不是移动动作本身;
  3. 移动资源后,原对象变为空壳,禁止继续正常使用;
  4. 移动构造/移动赋值中,一定要将源对象指针置空,防止重复析构崩溃;
  5. 内置类型(int/double)移动和拷贝无区别,移动语义主要针对堆内存对象(string、vector、自定义类)。

总结

  1. 左值:有名字、可寻址;右值:临时值、无名字、不可寻址。
  2. C++11 新增 右值引用 &&,专门绑定右值。
  3. 移动语义:利用右值引用,转移临时对象资源,替代低效深拷贝。
  4. 移动构造/移动赋值:实现移动语义的核心函数。
  5. std::move:强制将左值转为右值,手动触发移动语义。
  6. 核心价值:优化内存拷贝,提升程序运行效率

更多推荐