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

【C++11】深入理解与应用右值引用

🔥 个人主页:大耳朵土土垚
🔥 所属专栏:C++从入门至进阶

这里将会不定期更新有关C/C++的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉

文章目录

  • 1.左值引用
  • 2.右值引用
  • 3.右值引用与左值引用的比较
  • 4. 右值引用使用场景和意义
  • 5.结语


1.左值引用

  在C++中,左值引用和右值引用是用来声明变量的引用类型的两种方式。传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以我们将C++11之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

例如:

int a = 10;
int& b = a;	//左值引用func(int& x)//左值引用
{
//...
}

我们之前学过对变量取别名和函数的传引用传参都是左值引用

  • 什么是左值?

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

例如,以下都是左值:

int* p = new int(0);
int b = 1;
const int c = 2;

简单来说,左值就是可以取地址的表达式,以上*p,b,c都可以取地址

  • 什么是左值引用?

  左值引用就是对左值的引用,就是给左值取别名。

  例如:

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

2.右值引用


  右值引用使用&&符号进行声明,它是对一个右值进行引用。

  右值(Rvalue)是指可以放在赋值运算符右侧的表达式,因为它没有一个确定的内存地址,所以右值也不能进行取地址。右值通常是一个临时的值或者一个将要被移动的值。右值可以用于初始化一个变量、传递给函数或者返回给调用者。

右值具有如下特点:

  1. 右值可以是字面量、临时对象、表达式的结果或者一些函数的返回值。
  2. 右值没有持久性,它们不能出现在赋值运算符的左侧。
  3. 右值可以被移动、交换或者销毁。

例如:

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);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;

结果如下:


需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,例如:上面例子中,不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

✨✨我们不能简单通过在赋值符号的左右来判断左值还是右值,因为左值也可以放在赋值符号的右边,但是我们可以通过是否可以取地址来判断左值和右值,左值可以取地址,右值不可以取地址。


3.右值引用与左值引用的比较


  • 对于左值引用:

  左值引用不可以直接引用右值,但是可以通过const修饰左值引用来引用右值

// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a;   // ra1为a的别名
//int& ra2 = 10;   // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;//引用右值
const int& ra4 = a;//引用左值

上述例子中,10具有常性,不能直接被左值引用,但是加了const之后就可以;右值的字面量、临时对象、表达式的结果或者一些函数的返回值都具有常性,所有被const修饰后可以左值引用右值。

  • 对于右值引用:

  右值引用不可以直接引用左值,但可以通过move(左值)来达到右值引用

// 右值引用只能右值,不能引用左值。
int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);

简单来说move可以将左值转换为右值,这样就可以对左值进行间接地右值引用

✨左值引用总结:

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

✨右值引用总结:

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

4. 右值引用使用场景和意义


  这里有一个string类,放在自己的命名空间内,和之前实现过的string类一样,用来当作辅助理解的例子:

#include<assert.h>
#include<iostream>
using namespace std;namespace tutu
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{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;reserve(s._capacity);for (auto ch : s)push_back(ch);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s)push_back(ch);}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];if (_str){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 = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};tutu::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}tutu::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}return str;}
}
  • 首先对于左值引用的使用场景:

左值引用做参数和做返回值都可以减少不必要的拷贝,提高效率。

例如:

void func1(tutu::string s)
{}
void func2(const tutu::string& s)
{}
int main()
{tutu::string s1("hello world");// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);
}

结果如下:


但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:将整型转换为string类型的函数:string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造):


这里可以看到传值返回时需要拷贝构造两次,但是编译器一般会给出优化,只拷贝一次:


但是不管怎样,传值返回都至少需要拷贝一次,所以这时候右值引用就可以帮助我们啦

  • 对于右值引用的使用场景:

因为右值一般是临时对象,都是将亡值,所有我们可以在bit::string中增加移动构造移动赋值,对右值对象使用,将参数右值的资源窃取过来,占位已有,那么就不用进行深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。这就像那些因疾病或者意外即将去世的人,如果签署了器官捐赠,就可以将他们的资源交换给别人。

// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造" << endl;swap(s);//直接交换s的资源,因为s是临时对象,很快就会被销毁}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}

这样对于将整型转换为string类型的函数:string to_string(int value)函数中传值返回构造的临时对象是右值,很快就会被销毁,所以将它拷贝给str时我们就可以通过移动构造直接交换临时对象的内容:


这里可以看到传值返回时需要构造两次,但是编译器一般会给出优化,将临时对象的拷贝省去,只构造一次:


我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。同样对于移动赋值,如果被赋值对象是右值,我们就可以走移动赋值,交换右值的资源,不用再进行深拷贝。


5.结语

  区分右值还是左值的关键在于是否可以取地址,对于左值的引用是左值引用,右值的引用是右值引用,它们都是给对象取别名。const左值引用可以引用右值,右值引用可以引用move后的左值;右值一般是临时对象,是将亡值,所以我们就可以在类中专门写一个供右值对象使用的拷贝构造和赋值重载函数,来置换它们的资源,从而减少不必要的深拷贝,我们将这两个函数称为移动构造和移动赋值。以上就是今天所有的内容啦~ 完结撒花 ~🥳🎉🎉

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • GPT教我学【这天学的物理知识】速度的相对性
  • DAY14:一条SQL查询语句是如何执行的?| 事务的四大特性有哪些?| 数据库的事物隔离级别有哪些
  • 区块链开发解决方案有哪些
  • 【LVI-SAM】激光点云如何辅助视觉特征深度提取
  • bps,bit,Byte,字符,字节,Mbps,Kbps,bps,MB,KB,B这些单位的区别与联系
  • jEasyUI 创建 CRUD 数据网格
  • 联通主机托管产品
  • 值得听歌入手的开放式耳机推荐?分享四款开放式蓝牙耳机
  • 手撕Python之函数
  • 环保专包二级资质续期:了解必要的时间准备
  • 机器视觉-3 光学成像之明场与暗场
  • 微信小程序手写签名
  • Spring Boot 的Web项目如何直接显示html
  • Linux开发:在 VSCode 中配置 Linux C++ 项目的头文件路径
  • 嵌入式——什么是堆、什么是栈
  • ES6指北【2】—— 箭头函数
  • 2018天猫双11|这就是阿里云!不止有新技术,更有温暖的社会力量
  • Angular Elements 及其运作原理
  • CSS魔法堂:Absolute Positioning就这个样
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • Javascript弹出层-初探
  • Java编程基础24——递归练习
  • JAVA之继承和多态
  • Linux链接文件
  • Python学习之路16-使用API
  • spring cloud gateway 源码解析(4)跨域问题处理
  • Travix是如何部署应用程序到Kubernetes上的
  • uni-app项目数字滚动
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • 测试开发系类之接口自动化测试
  • 关于for循环的简单归纳
  • 那些被忽略的 JavaScript 数组方法细节
  • 什么是Javascript函数节流?
  • 通过git安装npm私有模块
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • 教程:使用iPhone相机和openCV来完成3D重建(第一部分) ...
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • # Maven错误Error executing Maven
  • ()、[]、{}、(())、[[]]命令替换
  • (145)光线追踪距离场柔和阴影
  • (附源码)ssm基于jsp的在线点餐系统 毕业设计 111016
  • (黑客游戏)HackTheGame1.21 过关攻略
  • (六)激光线扫描-三维重建
  • (十二)devops持续集成开发——jenkins的全局工具配置之sonar qube环境安装及配置
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (一)SpringBoot3---尚硅谷总结
  • (一)基于IDEA的JAVA基础12
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • (转)详解PHP处理密码的几种方式
  • **PHP分步表单提交思路(分页表单提交)
  • . Flume面试题
  • .NET : 在VS2008中计算代码度量值
  • .NET CF命令行调试器MDbg入门(三) 进程控制
  • .Net Winform开发笔记(一)