C++ 左值引用与右值引用超详解

目录

一 左值与右值

        1.左值

        2.右值

        3.总结

二 左值引用与右值引用

1.左值引用

2.右值引用

3.总结与探究

  3.1右值引用可以修改么?取地址么?

   3.2左值引用与右值引用转化

        左值引用 引用 右值

右值引用 引用 左值

    3.3左值引用与右值引用相同之处

     3.4左值引用与右值引用不同之处

  三 引用用处

        1.左值引用

     2.右值引用

        2.1移动构造

2.2移动赋值

2.3完美转发

3.总结


一 左值与右值

        1.左值

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

        如下都c,p,b为左值。

int c = 0;
const int b = 2;
int* p = nullptr;

        2.右值

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

        以下都为右值

int fun()
{return 1;
}
void test()
{int a = 1, b = 2;//右值10;a + b;fun();
}

        如下图,将右值放在=左边就会报错,对其取地址也会报错

        3.总结

        左值一般为我们自己定义的变量,在定义时开辟了内存,我们可以对这块内存赋值,修改内存中的值,如果有const也仅从语法层面上不允许修改,这块内存在其生命周期结束的时候销毁。

        右值一般为临时变量,是程序运行时产生的中间产物,他不是我们用户自己定义开辟空间的,是由编译器帮我们开辟空间,并且在用完就立即销毁。右值的生命周期一般只在当前语句,当我们要对右值进行赋值时,他已经释放空间了,此时我们再进行访问就是野访问,(与野指针一样造成内存问题),所以我们不能对右值进行修改,编译器强制语法检查,遇到修改操作就报错。

        右值中特殊的就是字面常量,他们存储在内存的常量区,内存为只读属性,不可以修改,不可以取地址,他们的生命周期与程序的生命周期一样,不会用完即销。

        分辨左右值最常用方法就是判断他是否可以取地址。如果不可以就为右值。

二 左值引用与右值引用

       

1.左值引用

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

int c = 0;
const int b = 2;
int* p = nullptr;//左值引用
int& lc = c;
const int& lb = b;
int*& lp = p;

        在int& lc = c;语句后,lc与c使用的是同一块空间,lc与c完全等效,一荣俱荣,一损俱损。

2.右值引用

        右值引用就是对右值的引用,给右值取别名。他的语法与左值引用十分相似,右值引用语法为对应类型加上两个& 。

int fun()
{return 1;
}
void test()
{int a = 1, b = 2;//右值引用int && r1=10;int && r2=(a + b);int && r3=fun();
}

        如果改为左值引用就会报错,如下图。

3.总结与探究

        首先明确一点就是不管是左值引用还是右值引用,都是给一段数据取别名。对于左值引用而言,他与绑定的变量共用同一块空间,是之前我们定义该变量(左值)开辟的空间,没有再开辟出新内存。       

        对于右值引用而言,他也没有开辟内存,他绑定的一块空间是编译器运行时为右值自动开辟的空间,没有再开辟出新内存。

        在这里有个矛盾?右值引用引用干了什么?如果按照编译器对于普通右值的处理,当前语句结束就销毁,那么右值引用指向的空间就是野空间(参照指针叫法),我们对右值引用修改就会造成内存泄漏等重大内存问题,那么这样不就是脱裤子放屁了么!

        显然右值引用语法规定不是这样的,(以该句为例int && r3=fun();)当我们用r3引用fun()这个右值的时候,这块空间原本是由编译器释放销毁的,但被人为的抢走释放权限,加上右值引用这句话,就相当于说明我认为这块空间还有利用的价值,你编译器先别着急释放,交给我来释放,这里水很深,交给我来把握。我们就拥有了这块空间的生杀大权,编译器就不会立即释放该内存。

  3.1右值引用可以修改么?取地址么?

        右值是一定不可以修改和取地址的,那么右值引用可以么?注意右值引用和右值是两个东西,只有左值引用和左值才一样,左值引用可以修改和取地址。

        实践是检验真理的唯一标准,上代码。

int main()
{//右值引用int&& r1 = 10;cout << r1 << endl;cout << (void *) & r1 << endl;r1 = 20;cout << r1 << endl;return 0;
}

        运行结果如下。右值引用时可以修改的,也可以取地址。

        其实当执行完int&& r1 = 10;语句后他就与int c = 10;int& r1 = c;这两句的效果一样,到这里可能很多人被饶了进去,我们来捋一捋。

        为什么右值不可以修改?(取地址实际上也是为了后面修改操作)因为右值当当前语句结束就销毁,后面语句再访问就是访问野空间。

        为什么右值引用可以修改?我们从编译器手中拿走了右值销毁的权利,“右值”在引用后依旧存在,可以进行正常的访问修改操作。

        左值引用和左值使用的都是用户自定义开辟的空间,所以左值支持修改。

        我们可以通过一些汇编了解左右值引用

        通过上图我们可以更好的理解这句话。不管是左值引用还是右值引用,都是给一段数据取别名。

   3.2左值引用与右值引用转化

        左值引用 引用 右值

        首先如下代码是正常的定义右值引用,是没有任何问题的,那么我们可以使用左值引用么?

int&& ra = 10;

        如下代码,直接使用左值是错误的,编译器报错。通过上述的汇编图我们知道左值引用与右值引用最后都转化为指针,那么左值引用与右值引用的语法就是为了通过编译器的语法检测,编译器检测右值不支持修改操作,但一般的左值引用支持修改,我们就可以加上const,禁止修改,骗过编译器检测。

int& la = 10;

        如下图。直接使用左值引用是会报错的,加上const修饰就可以通过编译器语法检查。

       将上述右值引用修改便可以通过编译

int fun()
{return 1;
}
void test()
{int a = 1, b = 2;//左值引用const int& r1 = 10;const int& r2 = (a + b);const int& r3 = fun();
}
右值引用 引用 左值

         首先如下代码是正常的定义左值引用,是没有任何问题的,那么我们可以使用右值引用么?

int c = 0;
//左值引用
int& lc = c;

        

        直接写如上图是肯定不可以的,编译器会进行类型检查,类型不匹配就报错。我们就可以通过强制类型转换骗过编译器。如下图,此时就不会有报错了。

        如下都是可以通过编译的。

int c = 0;
const int b = 2;
int* p = nullptr;//右值引用
int&& lc = (int&&)c;
const int&& lb = (const int&&)b;
int*&& lp = (int*&&)p;

        并且在右值引用后,lc与c用的是同一块空间,此时仿佛lc是左值引用一样。我们可以通过如下代码检测。

int main()
{int c = 0;//右值引用int&& lc = (int&&)c;cout << c <<" " << lc << endl;c = 1;cout << c <<" " << lc << endl;lc = 112;cout << c <<" " << lc << endl;return 0;
}

        通过上述代码验证,我们就可以断定他们用的是同一块空间。

        看到大家这都会十分疑惑,C++11更新右值引用干什么?绕来绕去,闲的没事么?其实不尽然,C++这样设计是为了提高效率,减少深拷贝的消耗,在第三大模块再细谈。

    3.3左值引用与右值引用相同之处

       1.都是引用,都是再给一段空间取别名

        2.除了const修饰外,左值引用与右值引用都可以修改。

        3.底层都是指针,语法层面不同罢了

     3.4左值引用与右值引用不同之处

        1.左值引用引用左值,左值可以修改;右值引用引用右值,右值不可以修改

        2.左值是由用户自定义变量组成,右值一般为编译器运行时定义临时变量。

  三 引用用处

        1.左值引用

               通过上图的汇编代码,我们可以知道引用有和指针同等的效率,那么在函数设计传递参数的时候,我们就可以传递引用来代替指针,如下代码。

void Print1(vector<int> & t)
{}
void Print2(vector<int>* pt)
{}int main()
{vector<int> t1(10);Print1(t1);Print2(&t1);return 0;
}

        用左值引用代替指针后,不仅用起来更方便,在传参时不需要加上&,使用时不用加上*,并且引用比指针更加的安全。如果在 Print2中修改pt的值,就会造成野指针问题,而vector<int> & t就相对而言更加的安全了。

     2.右值引用

        2.1移动构造

        为了下面举例现象明显,可以简单实现string类

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;//MyString
namespace MS
{class string{public:const  size_t npos=-1;//构造函数string(){cout << "string():" << endl;_size = 0;_capacity = 0;_str = new char[1] {'\0' };}string(const char* str){cout << "string(const char* str):" << endl;int sz = strlen(str);_str = new char[sz + 1];strcpy(_str, str);_size = sz;_capacity = sz;}string(const string& s){cout << "string(const string& s):" << endl;_str = new char[s._size + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}//析构函数~string(){delete[] _str;_str = nullptr;}string& operator=(const string& s){cout << "operator=(const string& s):" << endl;if (this != &s){delete[] _str;_str = new char[s._size + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;}string& operator+= (const string& str){int len = str._size;reserve(_size + len);strcpy(_str + _size, str.c_str());_size += len;return *this;}const char* c_str()const{return _str;}string& operator+= (const char* s){int len = strlen(s);reserve(_size + len);strcpy(_str + _size, s);_size += len;return *this;}string& operator+= (char c){reserve(_size + 1);_str[_size] = c;_str[_size + 1] = '\0';_size++;return *this;}void reserve(size_t n){if (n <= _capacity)return;char* tmp = new char[n + 1];strcpy(tmp, _str);delete[]_str;_str = tmp;_capacity = n;}//容量size_t size()const{return _size;}size_t capacity()const{return _capacity;}void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}friend string to_string(int value);friend ostream& operator<< (ostream& os, string& str);private:int _size=0;int _capacity=0;char* _str=nullptr;};string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}string str;int d = 0;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);d++;}if (flag == false){str += '-';}for (int i = 0; i < d / 2; i++)swap(str._str[i], str._str[str._size - i - 1]);return str;}ostream& operator<< (ostream& os, string& str){for (int i = 0; i < str.size(); i++){os << str._str[i];}return os;}
}

        假如我们运行下述代码。

int main()
{MS::string s1 = MS::to_string(123);cout << s1<<endl;return 0;
}

         MS::to_string(123)返回的是string类型对象,然后s1拷贝构造这个对象完成初始化。如下图

        由上图可以知道我们为了初始化s2创建了两次str变量,一次是在to_string函数中,一次是为函数返回值创建,这是完全不优化的情况,事实上现在的编译器会有优化,可以将为返回值创建str改为直接创建s2,减少依次拷贝消耗,如下图。

        上面这种还是比较可以理解的优化,但VS2022开的优化更大更叹为观止,即使上面优化后,我们任然要建立两次变量,但VS2022把s2与to_string中的str优化为一个变量,这样就只用建立一个变量如下图。

        这里调用的是str的构造函数,VS将s2与str优化成一个变量,故只有一次构造函数。

        VS2022的优化程度十分大,如果想运行完全不优化的编译器,可以在linux下添加额外指令运行,如下

命令为:其中a.cpp 为源文件名,-o 后面的aobj是你命名的文件名字

g++ -fno-elide-constructors Teacher.cpp -o aobj

        经管编译器优化做的很好,但是这不是语言本身控制的,不同的编译器对上述代码的优化不同,很少像VS2022优化这么大,C++11就引入了右值引用的语法用来优化上述的代码。

        回到最初的代码。MS::to_string(123);返回值按照我们之前讲的就是右值,那么我们就可以根据右值类型增加个构造函数。

int main()
{MS::string s1 = MS::to_string(123);cout << s1<<endl;return 0;
}

        如下代码。我们就在构造函数中直接掠夺右值引用的资源,交换后就相当于初始化完了s2变量,我们称这种特殊的构造函数叫做移动构造。

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

        加上移动构造后,上述代码运行结果如下。这看起来我们创建了两次string变量,实际上我们只消耗了创建一个str变量的时间,第二次创建只是交换资源,代价极小。这样不管编译器做不做优化,根据语法我们就可以优化上述的情况,提高效率。

2.2移动赋值

        显然我们不只会有构造时候有右值,=赋值的时候也会出现右值,这样我们就可以像上面重载构造函数一样,重载赋值函数,提高效率。

int main()
{MS::string s2;s2 = MS::to_string(123);cout << s2;return 0;
}

        上述代码运行结果如下

        经过编译器的优化后,我们任然需要消耗创建两个个str变量的时间,此时就可以重载右值版本的赋值重载函数如下。

string& operator=(string&& s)
{cout << "operator=(string&& s):移动赋值" << endl;swap(s);return *this;
}

        运行结果如下。这样看起来还是消耗创建两个个str变量的时间,但我们知道移动赋值只交换了资源,消耗极小。于是就相当于提高了效率。

2.3完美转发

        首先了解一个std中函数move,他的作用是将参数类型强制转换为右值,不管参数是左值还是右值。

        运行如下代码

int main()
{MS::string s1("1234");MS::string s2(s1);MS::string s3(move(s1));return 0;
}

        如下分析

        现有如下模板,C++11规定T&&可以接受右值引用也可以接受左值引用,也就是说t可以为右值也可以为左值。

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

        如下代码是可以正常运行的。结果如下图。


template<typename T>
void PerfectForward(T&& t)
{cout << t << endl;
}int main()
{PerfectForward(10);           // 右值int a = 1;PerfectForward(a);            // 左值return 0;
}

        经过之前我们看的汇编代码我们也可以更好的理解这种用法,不管左值引用还是右值引用最后都转化为指针处理,只要强制转换或者加上const就可以骗过编译器。

        接着运行下述代码

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=1;PerfectForward(a);            // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);      // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

        运行结果如下。我们最后打印的都是左值,只是由于右值引用在绑定右值后就退化为左值引用,编译器将t都识别为左值引用。绑定后的右值引用和左值引用没什么区别,都可以赋值都可以取地址,编译器不会主动的区分他们。

        为了解决上述的情况,就引入了完美转发概念,即在传递过程中保持引用的特性,即右值引用不退化。引入forward函数,如果为左值什么都不办,为退化后的右值,进行一次move

template<typename T>
void PerfectForward(T&& t)
{Fun(std::forward<T>(t));
}

       

        对于左值,模板相当于实例化出如下代码。

int a=1;
PerfectForward(a);            // 左值
void PerfectForward(T&& t)
{Fun((t));
}

        对于右值,模板相当于实例化出如下代码。在传递中保持右值属性。

PerfectForward(std::move(a)); // 右值
void PerfectForward(T&& t)
{Fun(move(t));
}

        加上这句代码后就可以正常运行,如下图

3.总结

        C++11引入右值引用的概念就是为了提高效率解决一些特殊场景下的问题,不管是右值引用还是左值引用都是再给一段空间取别名,只不过这段空间分为用户主动开辟与运行时临时变量两种。

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

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

相关文章

栈与队列 - 逆波兰表达式求值

150. 逆波兰表达式求值 方法一&#xff1a;栈 /*** param {string[]} tokens* return {number}*/ var evalRPN function(tokens) {const stack [];for (const token of tokens) {if (isNaN(Number(token))) { // 非数字const n2 stack.pop(); // 出栈两个数字const n1 s…

【Unity开发】几种空值判断的性能测试

【Unity开发】几种空值判断的性能测试&#xff09; 项目优化过程中&#xff0c;一个非常细节的优化&#xff0c;就是在项目数据处理过程中&#xff0c;会用大量的null和“”空值的判断&#xff0c;参考了一些网友说的性能差别很大&#xff0c;是不是真的需要优化的问题&#xf…

16:【stm32】I2C的使用一:I2C片上外设的使用

I2C 1、片上外设1.1&#xff1a;寄存器与内部结构 2、通过I2C向外发送数据2.1&#xff1a;I2C的初始化2.1.1&#xff1a;初始化SCL和SDA2.1.2&#xff1a;使能时钟PCLK1&#xff08;APB1&#xff09;2.1.3&#xff1a;配置I2C1的参数 2.2&#xff1a;发送数据2.2.1&#xff1a;…

Ⅰ、基于 WebGPU 从 0 到 1 渲染 GLTF:第一个三角形

Ⅰ、基于 WebGPU 从 0 到 1 渲染 GLTF&#xff1a;第一个三角形 WebGPU 是一种面相网页的现代图形 API&#xff0c;由主要浏览器供应商开发。与 WebGL 相比&#xff0c;WebGPU 对 GPU 提供了更直接的控制&#xff0c;使应用程序能更有效地利用硬件&#xff0c;类似于 Vulkan 和…

如何在C++ QT 程序中集成cef3浏览器组件去显示网页?

目录 1、问题描述 2、为什么选择cef3浏览器组件 3、cef3组件的介绍与下载 4、将cef3组件封装成sdk 5、如何使用cef3组件加载web页面 5.1、了解CefApp与CefClient 5.2、初始化与消息循环 5.3、如何创建浏览器 5.4、重载CefClient类 6、在qt客户端集成cef组件 7、最后…

「12月·长沙」第三届传感、测量、通信和物联网技术国际会议(SMC-IoT 2024)

第三届传感、测量、通信和物联网技术国际会议&#xff08;SMC-IoT 2024&#xff09;将于2024年11月29日-2024年12月1日召开&#xff0c;由湖南涉外经济学院主办。会议中发表的文章将会被收录, 并于见刊后提交EI核心索引。 会议旨在围绕传感、测量、通信和物联网技术等相关研究…

基于node.js的宠物寄存管理系统,基于express的宠物寄存系统

摘 要 伴随着社会以及科学技术的发展&#xff0c;互联网已经渗透在人们的身边&#xff0c;网络慢慢的变成了人们的生活必不可少的一部分&#xff0c;紧接着网络飞速的发展&#xff0c;系统管理这一名词已不陌生&#xff0c;越来越多的宠物店等机构都会定制一款属于自己个性化…

DWA局部路径规划算法

DWA——Dynamic Window Approach动态窗口法 发展 动态窗口法是一种局部路径规划算法&#xff0c;起源于对移动机器人在复杂环境中实时避障的需求。该算法由F. D. Proentzen和O. Khatib提出&#xff0c;后经过不断优化&#xff0c;已成为移动机器人领域中的标准算法之一。 运…

xss.function靶场(hard)

文章目录 WW3源码分析源码 DOMPpurify框架绕过覆盖变量notifyjs作用域和作用链域构建payload WW3 源码 <!-- Challenge --> <div><h4>Meme Code</h4><textarea class"form-control" id"meme-code" rows"4"><…

Spring Boot实战:使用模板方法模式优化数据处理流程

概述 在软件开发过程中&#xff0c;我们经常需要处理各种各样的数据&#xff0c;这些数据可能来自不同的源&#xff0c;比如数据库、文件系统或者外部API等。尽管数据来源不同&#xff0c;但很多情况下处理这些数据的步骤是相似的&#xff1a;读取数据、清洗数据、转换数据格式…

华为的流程管理

华为建设流程体系始于2000年&#xff0c;那时华为公司面临着快速扩张和全球化发展的挑战&#xff0c;意识到传统的管理模式已经无法满足业务发展的需求。为了提高公司的管理效率和竞争优势&#xff0c;华为决定启动流程体系的建设。在建设过程中&#xff0c;华为借鉴了业界最佳…

云计算的三大服务模式:IaaS、PaaS、SaaS的深入解析

在数字化转型的浪潮中&#xff0c;云计算以其独特的灵活性、可扩展性和成本效益&#xff0c;正逐渐成为企业IT架构的核心。云计算提供了三种主要的服务模式&#xff0c;分别是基础设施即服务&#xff08;IaaS&#xff09;、平台即服务&#xff08;PaaS&#xff09;和软件即服务…

Spring发送邮件性能优化?如何集成发邮件?

Spring发送邮件安全性探讨&#xff01;Spring发送邮件功能有哪些&#xff1f; 邮件发送的性能逐渐成为影响用户体验的重要因素之一。AokSend将探讨如何在Spring框架中进行Spring发送邮件的性能优化&#xff0c;确保系统能够高效、稳定地处理大量邮件请求。 Spring发送邮件&am…

和鲸携手山东大学数字人文实验室,推动新文科与人工智能融合发展

为深入推进产教融合与校企合作&#xff0c;推动人工智能在人文学科中的广泛应用与深入发展&#xff0c;8 月 15 日&#xff0c;山东大学数字人文实验室与和鲸科技 101 计划推进会暨新文科人工智能实验室标杆案例打造讨论会于威海顺利召开。山东大学数字人文实验室副主任陈建红、…

12.2 使用prometheus-sdk向pushgateway打点

本节重点介绍 : 使用golang sdk打prometheus4种指标&#xff0c;推送到pushgateway gauge、counter、histogram、summary的初始化4种类似的设置值的方法推送到pushgateway的方法 prometheus配置采集pushgateway&#xff0c;grafana上配大盘 golang-sdk 项目地址 https://git…

系统架构设计师 - 软件工程(2)

软件工程 软件工程&#xff08;13-22分&#xff09;非常重要软件系统建模系统设计界面设计 ★★软件设计结构化设计 ★★面向对象设计 ★★★★★基本过程设计原则设计模式创建型模式&#xff1a;创建对象结构型模式&#xff1a;更大的结构行为型模式&#xff1a;交互及职责分配…

科三预约考试,为什么我场次排名在前,后面排名又变了

什么时候知道是否预约成功 系统确认考试预约结果的时间一般为考试前5-7个工作日&#xff0c;同时根据预约人数系统会自行判断提前1-2日或延长1-2日公示预约结果&#xff0c;学员至少考试前三天会收到预约成功短信通知。 如果预约失败了怎么办&#xff1f;会计入考试次数吗&am…

Java之线程篇一

目录 如何理解进程&#xff1f; 进程和线程的区别 线程的优点 线程的缺点 线程异常 线程用途 创建线程 方法一&#xff1a;继承Thread类&#xff0c;重写run() 观察线程 小结 方法二&#xff1a; 实现Runnable接口&#xff0c;重写run() 方法三&#xff1a;继承Threa…

【西安电子科技大学】2024年士兵计划考研信息总结!

西安电子科技大学 学校简介上方图片奖助学金下方图片研招网址https://gr.xidian.edu.cn/普通复试https://gr.xidian.edu.cn/info/1073/13301.htm士兵复试总分为各学科门类、各专业学位类别&#xff08;领域&#xff09;国家A类线&#xff0c;单科不限。士兵名额20报考说明无录取…

MES系统从哪几方面提升企业制造水平?

在当今这个快速变化的制造环境中&#xff0c;企业对于提升制造水平的追求从未停止。制造执行系统&#xff08;MES&#xff09;作为连接企业战略规划与车间实际操作的核心工具&#xff0c;其重要性日益凸显。盘古信息MES系统&#xff0c;凭借其独特的功能模块和创新的设计理念&a…