当前位置: 首页 > news >正文

【C++11】右值引用 + 移动语义 + 完美转发(重点)

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 前言:STL中一些变化
      • • 新容器
      • • 容器中的一些新方法
  • 一、如何判断左值和右值
  • 二、左值引用和右值引用
  • 三、左值引用 vs 右值引用
  • 四、move函数
  • 五、 右值引用使用场景
      • 5.1 场景一:移动语义
      • 5.2 场景二:STL容器插入接口函数
      • 5.3 场景三:完美转发
        • 5.3.1 万能引用
        • 5.3.2 完美转发保持值的属性
  • 六、相关代码

前言:STL中一些变化

• 新容器

C++11中新增了以下几个容器(用橘色圈起来):

在这里插入图片描述

实际上最有用的是哈希系列unordered_mapunordered_set

剩下的容器arrayforward_list非常鸡肋,实际上很少使用。

  • array容器

文档介绍:点击跳转

在这里插入图片描述

C++11标准中,引入了一个容器array,它的底层使用了非类型模板参数,是一个真正意义上的泛型数组(定长数组),这个是用来对标C语言传统数组的。

以下是array容器的基本用法:

在这里插入图片描述

看完以上接口,array支持的,数组也都是支持的。那么它们有什么区别呢?

  • 相同点:array也并没有进行初始化。

在这里插入图片描述

  • 要说有区别的话:array对于越界读、写检查更为严格;传统数组越界读写,不会发生报错

在这里插入图片描述

【吐槽】虽然对越界行为检查严格 ,但在实际开发中,很少使用array容器,因为它对标传统数组,连初始化都没有,并且大小也是固定的,因此不够灵活。

相比之下,vector也是类似于数组的容器,它允许动态改变大小、对于越界读和写检查也一样严格。因此,在功能和实用性上可以全面碾压array,因此可以说array是一个鸡肋的容器。

  • forward_list容器

文档介绍:点击跳转

在这里插入图片描述

以下是forward_list的常见接口:

在这里插入图片描述

forward_list翻译过来就是单链表,因此一个结点只存值和指向下一个结点的指针。算了,直接开始(吐槽)吧。首先先说结论:forward_list还是一个非常鸡肋的容器。

  • 从以上接口可以看出,它只支持头删pop_front和头插push_front。为什么不支持尾删和尾插呢?因为它效率低啊!尾插需要找到尾结点、尾删需要找到尾节点的前一个结点,这些操作都要从头部开始向后遍历,时间复杂度铁铁的O(N)
  • 另外,forward_list还不提供size()接口

如果要说forward_list有优势,那就是内存占用更小(每个结点节省了一个前驱指针),但是它还是比较鸡肋,因此在实际中使用list会更多一点。

• 容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。

比如cbegincend

在这里插入图片描述

这玩意其实也很鸡肋,因为普通版的beginend都已经重载了const版本。对于C++开发人员还是区分得开的。

有坏当然也有好的方面,比如:

  1. 所有容器均重载了initializer_list<T>,使容器初始化更加方便。
  2. 所有容器均支持移动构造和移动赋值,可以极大提高效率(本篇重点)

在这里插入图片描述

  1. 所有容器均支持右值引用相关插入接口,同样可以提高效率(本篇重点)

在这里插入图片描述


一、如何判断左值和右值

要想搞懂左值引用和右值引用,首先要得明白什么是左值和右值。很多人认为在赋值符号左边的就是左值,在赋值符号右边的就是右值。 这其实并不完全正确,比如:

int main()
{int a = 1; // a是左值int b = a; // a又变成右值?
}

所以,以上变量a到底是左值还是右值?(答案是:左值)

  • 交给大家简单粗暴的判断左值的方法:可以取地址就是左值

举个例子,以下是常见的左值:

在这里插入图片描述


  • 如何判断右值?

右值通常被认为是临时的、一次性的值或者是将亡值右值可以出现在赋值符号的右边,但是绝不能出现出现在赋值符号的左边。就这么说吧,只要 不能取地址的就是右值。常见的右值有:字面常量、表达式返回值,函数返回值(临时对象返回)等。

在这里插入图片描述

二、左值引用和右值引用

大家只要记住一句话,不管是什么引用,都是取别名左值引用就是对左值取别名,右值引用就是对右值取别名

  • 首先先来看看左值引用的例子
int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}

由上可以看出,左值引用就是C++刚入门学的那个引用,唯一有区别的还是右值引用。

  • 用两个&&表示右值引用。
double Min(double x, double y)
{return x > y ? y : x;
}int main()
{// 以下几个都是常见的右值10;1.1 + 2.2;Min(1.1, 2.2);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = 1 + 2;double&& rr3 = Min(1, 2);return 0;
}

三、左值引用 vs 右值引用

  • 问题1:左值引用能否给右值取别名?

在这里插入图片描述

正常来说是不行的,但由于 右值都具有常属性,因此用const引用即可

在这里插入图片描述

  • 问题2:右值引用能否引用左值?

在这里插入图片描述

编译器已经给出很明确的报错信息了:无法将右值引用绑定到左值

但右值引用可以引用move以后的左值。点击跳转

既然左值引用即可以引用左值,也能引用右值。那么以下情景是否构造函数重载是否存在调用歧义呢

在这里插入图片描述

答案是当然构成重载,编译器会选择最匹配的参数调用

在这里插入图片描述

四、move函数

  • 虽然右值引用不能直接引用左值,但是可以通过调用一个名为move函数来获得绑定到左值上的右值引用

  • move的主要作用是显式地标识对象为右值,以便编译器能够选择调用移动语义相关的操作,而不是进行拷贝操作

int a = 0;
int&& rr = move(a);

可以这么理解:当右值引用 引用右值时,则是先将引用的对象的临时资源 “转移” 到特定位置(不会发生拷贝),然后指向该位置中的资源,此时可以对右值进行修改操作。

在这里插入图片描述

另外,虽然右值引用引用的是右值,但右值引用本身是可以取地址的

在这里插入图片描述

除此之外,语法还支持给右值引用加const,这样做的含义是 不能修改右值引用后的值

在这里插入图片描述

但我们一般建议不要用const右值引用,因为使用右值引用就是为了“转移”资源,加了const还不如直接改用const左值引用。


注意:不要轻易使用move函数,左值中的资源可能会被转走。如果此时我们直接将左值move后构造一个新对象,会导致原本左值中的资源丢失

在这里插入图片描述

五、 右值引用使用场景

5.1 场景一:移动语义

前面我们可以看到左值引用既可以引用左值也可以通过const引用引用右值,那为什么C++11还要提出右值引用呢?

既然提出了就一定是为了解决左值引用存在的缺陷,那么我们可以通过分析左值引用的使用场景及核心价值来推断。

【左值引用】

  • 使用场景:1. 做输出型参数(形参的改变影响实参)。 2. 做返回值。
  • 核心价值:减少拷贝,提高效率。

我们知道,左值引用做返回值是有一定的缺陷的!如果是左值引用做返回值,出了作用域,对象不能被销毁;如果出了作用域,对象被销毁,那么就不能使用左值引用做返回值
(不知道为什么可以看看这篇博客:点击跳转)

string func()
{string str("hello world");return str;
}int main()
{string s = func();cout << s << endl;return 0;
}

str是局部对象,出了func函数作用域,对象销毁,那么就不能用左值引用返回,那么按照惯例只能使用传值返回。而 传值返回是有代价的,对于较大的对象(如大型结构体、类对象等),可能会导致较大的性能开销,因为它需要在内存中复制整个对象的内容

接下来可以简单的来验证一下,下面是简单模拟实现的string类,为了更好的观察是否发生了深拷贝行为,在 拷贝构造函数中加入了对应的打印语句。

  • string.h
#pragma once
#include <iostream>
#include <assert.h>
namespace wj
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){swap(_str, s._str);swap(_size, s._size);swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}
  • Test.cpp

在这里插入图片描述

以上代码本来应该是两次拷贝构造,但对于一行的两次拷贝构造,新一点的编译器会优化成一次拷贝构造。虽然优化成一次拷贝构造,但是还是需要拷贝整个对象的内容,但是有很大的消耗。

因此,C++11中允许使用移动语义之移动构造解决上述问题:在wj::string中增加移动构造,移动构造本质是窃取别人的资源来构造自己,占位已有,那么就不用做深拷贝了(提高性能),所以它叫做移动构造。

  • 移动构造
string(string&& s):_str(nullptr)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}

接下来再来看看结果:

在这里插入图片描述

那么问题来了,对象str是一个左值啊,它是怎么调用移动构造函数的?

这其实是编译器的优化!当Myfunc函数返回一个wj::string对象时,编译器会在其内部将str视为右值,并使用移动构造函数来将其内容转移到 s 中。这样做可以避免不必要的拷贝操作,提高了程序的性能


接下来再来看,如果两次的拷贝构造不再同一行,编译器就不能实现优化,那么就是实打实的两次拷贝构造,那么这个消耗是巨大的。(在此之前我屏蔽了移动构造)

在这里插入图片描述

第三次打印的结果是无法避免的,因为在调用operator=会重新开辟空间来深拷贝对象的资源。

在这里插入图片描述

因此,C++11同时也引入了移动语义之移动赋值,用于在对象之间实现资源的高效转移。移动赋值运算符允许将一个对象的资源从另一个对象转移到自身,而不是通过拷贝构造或拷贝赋值运算符来进行资源的复制。

  • 移动赋值
// 赋值重载
string& operator=(string&& s)
{cout << "string& operator=(string& s) -- 移动赋值" << endl;swap(s);return *this;
}

在这里插入图片描述

接下来再将移动拷贝的代码取消注释,然后再来看看打印结果

在这里插入图片描述

综上,移动语义(移动构造 + 移动赋值)弥补了自定义类型中深拷贝的类,必须传值返回的场景。避免不必要的资源复制,提高了程序的性能和效率

5.2 场景二:STL容器插入接口函数

C++11标准出来之后,STL中的容器除了增加移动构造和移动赋值之外,STL容器插入接口函数也增加了右值引用版本。

在这里插入图片描述

那么右值引用版本插入函数的意义是什么呢?

如果list容器当中存储的是string对象,那么在调用push_backlist容器中插入元素时,可能会有如下几种插入方式:

在这里插入图片描述

  • 对于第一个一定会完成深拷贝,因为s对象是左值,那么lt对象在调用push_back一定会选择最合适的,也就是void push_back (const value_type& val);

  • 剩下的一定会调用void push_back (value_type&& val);。字符串字面量(如 "11111111111111")时,它会调用右值引用版本的 push_back。这是因为字符串字面量是临时对象,是右值,而 push_back 函数的右值引用版本接受右值参数(提高效率)。

5.3 场景三:完美转发

5.3.1 万能引用

在模板中的&&不代表右值引用,而是万能引用,其既能接收任意类型的左值和右值。

  • 如果传入的实参是左值,那么编译器就会将模板实例化为左值引用,也称做引用折叠。
  • 如果传入的实参是右值,那么编译器就会将模板实例化为右值引用。

【基本语法】

template<class T>
void PerfectForward(T&& t)
{//...
}

下面重载了四个Func函数,参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值,在PerfectForward函数中再调用Func函数。

我们来判断是否真的很“万能”

void Fun(int& x) 
{ cout << "左值引用" << endl; 
}void Fun(const int& x) 
{ cout << "const 左值引用" << endl; 
}void Fun(int&& x) 
{ cout << "右值引用" << endl; 
}
void Fun(const int&& x) 
{ cout << "const 右值引用" << endl; 
}template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

【程序结果】

在这里插入图片描述

输出的结果好像和我们一开始说的不太一样,最终都匹配到了左值引用版本的Func函数。接下来可以分析为什么?

首先先看第一次调用PerfectForward(10),由于PerfectForward函数的参数类型是万能引用,因此在编译器眼中其实是如下这样的:

// 实参10是int类型,那么对应的T应该实例化为int
// 并且实参10是右值,编译器就会将模板实例化为右值引用
template<typename int>
void PerfectForward(int&& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值return 0;
}

这下好像有点眉目了,实参(右值)10传递给形参t,然后再通过t去调用Func函数,而t虽然引用右值,但是它本身是可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。

也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。

5.3.2 完美转发保持值的属性

要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数。基本语法如下:

template<class T>
void PerfectForward(T&& t)
{Func(forward<T>(t));
}

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值。

在这里插入图片描述

六、相关代码

Gitee仓库链接:点击跳转

相关文章:

  • Vue - 你知道Vue组件之间是如何进行数据传递的吗
  • css伪类:last-child或:first-child不生效
  • 【数据库】MySQL数据库学习涵盖的多个方面
  • vue项目安装下载项目包,报错clear up some disk space and try again
  • python学习25:python中的元组(tuple)
  • 【C语言】——指针八:指针运算笔试题解析
  • 明日周刊-第5期
  • Unity框架,ET框架8.1版本的打包流程记录
  • Linux——gdb
  • 计算机毕业设计选题之基于SSM的旅游管理系统【源码+PPT+文档+包运行成功+部署讲解】
  • Nginx: proxy_set_header 与 add_header 区别
  • 卫星遥感监测森林植被健康度
  • 影院座位选择简易实现(uniapp)
  • 【Qt】:常用控件(一:概述和QWidget核心属性)
  • 容器和K8s常见概念
  • 【347天】每日项目总结系列085(2018.01.18)
  • 【Amaple教程】5. 插件
  • Java到底能干嘛?
  • MD5加密原理解析及OC版原理实现
  • React-生命周期杂记
  • SpriteKit 技巧之添加背景图片
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • 彻底搞懂浏览器Event-loop
  • 分享几个不错的工具
  • 浮现式设计
  • 我建了一个叫Hello World的项目
  • 云大使推广中的常见热门问题
  • 走向全栈之MongoDB的使用
  • Spring第一个helloWorld
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • #QT项目实战(天气预报)
  • (Git) gitignore基础使用
  • (libusb) usb口自动刷新
  • (三)c52学习之旅-点亮LED灯
  • (算法)Game
  • .gitignore
  • .java 指数平滑_转载:二次指数平滑法求预测值的Java代码
  • .NET Standard 支持的 .NET Framework 和 .NET Core
  • .NetCore Flurl.Http 升级到4.0后 https 无法建立SSL连接
  • .vue文件怎么使用_我在项目中是这样配置Vue的
  • 。Net下Windows服务程序开发疑惑
  • /bin/rm: 参数列表过长"的解决办法
  • [Android Pro] Notification的使用
  • [Android] 240204批量生成联系人,短信,通话记录的APK
  • [C#C++]类CLASS
  • [ERROR] ocp-server-ce-py_script_start_check-4.2.1 RuntimeError: ‘tenant_name‘
  • [HUBUCTF 2022 新生赛]
  • [LeetCode] Wildcard Matching
  • [Linux](16)网络编程:网络概述,网络基本原理,套接字,UDP,TCP,并发服务器编程,守护(精灵)进程
  • [MongoDB]------windos下的安装部署与基础使用
  • [OpenAI]继ChatGPT后发布的Sora模型原理与体验通道
  • [python] os.path说明
  • [Silverlight 4 RC]RichTextBox概览
  • [VJ]输出m/n,若是循环体只输出第一节