【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仓库链接:点击跳转

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/798148.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第四百四十六回

文章目录 1. 概念介绍2. 使用方法3. 示例代码4. 经验与总结4.1 经验分享4.2 内容总结 我们在上一章回中介绍了"overlay_tooltip简介"相关的内容&#xff0c;本章回中将再谈flutter_launcher_icons包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

python练习三

模式A num int(input("请输入模式A的层数&#xff1a;")) for i in range(1, num 1):# 画数字for j in range(1, i 1):print(str(j) "\t", end"")print() 模式B num int(input("请输入模式B的层数&#xff1a;")) for i in ran…

Pandas中的 .map 方法

1. Pandas中的 .map 方法 在Pandas中&#xff0c;.map 方法通常用于Series对象&#xff0c;它允许你根据一个字典或者函数对Series中的每个元素进行转换。 import pandas as pd # 创建一个简单的DataFrame df pd.DataFrame({ Name: [Alice, Bob, Charlie, Alice, Bob, C…

【stm32】SPI通信简介

SPI通信 SPI简介部分 所有SPI设备的SCK、MOSI、MISO分别连在一起 从主机引出多根SS选择线&#xff0c;分别接到每个从机的SS输入端&#xff0c;主机的SS线都是输出&#xff0c;从机的SS线都是输入&#xff0c;SS线 是低电平有效&#xff0c;同一时间主机只能选择一个从机 只能…

cmake学习笔记1

基础概念 CMake是什么&#xff1f; CMake是一个元构建系统(meta build-system),用于生产其他构建系统文件&#xff08;如Makefile或Ninja&#xff09;。 基础操作方式 CMake使用一个CMakeLists.txt文件描述配置&#xff0c;然后使用cmake驱动这个文件生成对应构建系统文件。…

websokcet服务端实现

一/websokcet服务端实现 步骤一&#xff1a; springboot底层帮我们自动配置了websokcet&#xff0c;引入maven依赖 1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</arti…

AI图片智能选区抠像解决方案

高质量的图片处理往往依赖于繁琐的手动操作&#xff0c;耗费大量时间与精力。美摄科技推出了一款革命性的AI图片智能选区抠像解决方案&#xff0c;旨在帮助企业轻松实现图片的高效处理&#xff0c;提升内容创作效率与质量。 美摄科技的AI图片智能选区抠像解决方案&#xff0c;…

AFCI 应用笔记二之数据采集

1. 简介 基于监督学习的神经网络算法需要大量数据作为输入&#xff0c;模型完全由数据驱动&#xff0c;其数据质量是算法有效的必要条件&#xff0c;所以如何高效的采集到数据&#xff0c;以及正确的标注或分析是极其重要的&#xff0c;如果第一步有问题&#xff0c;后续的所有…

C++搭建深度学习的推理框架

我们的目的是:借助C++搭建一个类似于pytorch,tensorflow的深度学习框架,对标pytorch,tensorflow实现对应的功能。由于本人能力有限,下面本人将借助C++搭建一个简单的全连接神经网络,并且尝试解释里面的算子定义和计算图构建。 算子定义 回顾pytorch里面搭建的全连接神经网…

ESP32S3网络编程学习笔记(1)—— Wi-Fi扫描实验

前言 &#xff08;1&#xff09;如果有嵌入式企业需要招聘湖南区域日常实习生&#xff0c;任何区域的暑假Linux驱动/单片机/RTOS的实习岗位&#xff0c;可C站直接私聊&#xff0c;或者邮件&#xff1a;zhangyixu02gmail.com&#xff0c;此消息至2025年1月1日前均有效 &#xff…

基于DPDK的VPP 插件demo代码

VPP的插件编写&#xff0c; 首先要把VPP 工程下载下来&#xff0c; 编译通过。 然后按照example程序的套中来编写插件。 还有一个前提&#xff0c; 就是测试机上已经具备了DPDK 已经可用版本。 1. 下载VPP。 可以从github上下载VPP的指定版本的zip包&#xff0c; 也可以用…

2024年租用阿里云服务器多少钱一年?连夜整理分享

阿里云服务器租用价格表2024年最新&#xff0c;云服务器ECS经济型e实例2核2G、3M固定带宽99元一年&#xff0c;轻量应用服务器2核2G3M带宽轻量服务器一年61元&#xff0c;ECS u1服务器2核4G5M固定带宽199元一年&#xff0c;2核4G4M带宽轻量服务器一年165元12个月&#xff0c;2核…

软考高级架构师:嵌入式系统的内核架构

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

2024/4/1—力扣—二叉树的最近公共祖先

代码实现&#xff1a; 思路&#xff1a; 递归判断左子树和右子树&#xff0c;查找p或者q是否在当前节点的子树上 1&#xff0c;在同一子树上&#xff0c;同一左子树&#xff0c;返回第一个找到的相同值&#xff0c;同一右子树上&#xff0c;返回第一个找到的相同值 2&#xff0…

UML 绘制工具 starUML 入门介绍

拓展阅读 常见免费开源绘图工具 OmniGraffle 创建精确、美观图形的工具 UML-架构图入门介绍 starUML UML 绘制工具 starUML 入门介绍 PlantUML 是绘制 uml 的一个开源项目 UML 等常见图绘制工具 绘图工具 draw.io / diagrams.net 免费在线图表编辑器 绘图工具 excalidr…

工具推荐-针对Nacos利器-NacosExploitGUI_v4.0

Nacos是由阿里所开发的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 工具简介 集成Nacos的各种poc Nacos控制台默认口令漏洞(nacos,nacos)Nacostoken.secret.key默认配置(QVD-2023-6271)Nacos-clientYaml反序列化漏洞Nacos Jraft Hessian反序列化漏洞…

PET-SQL:基于大模型的两阶段Text2SQL方法

简介 PET-SQL出自论文《PET-SQL: A Prompt-enhanced Two-stage Text-to-SQL Framework with Cross-consistency》&#xff0c;将基于大模型的Text2SQL分为两个阶段进行&#xff0c;在第一阶段使用数据表schema信息、数据表采样数据、相似问答问答对生成初步的SQL(PreSQL)&…

【边缘智能】00_边缘计算发展背景

本系列是个人学习《边缘就算基础知识入门》的笔记&#xff0c;仅为个人学习记录&#xff0c;欢迎交流&#xff0c;感谢批评指正 移动物联设备产生海量数据&#xff0c;数据密集型移动智能应用&#xff0c;计算密集、动态性高&#xff0c;实时性强 传统云计算架构 基于广域互联…

matrix-breakout-2-morpheus 靶机渗透

信息收集&#xff1a; 1.nmap存活探测&#xff1a; nmap -sn -r 192.168.10.1/24 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-06 12:13 CST Nmap scan report for 192.168.10.1 Host is up (0.00056s latency). MAC Address: 00:50:56:C0:00:08 (VMware) Nmap…

鸿蒙内核源码分析 (双向链表篇) | 谁是内核最重要结构体

双向链表是什么&#xff1f; 谁是鸿蒙内核最重要的结构体 &#xff1f; 一定是: LOS_DL_LIST(双向链表)&#xff0c; 它长这样。 typedef struct LOS_DL_LIST {struct LOS_DL_LIST *pstPrev; /**< Current nodes pointer to the previous node | 前驱节点(左手)*/struct L…