【C++】—— C++11新特性之 “右值引用和移动语义”

前言:

  • 本期,我们将要的介绍有关 C++右值引用 的相关知识。对于本期知识内容,大家是必须要能够掌握的,在面试中是属于重点考察对象。

目录

(一)左值引用和右值引用

1、什么是左值?什么是左值引用?

2、什么是右值?什么是右值引用?

(二)左值引用与右值引用比较

(三)右值引用使用场景和意义

(四)完美转发 

1、概念

2、模板中的&& 万能引用

3、std::forward

总结


(一)左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

1、什么是左值?什么是左值引用?

C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++中的左值,无法对右值添加引用。举个例子:
 

int main()
{int num = 10;int& b = num; //正确int& c = 10; //错误return 0;
}

输出展示:

 【解释说明】

  • 如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。
     

那么到底什么是左值?什么是左值引用呢?

  1. 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边;
  2. 定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
     

注意:虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:
 

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;
}

2、什么是右值?什么是右值引用?
 

我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。


为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示:

  • 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等;
  • 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
  • 右值引用就是对右值的引用,给右值取别名。

int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);return 0;
}

输出展示:

 但是如果是下面这几个表达式,就会发生报错现象:

10 = 1;
x + y = 1;
fmin(x, y) = 1;

输出显示:

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址。例如下面代码所示:

int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;double&& rr2 = x + y;cout << rr1 << " " << rr2 << " " << endl;rr1 = 20;rr2 = 5.5;cout << rr1 << " " << rr2 << " " << endl;return 0;
}

输出展示:

 当我们不想被修改时,我们可以加上 【const】关键字:

【解释说明】

  1. 不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1;
  2. 如果不想rr1被修改,可以用const int&& rr1 去引用;
  3. 是不是感觉很神奇这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要
     

(二)左值引用与右值引用比较

左值引用总结:

  • 1. 左值引用只能引用左值,不能引用右值。
  • 2. 但是const左值引用既可引用左值,也可引用右值

int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra为a的别名return 0;
}

输出展示:

 又例如以下示例:

int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra2 = 10; // 编译失败,因为10是右值return 0;
}

输出展示:

 左值引用只能引用左值,不能引用右值。但是当我们加上 const 时,此时左值引用可以给右值取别名:

int main()
{int a = 10;// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}

输出展示:

【解释说明】

值得一提的是,虽然C++ 语法上是支持定义常量右值引用的,但这种定义出来的右值引用并无实际用处:

const int& ra3 = 10;
  1. 一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;
  2. 其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。
     


右值引用总结:

  • 1. 右值引用只能右值,不能引用左值。
  • 2. 但是右值引用可以move以后的左值。
     

代码展示:

 引用左值会发生报错行为:


通过 move 可以支持将左值转换为右值引用 

 在C++中,move是一个函数模板,可以将给定的对象转换为对应的右值引用。它并不执行实际的内存移动操作,而是将对象标记为可以进行移动操作的右值。这样,用户可以利用该标记来实现更高效的移动语义。


(三)右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

现有以下代码: 

 【解释说明】

首先,对于上述代码中的 res1 和 res2 ,它们分别为左值和右值;

紧接着大家想想,我们对左值和对右值拷贝有没有什么区别呢?

  • 如果是内置类型,他们其实区别不是很大,但是对于自定类型他们的区别可就很大了,
  • 因为自定义类型的右值,一般很多地方又把它叫做将亡值。通常都是一些表达式的返回值、一个函数调用等;
  • 而对于右值又分为 纯右值(一般来说是内置类型)和将亡值(一般来说是自定义类型)

对于上述的 res1,它作为一个左值,我们不能对其进行操作,只能去做深拷贝。因为虽然看起来这里是一个赋值,其实应该是拷贝构造;

而对于 res2 来说,它本身是右值,假如是自定义类型作为一个将亡值,我们就没有必要去对其进行拷贝操作。此时就引出了关于右值引用实现引动构造的概念。


例如现在有这样一个我们手写模拟的 string :

namespace zp
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)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;}string operator+(char ch){string tmp(*this);tmp += ch;return tmp;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}

当我们再这样的场景在来观察上述的 res1 和res2:

 我们可以发现此处的右值发生的是相应的深拷贝,这样显然是会造成不必要的浪费的。为了解决上述这样的问题,我们就可以引入 “移动构造” 这样的概念:

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

紧接着,再次运行上述代码,我们可以发现编译器会去自动识别:

此时,当我们就是想把 s1 转为右值可以怎么做呢?其实很简单(这里就体现了move):

 输出展示:

我们通过调试也可以发现此时确实达到了预期的效果:

 【小结】

通过上述我们可以发现左值引用的好处就是直接减少拷贝

左值引用的使用场景可以分为以下两个部分:

  • 做参数和做返回值都可以提高效率

左值引用的短板:

  • 但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。

例如现在有以下这样的代码: 

    zp::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}zp::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}

【说明】

  • zp::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

 紧接着,我们去打印看结果是什么:

 此时,传值返回带来的代价得到了极大的解决:

 右值引用和移动语义解决上述问题:

  • 在zp::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己;

再运行上面zp::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。


c++11 不仅仅有移动构造,还有移动赋值:

在zp::string类中增加移动赋值函数,再去调用zp::to_string(1234),不过这次是zp::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

输出展示:

 【解释说明】

  • 这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。zp::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为 zp::to_string 函数调用的返回值赋值给ret1,这里调用的移动赋值。

(四)完美转发 

1、概念

  • 完美转发(perfect forwarding)是C++11引入的一项特性,旨在实现在函数模板中对参数类型进行精确传递的能力;
  • 它主要用于保留传递到函数模板的实参的值类别,并将其转发到内部调用的函数,从而实现类型和值类别的完全保持;

举个例子:

template<typename T>
void PerfectForward(T t)
{Fun(t);
}

【解释说明】

  1. 如上所示,PerfectForward() 函数模板中调用了 Func() 函数;
  2. 在此基础上,完美转发指的是:如果 PerfectForward() 函数接收到的参数 t 为左值,那么该函数传递给 Func() 的参数 t 也是左值;
  3. 反之如果 function() 函数接收到的参数 t 为右值,那么传递给 Func() 函数的参数 t 也必须为右值。

使用任何一种引用形式,可以实现转发,但无法保证完美。因此如果使用 C++ 98/03 标准下的 C++ 语言,我们可以采用函数模板重载的方式实现完美转发,例如:

template<typename T>
void Func(T& arg)
{cout << "左值引用:" << arg << endl;
}template<typename T>
void Func(T&& arg)
{cout << "右值引用:" << arg << endl;
}template<typename T>
void PerfectForward(T&& arg)
{Func(arg);  // 利用重载的process函数进行处理
}int main()
{int value = 42;PerfectForward(value);       // 传递左值PerfectForward(123);         // 传递右值return 0;
}

输出展示:

 【解释说明】

  1. 在上述示例中,我们定义了两个重载的函数模板 Func,一个接收左值引用参数T& arg,另一个接收转发引用参数T&& arg;
  2. 然后,我们再定义一个模板函数PerfectForward,其参数也是转发引用T&& arg。在PerfectForward函数内部,我们通过调用Func函数来处理传递的参数;
  3. 通过函数重载的机制,传递的左值参数将匹配到接收左值引用的Func函数,传递的右值参数则匹配到接收转发引用的Func函数,从而正确地进行区分和处理;
  4. 通过函数模板的重载,我们可以根据参数类型将左值和右值区分开来,并分别处理,实现了针对不同值类别的精确匹配和操作。

2、模板中的&& 万能引用

显然,上述使用重载的模板函数实现完美转发也是有弊端的,此实现方式仅适用于模板函数仅有少量参数的情况,否则就需要编写大量的重载函数模板,造成代码的冗余。为了方便用户更快速地实现完美转发,C++ 11 标准中允许在函数模板中使用右值引用来实现完美转发。

还是以 PerfectForward() 函数为例,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:

//模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}

以如下代码为例:

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;
}

输出展示:

 【解释说明】

  1. 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
  2. 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
  3. 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
  4. 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
     

3、std::forward

C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地解决此问题。

  1. 完美转发通常与转发引用(forwarding reference)和 std::forward 函数一起使用;
  2. 转发引用是一种特殊的引用类型,使用&&语法进行声明,用于在函数模板中捕获传递的实参;
  3. std::forward 是一个模板函数,用于在函数模板内部将转发引用作为右值或左值引用进行转发。

如下演示了该函数模板的用法:

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)
{// forward<T>(t)在传参的过程中保持了t的原生类型属性。Fun(std::forward<T>(t));
}
int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(move(b)); // const 右值return 0;
}

程序执行结果为:

通过完美转发,我们可以在函数模板中正确处理传递实参的值类别,并将其转发到内部函数,以达到类型和值类别的完全保持,提高代码的灵活性和效率。


总结

学到这里,一些读者可能无法记清楚左值引用和右值引用各自可以引用左值还是右值,这里给大家一张表格,方便大家记忆:
 

  •  表中,Y 表示支持,N 表示不支持。

以上便是关于左值引用和右值引用的全部知识讲解!感谢大家的观看与支持!!!

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

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

相关文章

PID直观感受简述

0、仿真控制框图 1、增加p的作用&#xff08;增加响应&#xff09;P 2、增加I的作用&#xff08;消除稳差&#xff09;PI 3、增加D的作用&#xff08;抑制波动&#xff09;PID 加入对噪声很敏 4、综合比对

linux中定时器的使用

在Linux中&#xff0c;可以使用timer_create、timer_settime和timer_delete等函数来创建和管理定时器。下面是一个简单的示例程序&#xff0c;演示如何在Linux中使用定时器&#xff1a; #include <stdio.h> #include <stdlib.h> #include <signal.h> #inclu…

STL---list

目录 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用注意事项 2.list接口介绍及模拟实现 2.1构造​编辑 2.2容量 2.3修改 3.list迭代器 4.迭代器失效 5.模拟实现 6.vector和list的区别 1. list的介绍及使用 1.1 list的介绍 list的文档介绍 1. list是可以在常…

数据库第十七课-------ETL任务调度系统的安装和使用

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

Jenkins配置远程服务器SSH Server流程

说明&#xff1a;以阿里云轻量应用服务器&#xff0c;本文介绍如何在Jenkins中配置远程服务器&#xff0c;Jenkins安装参考这篇文章&#xff1b; 第一步&#xff1a;启动服务 首先&#xff0c;启动Jenkins容器&#xff0c;进入Jenkins管理后台&#xff0c;点击系统配置&#…

echarts 的dataZoom滑块两端文字被遮挡

问题&#xff1a; 期望&#xff1a; 解决方案&#xff1a; 1&#xff1a;调整宽度&#xff08;4版本的没有width属性&#xff09; 2. 参考&#xff1a;echarts图标设置dataZoom拖拽时间轴时自动调整两侧文字的位置_datazoom 位置_乌栖曲的博客-CSDN博客 设置文字的定位 cons…

物联网(IoT)安全挑战与解决方案: 分析物联网设备面临的安全威胁,以及如何设计和管理安全的IoT生态系统

第一章&#xff1a;引言 随着科技的飞速发展&#xff0c;物联网&#xff08;IoT&#xff09;作为连接世界的桥梁&#xff0c;已经成为现代社会不可或缺的一部分。然而&#xff0c;随着IoT设备数量的不断增加&#xff0c;其安全问题也日益显著。本文将深入探讨IoT领域面临的安全…

暄桐展览| 我们桐学有自己的习作展(1)

林曦老师《从书法之美到生活之美》的第五阶课程《静定的滋养2021》已告一段落。570天的用功&#xff0c;桐学们的技艺都有了水涨船高的进益。      无论书法课&#xff08;全阶和五阶&#xff09;还是国画课&#xff0c;暄桐都有一套完整系统的教学体系&#xff0c;也会在桐…

Java | IDEA中Netty运行多个client的方法

想要运行多个client但出现这种提示&#xff1a; 解决方法 1、打开IDEA&#xff0c;右上角找到下图&#xff0c;并点击 2、勾选

微信支付

文档地址&#xff1a;https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter9_1 封装的工具类 package com.qf.fmall.utils;import cn.hutool.core.util.XmlUtil; import cn.hutool.http.HttpRequest; import org.apache.shiro.crypto.hash.Md5Hash;import java.util.…

【keepalived双机热备与 lvs(DR)】

目录 一、概述 1.简介 2.原理 3.作用 二、安装 1.配置文件 2.配置项 三、功能模块 1.core 2.vrrp 3.check 四、配置双机热备 1.master 2.backup 五、验证 1.ping验证 2.服务验证 六、双机热备的脑裂现象 七、keepalivedlvs&#xff08;DR&#xff09; 1.作…

性能测试告诉你 mysql 数据库存储引擎该如何选?

简介 数据库存储引擎&#xff1a;是数据库底层软件组织&#xff0c;数据库管理系统&#xff08;DBMS&#xff09;使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能&#xff0c;使用不同的存储引擎&#xff0c;还可以…

Wireshark数据抓包分析之互联网控制报文协议_ICMP

一、实验目的: 通过使用wireshark抓取的ICMP数据包对这个ICMP控制报文进行分析 二、预备知识&#xff1a; 1.ICMP协议概述&#xff1a;ICMP是Internet Control Message Protocol的缩写&#xff0c;即互联网控制报文协议。它是TCP/IP协议族的一个子协议&#xff0c;用于IP主机、…

HDLBits-Verilog学习记录 | Verilog Language-Vectors

文章目录 11.vectors | vector012.vectors in more detail | vector113.Vector part select | Vector214.Bitwise operators | Vectorgates15.Four-input gates | Gates416.Vector concatenation operator | Vector317.Vector reversal 1 | Vectorr18. Replication operator | …

SD-WebUI和ComfyUI的局域网访问设置!

如何通过局域网访问AI绘画软件&#xff0c;这是星球成员提的一个问题&#xff0c;而且两个软件都问到了&#xff0c;我也回答过了。现在把内容整理一下发出来&#xff0c;大家可能用得着。 SD-WebUI和ComfyUI这两个AI绘画工具都是通过浏览器来使用&#xff0c;但是默认情况下并…

Request对象和response对象

一、概念 request对象和response对象是通过Servlet容器&#xff08;如Tomcat&#xff09;自动创建并传递给Servlet的。 Servlet容器负责接收客户端的请求&#xff0c;并将请求信息封装到request对象中&#xff0c;然后将request对象传 递给相应的Servlet进行处理。类似地&…

低代码赋能| 智慧园区项目开发痛点及解决方案

智慧园区是一个综合体&#xff0c;集技术开发、产业发展和学术研究于一体。作为未来智慧城市建设的核心&#xff0c;智慧园区充当着“产业大脑”和“指挥中心”的角色。它通过整合园区内的制造资源和第三方服务能力&#xff0c;实现园区各组成部分的协调运作、良性循环和相互促…

【数学建模】清风数模中正课4 拟合算法

拟合算法 在插值算法中&#xff0c;我们得到的曲线一定是要经过所有的函数点的&#xff1b;而用拟合所得到的曲线则不一样&#xff0c;拟合问题中&#xff0c;不需要得到的曲线一定经过给定的点。 拟合的目的是寻求一个函数曲线&#xff0c;使得该曲线在某种准则下与所有的数…

TCP可靠性机制

确认号/序列号/ACK TCP帮助确保数据的准确传递。为了做到这一点&#xff0c;其使用了一些特殊的标记和信息&#xff0c;其中包括序号、确认号和ACK字段。 其中&#xff0c;它将每个字节的数据都进行了编号. 即为序列号. 序列号&#xff1a;就像给书中的每一页都编了号码一样&a…

EMR电子病历系统 SaaS电子病历编辑器源码 电子病历模板编辑器

EMR&#xff08;Electronic Medical Record&#xff09;指的是电子病历。它是一种基于电子文档的个人医疗记录&#xff0c;可以包括病人的病史、诊断、治疗方案、药物处方、检查报告和护理计划等信息。EMR采用计算机化的方式来存储、管理和共享这些信息&#xff0c;以便医生和医…