【C++】C++11(1):右值引用和移动语义

目录

[一 C++11的发展历史](#一 C++11的发展历史)

[二 C++11中的{ }](#二 C++11中的{ })

[1 C++98传统的{}](#1 C++98传统的{})

[2 C++11的{}](#2 C++11的{})

[三 C++11中的std::initializer_list](#三 C++11中的std::initializer_list)

[四 右值引用和移动语义](#四 右值引用和移动语义)

[1 左值和右值](#1 左值和右值)

[2 左值引用和右值引用](#2 左值引用和右值引用)

[3 引用延长生命周期](#3 引用延长生命周期)

[4 左值和右值的参数匹配](#4 左值和右值的参数匹配)

[5 右值引用和引用语义的使用场景](#5 右值引用和引用语义的使用场景)

[(1) 左值引用主要使用场景回顾](#(1) 左值引用主要使用场景回顾)

(2)移动构造和移动赋值

(3)右值引用和移动语义解决传值返回问题

(4)右值引用和移动语义在传参中的提效


一 C++11的发展历史

C++11是C++的第二个主要版本,并且是从C++98起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,人们曾使用名称"C++0x",因为它曾被期待在2010年之前发布。C++03与C++11期间花了8年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新一次。

我们目前在学习C++时,用到的主要是C++98和C++11。C++14-C++20称为现代C++

由标准委员会指定理论语法


二 C++11中的{ }

1 C++98传统的{}

C++98中⼀般数组和结构体可以⽤{}进行初始化。

cpp 复制代码
struct Point
{
 int _x;
 int _y;
};
int main()
{
 int array1[] = { 1, 2, 3, 4, 5 };
 int array2[5] = { 0 };
 Point p = { 1, 2 };
 
 return 0;
}

2 C++11的{}

• C++11以后想统⼀初始化方式,试图实现⼀切对象皆可用{}初始化,{}初始化也叫做列表初始化。

• 内置类型支持,自定义类型也支持,⾃定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。

• {}初始化的过程中,可以省略掉=

• C++11列表初始化的本意是想实现⼀个大统⼀的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便

cpp 复制代码
int main()
{
    // C++98 风格初始化
    int array1[] = { 1, 2, 3, 4, 5 }; // 聚合初始化,长度由初始化列表推导
    int array2[5] = { 0 };            // 部分初始化,剩余元素默认置 0(C++98 特性)
    Point p = { 1, 2 };               // 结构体聚合初始化(C++98 支持)

    Date d1(2025, 11, 15);            // 调用全参构造函数

    Date d2 = 2025;                   // 隐式转换:int -> Date(调用单参构造)
    Date d5 = { 2025, 11, 15 };       // 列表初始化(C++98 聚合初始化兼容,C++11 增强)
    Date d6{ 2025, 11, 15 };          // C++11 列表初始化(直接初始化)
    Date d7{};                        // C++11 列表初始化(无参,调用默认构造)

    Insert(2025);                     // 调用 Insert(int)
    Insert({ 2025, 11, 15 });         // 列表初始化临时 Date 对象,调用 Insert(const Date&)

    int i = 0;                        // C++98 拷贝初始化
    int j = { 1 };                    // C++11 列表拷贝初始化
    int k{ 2 };                       // C++11 列表直接初始化
    int m{};                          // C++11 列表初始化,默认置 0(值初始化)

    return 0;
}

当只传一个参数时,剩下两个用缺省值(例如d2)

d5和d6相当于直接构造一个日期类(可省略=)

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

struct Point
{
 int _x;
 int _y;
};

class Date
{
 public:
 Date(int year = 1, int month = 1, int day = 1)
 :_year(year)
 , _month(month)
 , _day(day)
 {
 cout << "Date(int year, int month, int day)" << endl;
 }
 Date(const Date& d)
 :_year(d._year)
 , _month(d._month)
 , _day(d._day)
 {
 cout << "Date(const Date& d)" << endl;
 }

 private:
 int _year;
 int _month;
 int _day;
};

// ⼀切皆可⽤列表初始化,且可以不加= 
int main()
{
 // C++98⽀持的 
 int a1[] = { 1, 2, 3, 4, 5 };
 int a2[5] = { 0 };
 Point p = { 1, 2 };

 // C++11⽀持的 
 // 内置类型⽀持 
 int x1 = { 2 };

 // ⾃定义类型⽀持 
 // 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象 
 // 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化
d1
 // 运⾏⼀下,我们可以验证上⾯的理论,发现是没调⽤拷⻉构造的 
 Date d1 = { 2025, 1, 1};

 // 这⾥d2引⽤的是{ 2024, 7, 25 }构造的临时对象 
 const Date& d2 = { 2024, 7, 25 };

 // 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{} 
 Date d3 = { 2025};
 Date d4 = 2025;

 // 可以省略掉= 
 Point p1 { 1, 2 };
 int x2 { 2 };
 Date d6 { 2024, 7, 25 };
 const Date& d7 { 2024, 7, 25 };

 // 不⽀持,只有{}初始化,才能省略= 
 // Date d8 2025;
 vector<Date> v;
 v.push_back(d1);
 v.push_back(Date(2025, 1, 1));

 // ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐ 
 v.push_back({ 2025, 1, 1 });
 return 0;
}

单参数类型转换指:通过 "仅接收一个参数的构造函数" 或 "类型转换运算符",实现从一种类型到另一种类型的隐式 / 显式转换

内置类型既可以直接赋值,也可以用{ }初始化,那么这里还用{ }初始化的意义是什么呢?

C++就是要让一切皆可用{ }初始化


三 C++11中的std::initializer_list

• 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能支持

cpp 复制代码
vector<int> v1 = {1,2,3};
vector<int> v2 = {1,2,3,4,5};

• C++11库中提出了一个std::initializer_list的类

cpp 复制代码
auto il = { 10, 20, 30 };

// the type of il is an initializer_list 

这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。

• 这是他的文档:initializer_list,std::initializer_list支持迭代器遍历。

• 容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL中的容器支持任意多个值构成的 {x1,x2,x3...} 进行初始化,就是通过std::initializer_list的构造函数支持的。

我们来详细解释一下:

因为容器的长度可能是任意长度,所以我们不能使用使用C++98的思路去初始化。例如想初始化一个包含 3 个元素的 vector<int>,C++98 只能这样写(繁琐):

所以C++11引入了std::initializer_list,它的核心作用是 "打包任意长度的同类型值列表"

不负责存储数据 ,数据实际存在编译器创建的临时数组中,initializer_list 只是 "引用" 这个数组

STL 容器(vector、map、set 等)都新增了一个以 std::initializer_list<T> 为参数的构造函数 ,从而支持 {x1,x2,x3...} 初始化。

cpp 复制代码
int main()
{
    // 1. vector 列表初始化:{} 被转为 initializer_list<int>,调用 vector 的对应构造函数
    // 支持 = 写法(拷贝初始化风格),内部元素由 {1,2,3,4} 初始化
    vector<int> v1 = { 1,2,3,4 };
    
    // vector 列表初始化:省略 =(直接初始化风格),语义与上面一致
    // 支持任意长度值列表,无需额外构造函数,由 initializer_list 打包
    vector<int> v2{ 1,2,3,4,5,6,6,7,7 };

    // 2. map 列表初始化:map 元素是 pair,需嵌套 {}
    // 外层 {}:转为 initializer_list<pair<string, string>>,传给 map 构造函数
    // 内层 {}:每个 {key, value} 是 pair<string, string> 的列表初始化(构造单个键值对元素)
    map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

    // 3. 容器列表赋值:vector 重载了 initializer_list 版本的 operator=
    // 清空 v1 原有元素,遍历 {10,20,30} 对应的 initializer_list,插入新元素
    v1 = { 10,20,30 };

    // 4. initializer_list 类型推导:auto 会将纯 {} 列表推导为 std::initializer_list<T>
    // il 的类型是 std::initializer_list<int>(T 由元素类型推导为 int)
    auto il = { 10, 20, 30 };
    // 打印 il 的类型名(编译器会输出 mangled 后的类型,本质是 std::initializer_list<int>)
    cout << typeid(il).name() << endl;

    // 5. initializer_list 直接定义与赋值
    std::initializer_list<int> mylist; // 定义空的 initializer_list(无绑定临时数组)
    mylist = { 10, 20, 30 }; // 赋值后,mylist 绑定栈上新建的临时数组 {10,20,30}
    // 打印 initializer_list 大小:与元素个数无关,仅存两个指针(64位占16字节,32位占8字节)
    cout << sizeof(mylist) << endl;

    // 6. 验证 initializer_list 底层临时数组的存储位置(栈上)
    // initializer_list 的 begin()/end() 返回指向临时数组的指针
    // 栈上内存连续分配,临时数组地址与局部变量 i 的地址会非常接近
    int i = 0; // 局部变量,存储在栈上
    cout << mylist.begin() << endl; //
    cout << mylist.end() << endl;   // 输出临时数组的结束地址(起始地址 + 元素个数*int字节数)
    cout << &i << endl;             // 输出局部变量 i 的栈地址(与临时数组地址接近,证明在栈上)

    return 0;
}
```

详细的解释在上面


四 右值引用和移动语义

C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

1 左值和右值

左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址左值可以出现赋值符号的左边,也可以出现在赋值符号右边 。定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址

• 右值也是一个表示数据的表达式,要么是字面量常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边右值不能取地址

• 值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value的缩写。现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象 ,而rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址 ,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
 // 左值:可以取地址 
 // 以下的p、b、c、*p、s、s[0]就是常⻅的左值 
 int* p = new int(0);
 int b = 1;
 const int c = b;
 *p = 10;
 string s("111111");
 s[0] = 'x';
 cout << &c << endl;
 cout << (void*)&s[0] << endl;
 // 右值:不能取地址 
 double x = 1.1, y = 2.2;
 // 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值 
 10;
 x + y;
 fmin(x, y);
 string("11111");
 //cout << &10 << endl;
 //cout << &(x+y) << endl;
 //cout << &(fmin(x, y)) << endl;
 //cout << &string("11111") << endl;
 return 0;
}

上述注释掉的部分:右值不能取地址,不然编译器会报错

2 左值引用和右值引用

• Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。

左值引用不能直接引用右值,但是const左值引用可以引用右值

• 右值引用不能直接引用左值,但是右值引用可以引用move(左值)

• template <class T> typename remove_reference<T>::type&& move (T&& arg);

• move是库里的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识,这个我们后面会细讲

• 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量的表达式属性是左值

• 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到一起去理解,互相佐证,这样反而是陷入迷途。

cpp 复制代码
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg) 
{ // forward _Arg as movable
 return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

#include<iostream>
using namespace std;

int main()
{
 // 左值:可以取地址 
 // 以下的p、b、c、*p、s、s[0]就是常⻅的左值 
 int* p = new int(0);
 int b = 1;
 const int c = b;
 *p = 10;
 string s("111111");
 s[0] = 'x';
 double x = 1.1, y = 2.2;

 // 左值引⽤给左值取别名 
 int& r1 = b;
 int*& r2 = p;
 int& r3 = *p;
 string& r4 = s;
 char& r5 = s[0];

 // 右值引⽤给右值取别名 
 int&& rr1 = 10;
 double&& rr2 = x + y;
 double&& rr3 = fmin(x, y);
 string&& rr4 = string("11111");

 // 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值 
 const int& rx1 = 10;
 const double& rx2 = x + y;
 const double& rx3 = fmin(x, y);
 const string& rx4 = string("11111");

 // 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值) 
 int&& rrx1 = move(b);
 int*&& rrx2 = move(p);
 int&& rrx3 = move(*p);
 string&& rrx4 = move(s);
 string&& rrx5 = (string&&)s;

 // b、r1、rr1都是变量表达式,都是左值 
 cout << &b << endl;
 cout << &r1 << endl;
 cout << &rr1 << endl;

 // 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下 
 int& r6 = r1;
 // int&& rrx6 = rr1;
 int&& rrx6 = move(rr1);

 return 0;
}

3 引用延长生命周期

• 右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。

cpp 复制代码
int main()
{
 std::string s1 = "Test";
 // std::string&& r1 = s1; // 错误:不能绑定到左值 

 const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期 
 // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改 

 std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期 
 r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改 

 std::cout << r3 << '\n';
 return 0;
}

4 左值和右值的参数匹配

• C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。

• C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值 会匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。

• 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,下一小节我们讲右值引用的使用场景时,就能体会这样设计的价值了。

cpp 复制代码
#include<iostream>
using namespace std;

void f(int& x)
{
 std::cout << "左值引⽤重载 f(" << x << ")\n";
}

void f(const int& x)
{
 std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}

void f(int&& x)
{
 std::cout << "右值引⽤重载 f(" << x << ")\n";
}

int main()
{
 int i = 1;
 const int ci = 2;

 f(i); // 调⽤ f(int&) 
 f(ci); // 调⽤ f(const int&) 
 f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&) 
 f(std::move(i)); // 调⽤ f(int&&) 

 // 右值引⽤变量在⽤于表达式时是左值
 int&& x = 1;
 f(x); // 调⽤ f(int& x) 
 f(std::move(x)); // 调⽤ f(int&& x) 

 return 0;
}

5 右值引用和引用语义的使用场景

(1) 左值引用主要使用场景回顾

左值引用的主要使用场景是在函数中通过左值引用传参和左值引用传返回值,以此减少拷贝操作,同时还具备修改实参和修改返回对象的作用。左值引用已经解决了大多数场景下的拷贝效率问题,但有些场景无法使用左值引用传返回值,比如 addStrings 和 generate 这类函数。在 C++98 中,对应的解决方案只能是被迫使用输出型参数来处理。那么 C++11 以后,这里可以使用右值引用来做返回值解决吗?显然是不可能的,因为核心问题在于返回的对象是局部对象,函数执行结束后该对象就会析构销毁,即便用右值引用返回,也无法改变对象已经析构销毁的事实。

左值引用解决的效率问题:

1 传左值引用传参,减少拷贝

2 传左值引用返回,减少拷贝(部分场景可用)

传统拷贝的问题:

传统拷贝时,会在返回str的时候生成一个临时对象,把str的内容拷贝 给临时对象,然后临时对象再拷贝给ret,一共要拷贝两次,效率低下

我们通过一个实例来看一下局部对象传值返回在传统模式下的拷贝代价问题以及局部对象生命周期限制导致无法用引用返回

(2)移动构造和移动赋值
  • 移动构造函数是一种构造函数(&&),类似拷贝构造函数(&)。它要求第一个参数是该类类型的引用,但不同的是,这个参数必须是右值引用;若存在其他参数,额外参数必须提供缺省值

  • 移动赋值运算符(&&)是赋值运算符的重载,与拷贝赋值运算符(&)构成函数重载。它类似拷贝赋值运算符,要求第一个参数是该类类型的引用,但不同的是,这个参数必须是右值引用

  • 对于 string、vector 这类需要深拷贝的类,或是包含深拷贝成员变量的类 ,移动构造和移动赋值才有实际意义。因为移动构造和移动赋值的第一个参数都是右值引用类型,其核心是"窃取"所引用右值对象的资源,而非像拷贝构造、拷贝赋值那样复制资源,从而提升代码执行效率。以下的 bit::string 样例实现了移动构造和移动赋值,我们需要结合具体场景理解其用法。

我们来对比一下,相同的函数,使用拷贝构造和移动构造的不同:

流程类型 步骤说明 效率差异
拷贝构造流程 函数内局部对象 str → 拷贝构造临时对象 → 拷贝构造 main 中的 ret → 两次拷贝,两次析构 效率低,深拷贝类(如 string)开销大
移动构造流程 函数内局部对象 str → 移动构造临时对象 → 移动构造 main 中的 ret → 无拷贝,仅 "窃取" 资源 效率高,避免深拷贝开销
(3)右值引用和移动语义解决传值返回问题

右值对象构造(仅拷贝构造、无移动构造的场景)

  • 图1展示了VS2019 debug环境下编译器对拷贝的优化:左边是未优化的情况,会执行两次拷贝构造;右边是编译器优化后的场景,将连续步骤中的拷贝操作合二为一,仅执行一次拷贝构造

  • 需注意:在VS2019的release模式,以及VS2022的debug和release模式下,以下代码的优化效果非常显著------会直接将"str对象构造、str拷贝构造临时对象、临时对象拷贝构造ret对象"这三个步骤合三为一,变为直接构造ret对象。要理解该优化,需从局部对象的生命周期和栈帧的角度切入,具体可参考图3

  • Linux环境下,可将以下代码拷贝到test.cpp文件中,编译时通过 `g++ test.cpp -fno-elideconstructors` 命令关闭构造优化,运行结果会呈现图1左边未优化时的两次拷贝构造过程。

右值对象构造(同时存在拷贝构造和移动构造的场景)

  • 图2展示了VS2019 debug环境下编译器对拷贝的优化:左边是未优化的情况,会执行两次移动构造;右边是编译器优化后的场景,将连续步骤中的拷贝操作合二为一,仅执行一次移动构造。

  • 需注意:在VS2019的release模式,以及VS2022的debug和release模式下,以下代码的优化效果非常显著------会直接将"str对象构造、str拷贝构造临时对象、临时对象拷贝构造ret对象"这三个步骤合三为一,变为直接构造ret对象。要理解该优化,需从局部对象的生命周期和栈帧的角度切入,具体可参考图3。

  • Linux环境下,可将以下代码拷贝到test.cpp文件中,编译时通过 `g++ test.cpp -fno-elideconstructors` 命令关闭构造优化,运行结果会呈现图2左边未优化时的两次移动构造过程。

右值对象赋值(仅存在拷贝构造和拷贝赋值、无移动构造和移动赋值的场景)

  • 图4左边展示了VS2019 debug环境和`g++ test.cpp -fno-elide-constructors`关闭优化环境下编译器的处理逻辑:会执行一次拷贝构造和一次拷贝赋值。

  • 需注意:在VS2019的release模式,以及VS2022的debug和release模式下,以下代码会进一步优化------直接构造要返回的临时对象,`str`本质是该临时对象的引用,底层通过指针实现。从运行结果的角度,可观察到`str`的析构在赋值之后,这说明`str`是临时对象的别名。

右值对象赋值(同时存在拷贝构造、拷贝赋值与移动构造、移动赋值的场景)

  • 图5左边展示了VS2019 debug环境和`g++ test.cpp -fno-elide-constructors`关闭优化环境下编译器的处理逻辑:会执行一次移动构造和一次移动赋值。

  • 需注意:在VS2019的release模式,以及VS2022的debug和release模式下,以下代码会进一步优化------直接构造要返回的临时对象,`str`本质是该临时对象的引用,底层通过指针实现。从运行结果的角度,可观察到`str`的析构在赋值之后,这说明`str`是临时对象的别名。

  1. :在 VS2019 release、g++ 9 release 等较新编译器环境下,传值返回可实现 "无拷贝" 优化。
  2. 对右值引用 / 移动语义意义的澄清:
    • 这种 "无拷贝" 优化不代表右值引用和移动语义失去意义。
    • 原因一:"无拷贝优化" 并非 C++ 标准规定的强制行为,仅部分较新编译器支持。
    • 原因二:在其他场景做不到这样的优化(不拷贝)
(4)右值引用和移动语义在传参中的提效
  • 查看 STL 文档可知,C++11 之后,容器的 `push` 和 `insert` 系列接口均新增了右值引用版本,以适配移动语义提升性能。

  • 当调用接口时传入左值实参,容器内部会继续调用拷贝构造函数,将左值对象的资源拷贝到容器空间的对象中,原左值对象资源保持独立。

  • 当传入右值实参(如临时对象、`std::move` 转换后的对象),容器内部会调用移动构造函数,直接"窃取"右值对象的资源转移到容器对象上,避免拷贝开销。

  • 需将之前模拟实现的 `bit::list` 拷贝过来,为其新增支持右值引用参数的 `push_back` 和 `insert` 接口,适配移动语义。

  • 此外,STL 容器还提供了 `emplace` 系列接口,该接口依赖可变参数模板实现,需在讲解可变参数模板后,再详细介绍 `emplace` 系列接口的原理与使用。

上图解释:

C++ 标准 函数原型 适用场景 构造行为
C++98 void push_back(const value_type& val) 接收左值(如已存在的变量s1 调用拷贝构造,深拷贝资源
C++11 void push_back(value_type&& val) 接收右值(如临时对象、move后的对象) 调用移动构造,窃取资源

代码中不同push_back调用的行为分析

调用方式 参数类型 触发的构造函数 效率与资源行为
lt.push_back(s1); 左值 拷贝构造 深拷贝s1的资源,原对象s1资源保持独立
lt.push_back(bit::string("22222...")); 右值(临时对象) 移动构造 窃取临时对象的资源,临时对象析构时无负担
lt.push_back("33333..."); 右值(字面量隐式转换为临时对象) 移动构造 先隐式构造临时bit::string对象,再窃取其资源
lt.push_back(move(s1)); 右值(move转换后的左值) 移动构造 窃取s1的资源,s1变为空对象(后续析构无开销)

这种重载设计让std::list在 C++11 及以后能根据参数是左值还是右值,自动选择 "拷贝" 或 "移动" 逻辑,既保证了兼容性,又实现了性能优化

上图解释:

要点 说明
右值引用的左值属性 右值被右值引用绑定后,该引用变量的表达式属性会变为左值(即可以取地址、能作为左值使用)
设计原因 为了让右值对象的资源能被 "安全转移":只有将右值引用视为左值,才能在移动构造 / 移动赋值中通过swap等操作,把右值对象的资源(如内存、数据)转移到新对象中,同时让原右值对象变为 "空对象"(析构时无负担)

右值是右值属性,右值引用属性是左值,方便完成交换

右值引用引用了一个右值,右值就被修改指向空了,右值的资源就被转移(深拷贝才有意义)

移动构造的特点就是移动右值的资源,语法设计层右值引用的属性本身是左值,要move一下强转成右值(只要往下传参,每一层都要move)

相关推荐
学困昇1 小时前
C++11中的包装器
开发语言·数据结构·c++·c++11
讨厌下雨的天空1 小时前
Linux信号量
java·开发语言
爱吃烤鸡翅的酸菜鱼2 小时前
Spring Boot 实现 WebSocket 实时通信:从原理到生产级实战
java·开发语言·spring boot·后端·websocket·spring
雪域迷影2 小时前
C++中编写UT单元测试用例时如何mock非虚函数?
开发语言·c++·测试用例·gmock·cpp-stub开源项目
AI街潜水的八角3 小时前
Python电脑屏幕&摄像头录制软件(提供源代码)
开发语言·python
hadage2333 小时前
--- git 的一些使用 ---
开发语言·git·python
sheeta19984 小时前
LeetCode 每日一题笔记 日期:2025.11.24 题目:1018. 可被5整除的二进制前缀
笔记·算法·leetcode
lly2024065 小时前
HTML与CSS:构建网页的基石
开发语言