完整教程:【C++:C++11】详解C++11右值引用与移动语义:从性能瓶颈到零拷贝优化

艾莉丝努力练剑:个人主页
❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶、测试开发要点全知道
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
艾莉丝的简介:

艾莉丝的C++专栏简介:

目录
2.3 C++11中的std:initializer_list(初始化列表)
3.4.2 传值返回已经没有拷贝了,是否意味上面的右值引用和移动语义就没意义?
3.4.5 以上两种情况结合局部对象生命周期和栈帧的角度的理解
3.4.6 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
3.4.7 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
C++学习阶段的三个参考文档
看库文件(非官方文档):cplusplus
这个文档在C++98、C++11时候还行,之后就完全没法用了……
还可以看语法——准官方文档(同步更新):C++准官方参考文档
这个行,包括C++26都同步了,我们以后会看这个。
有大佬——官方文档(类似论坛):Standard C++
这个网站上面会有很多大佬。
总结一下就是——


前情提示
1 C++学习的三个参考文档

2 {}初始化

3 C++11中的{}

4 引用

5 fmin

6 左值引用和右值引用


1 ~> C++11的历史发展
1.1 历史发展
既然谈到C++11的历史发展,那不得不再次献出这种经典老图——

C++11是C++的第二个主要版本,并且是自从C++98版本开始的最重要的更新。C++11引入了大量更改,标准化了既有的一些实践,并改进了对C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,人们曾使用名称“C++0x”——因为它曾被期待在2010年之前发布(结果一直拖到2011年)。C++03(没出什么新特征)与C++11期间花了8年时间!这是迄今为止最长的版本间隔——C++08烂尾了(五年计划),因为C++08的规划太大、特性太多——C++标准委员会想做的东西太多,结果来不及了,这导致委员会此后调整了发布策略:
从那时起,C++有规律地每3年更新一次——能出多少是多少。
1.2 拓展讨论
语言被公司使用是有一个过程的。
C++标准委员会:制定理论语法。
编译器:支持C++语法——可进行有原则性的支持(常用的肯定会支持,否则编译器不好用就会被淘汰的)
举个例子,C语言的C99标准更新了变长数组(用变量),VS就不支持(编译器不支持的体现)。
包括像C++23,大多数编译器还不完全支持——

上面这张图,uu们可以了解一下:编译器对几个版本的C++的支持情况。
语言的新的语法标准被大规模使用的缓冲期:5~10年。
(1)大多数公司就是使用到C++11,当然也有40%~50%的公司在使用C++14(小版本)、C++17(中版本,不少公司用)。
(2)C++23(公司使用偏少)是个大版本:特性还不够成熟(上层的库还不够完善),学的不错的程序员不多;C++23就更少了。

VS编译器:MSVC(微软);苹果编译器:Clang(支持苹果的ObjectC,也支持C / C++)。
看编译器是否支持语法特性。
2 ~> 列表初始化:{}
2.1 C++98中的{}
C++98中一般数组和结构体可以用0进行初始化。


2.2 C++11中的{}
C++11以后,想统一初始化方式:试图实现一切对象皆可用{}初始化,{}初始化也叫列表初始化;
内置类型和自定义类型都支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造;

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

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



2.3 C++11中的std:initializer_list(初始化列表)
上面的{}初始化已经很方便了,但是对于对象容器初始化还是不太方便,比如一个vector对象,如果我们想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持——
vectorv1 = {1 , 2 , 3};
vectorv2 = {1 , 2 , 3 , 4 , 5};
C++11库中提出了一个std:initializer_list的类——
autoil={10,20,30};//the typeofilisaninitializer_list
std:initializer_list这个类的本质是:底层开一个数组,将数据拷贝过来,std:initializer_list内部有两个指针分别指向数组的开始和结束。
文档:initializer_list,std:initializer_list支持迭代器遍历。

容器支持一个std:initializer_list的构造函数,也就支持任意多个值构成的{x1 , x2 , x3...}进行初始化。STL中的容器支持任意多个值构成的{x1 , x2 , x3 , ……}进行初始化,就是std::initializer_list的构造函数支持的。如下图所示——


3 ~> 右值引用 && 移动语义
C++98的C++语法中就有引用的语法,我们前面介绍的就是啦,而C++11中新增了的右值引|用语法特性,C++11之后,我们之前学习的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名(给对象取别名,不开空间)。
3.1 左值和右值

左值,可以取地址——左值是一个表示数据的表达式(如变量名或解引用的指针),一般有持久存在,存储在内存中,可以获取它的地址(区别),左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值。
右值,不能取地址(区别)——右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象右值可以出现在赋值符号的右边,但是一般不能出现出现在赋值符号的左边。

这里是会编译报错的,根据提示,注释掉后面的cout部分就能通过了——

3.2 左值引用和右值引用
3.2.1 概念
Type& r1 = x;
Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名;第二个语句就是右值引用,同样的道理,右值引用就是给右值取别名——这个左值和右值就是上面说的能不能取到地址的区别。
左值引用不能直接引用右值,但是const左值引用可以引用右值;
右值引用不能直接引用左值,但是右值引用可以引用move(左值)。

template typename remove_reference::type&& move (T&& arg);
move(强转)是库里面的一个函数模板,本质内部是进行强制类型转换,当然这里还涉及一些引用折叠的知识,这个我们后面会详细介绍的,现在先了解一下。
值得注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变
量表达式的属性是左值。
语法层面看,左值引用和右值引用都是取别名,不开空间。
从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要放到一起去理解,很容易搞混!uu们在学习的时候有时候喜欢互相佐证,这样反而是陷入迷途。
3.2.2 左值引用、右值引用可以相互交叉
左值引用、右值引用可以“相互交叉”,如下图所示——

3.2.3 引用延迟生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。如下图,艾莉丝演示了右值引用延长匿名对象的生命周期——


3.2.4 左值和右值的参数匹配问题
C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会
匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。
右值引用变量在用于表达式时属性是左值(天才的设计!),这个设计这里会感觉有点怪,等我们介绍右值引用的使用场景时,就能体会这样设计的价值了,这里先买个关子。


3.3 右值引用和移动语义的使用场景
3.3.1 左值引用主要使用场景
左值引用主要使用场景:函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回:如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。C++11以后这里可以使用右值引用作为返回值来解决吗?显然这是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。
3.3.2 传值返回需要拷贝

3.3.3 移动构造和移动赋值
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像string / vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有
意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的jqj:string样例实现了移动构造和移动赋值,我们需要结合场景理解。

3.3.4 左值拷贝和右值拷贝:拷贝构造和移动构造

3.4 右值引用和移动语义解决传值返回问题
3.4.1 两种场景实践



3.4.2 传值返回已经没有拷贝了,是否意味上面的右值引用和移动语义就没意义?

3.4.3 右值对象构造,只有拷贝构造,没有移动构造的场景
如下图展示了vs2019的debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码优化为非常恐怖——会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
linux下可以将下面代码拷贝到test.cpp文件,编译时用的方式关闭构造优化——
g++ test.cpp-fno-elide-constructors
运行结果可以看到下图中左边没有优化的两次拷贝。

3.4.4 右值对象构造,既有拷贝构造,也有移动构造的场景
如下图展示了vs2019的debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码优化为非常恐怖——会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
linux下可以将下面代码拷贝到test.cpp文件,编译时用的方式关闭移动优化——
g++ test.cpp-fno-elide-constructors
运行结果可以看到下图中左边没有优化的两次移动——

3.4.5 以上两种情况结合局部对象生命周期和栈帧的角度的理解

3.4.6 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
下图的左边展示了vs2019的debug版本以及——
g++test.cpp-fno-elide-constructors
关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

3.4.7 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
下图的左边展示了vs2019的debug版本以及——
g++test.cpp-fno-elide-constructors
关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

3.5 右值引用和移动语义在传参中的提效
查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本;
当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象;
当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上;
把我们之前模拟实现的bit:list拷贝过来,支持右值引用参数版本的push_back和insert;
其实这里还有一个emplace系列的接口(终于来啦),但是这个涉及可变参数模板,我们需要把可变参数模板介绍完以后再介绍emplace系列的接口——涉及引用折叠等内容——


完整代码示例与实践演示
list.h:
#pragma once
namespace jqj
{
// --------------链表节点结构--------------
template
struct list_node
{
list_node* _next; // 指向下一个节点的指针
list_node* _prev; // 指向前一个节点的指针
T _data; // 节点存储的数据
// --------------节点构造函数--------------
// 左值
list_node(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x) // x 节点数据,默认为T类型的默认值
{}
// 右值
list_node(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(move(x)) // x 节点数据,默认为T类型的默认值
{}
};
// --------------链表迭代器--------------
// 实现双向迭代器功能,支持前向和后向遍历
template // T 数据类型
// Ref 引用类型(T& 或 const T&)
struct list_iterator
{
// using还具有tepedef没有的功能
// 使用类型别名(C++11新特性)
using Self = list_iterator; // 自身类型
using Node = list_node; // 节点类型
Node* _node; // 当前指向的节点指针
// 迭代器构造函数
list_iterator(Node* node)
:_node(node)
{}
// 迭代器解引用操作
// *it = 1
// Ref 返回节点数据的引用(可读或可写)
Ref operator*() // 解引用,Ref就是reference,引用的意思
{
return _node->_data;
}
// operator*()返回对应数据类型的引用
Ptr operator->() // 返回对应数据类型的指针
{
return &_node->_data;
}
// ++it
// 前向迭代操作
Self& operator++() // Self& 返回递增后的迭代器引用
{
_node = _node->_next;
return *this;
}
Self operator++(int) // Self 返回递增前的迭代器副本
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// --it
// 后向迭代操作
Self& operator--() // Self& 返回递减后的迭代器引用
{
_node = _node->_prev;
return *this;
}
Self operator--(int) // Self 返回递减前的迭代器副本
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
// 迭代器比较操作
bool operator!=(const Self& s) const // bool 两个迭代器是否不指向同一节点
{
return _node != s._node;
}
bool operator==(const Self& s) const // bool 两个迭代器是否不指向同一节点
{
return _node == s._node;
}
};
//template
//struct list_const_literator
//{
// using Self = list_const_literator;
// using Node = list_node; Node* _node;
// Node* _node;
// list_const_iterator(Node* node)
// :_node(node)
// { }
// // *it
// const T& operator*()
// {
// return _node->_data;;
// }
// // ++it
// Self& operator++()
// {
// _node = _node->_next;
// return *this;
// }
// Self operator++(int)
// {
// Self tmp(*this);
// _node = _node->_next;
// return *this;
// }
// // --it
// Self& operator--()
// {
// _node = _node->_prev;
// return *this;
// }
// Self operator--(int)
// {
// Self tmp(*this);
// _node = _node->_prev;
// return tmp;
// }
// bool operator!=(const Self& s) const
// {
// return _node != s._node;
// }
// bool operator==(const Self& s) const
// {
// return _node == s._node;
// }
//};
// --------------链表主体类--------------
template
class list
{
using Node = list_node; // 节点类型别名
public:
// 迭代器类型定义
using iterator = list_iterator; // 普通迭代器
using const_iterator = list_iterator; // 常量迭代器
// const T* 只能读数据,不能修改数据
//using iterator = list_iterator;
//using const_iterator = list_const_iterator;
// --------------迭代器访问接口--------------
// 获取指向第一个元素的迭代器
// iterator 指向首元素的迭代器
iterator begin()
{
return iterator(_head->_next);
}
// iterator 指向哨兵节点的迭代器
iterator end()
{
return iterator(_head);
}
// 获取指向第一个元素的常量迭代器
// const_iterator 指向首元素的常量迭代器
const_iterator begin() const
{
return const_iterator(_head->_next);
}
// const_iterator 指向哨兵节点的常量迭代器
const_iterator end() const
{
return const_iterator(_head);
}
// ----------------链表初始化相关-----------------
void empty_init() // 初始化空链表(创建哨兵节点)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
// 默认构造函数
list()
{
empty_init();
}
// 初始化列表构造函数
// il 初始化列表
list(initializer_list il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
// 范围构造函数
// InputIterator 输入迭代器类型
template
list(InputIterator first, InputIterator last) // first 范围起始迭代器 // last 范围结束迭代器
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
// 数量构造函数(size_t版本)
list(size_t n, T val = T()) // val 元素值,默认为T的默认值
{
empty_init();
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
// 数量构造函数(int版本)
list(int n, T val = T()) // val 元素值,默认为T的默认值
{
empty_init();
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
// -------------析构函数-------------
// 清理所有节点并释放哨兵节点
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
// -----------拷贝控制函数----------
// lt 要拷贝的源链表
// lt2(lt1)
list(const list& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
// 拷贝赋值运算符
// lt 要拷贝的源链表
// list& 返回当前链表的引用
// lt1 = lt3
list& operator=(const list& lt)
{
if (this != <)
{
clear();
for (auto& e : lt)
{
push_back(e);
}
}
return *this;
}
////
//list(list& lt)
// list(const list& lt)
//{
// empty_init();
// list tmp(lt.begin(), lt.end());
// swap(tmp);
//}
//// lt1 = lt3
////list& operator=(list tmp)
//list& operator=(list tmp)
//{
// swap(tmp);
//}
// ----------------容量操作---------------
// 交换两个链表的内容
void swap(list& lt) // lt 要交换的另一个链表
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// 清空链表中的所有元素
// 保留哨兵节点,删除所有数据节点
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
//void push_back(const T& x)
//{
// Node* newnode = new Node(x);
// Node* tail = _head->_prev;
// tail->_next = newnode;
// newnode->_prev = tail;
// newnode->_next = _head;
// _head->_prev = newnode;
//}
// -----------------元素访问操作--------------------
// 复用
void push_back(const T& x) // 在链表尾部插入元素,x:要插入的元素值
{
insert(end(), x);
}
// 右值:尾插
void push_back(T&& x)
{
insert(end(), x);
}
// 在链表头部插入元素,x:要插入的元素值
void push_front(const T& x)
{
insert(begin(), x);
}
// // 右值
//void push_front(T&& x)
//{
// insert(begin(), x);
//}
// 删除链表尾部元素
void pop_back()
{
erase(--end());
}
// 删除链表头部元素
void pop_front()
{
erase(begin());
}
// ---------------- 指定位置操作 ----------------
// 在指定位置前插入元素
void insert(iterator pos, const T& x) // pos:插入位置的迭代器,x:要插入的元素值
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// // 连接新节点:prev -> newnode -> cur
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
// 删除指定位置的元素
iterator erase(iterator pos) // iterator 返回指向被删除元素后一个元素的迭代器
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
// 跳过被删除节点:prev -> next
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
/*return iterator(next);*/
return next; /// 返回下一个节点的迭代器
//两种写法都可以
}
// -------------- 容量信息 ------------------
size_t size() const
{
//size_t n = 0;
//for (auch e : *this)
//{
// ++n;
//}
//return n;
return _size;
}
private:
Node* _head; // 哨兵头节点指针
size_t _size = 0; // 链表元素个数计数器
};
}
Test.cpp:
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
最终运行结果

结尾
uu们,本文的内容到这里就全部结束了,艾莉丝再次感谢您的阅读!
往期回顾:
【C++:哈希表封装】用哈希表封装unordered_map和unordered_set
结语:既然都看到这里啦!请大佬不要忘记给博主来个“一键四连”哦!
博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!
૮₍ ˶ ˊ ᴥ ˋ˶₎ა





浙公网安备 33010602011771号