-
2021-12-15 09:51:39
#include <iostream> using namespace std; void change(int &rnum)//引用就是变量名的别名 { rnum =111; } //c++中能用引用的地方,就不要使用指针 int main() { int num(10);//左值,内存实体 int &rnum(num);//变量的别名 rnum =1;//等于num的别名 cout << num<<endl; change(num); cout << num<<endl; cout << "Hello World"; return 0; }
void show(int &&rrnum)//右值引用 { cout << rrnum<<endl; //6 } int main() { int a[5]{1,2,3,4,5}; show(a[3]+2); show(std::move(a[3]));//move移动语义,把左值变成右值 cout << "Hello World"; return 0; }
int main()
{
int num(10);//左值,内存实体
int data =0;
data = num+1;//右值
cout << data<<endl;
cout <<(void*)&data<<endl;
cout << "Hello World";
return 0;
}int main()
{
int num(10);//左值,内存实体
int data1 = num+4;
int &rdata(data1);//左值引用
int &&data (num+4);//右值引用,快速备份,编译器自动回收,节约内存
printf("%p",&data);
cout <<endl;
cout << data<<endl;
//cout <<(void*)&data<<endl;
cout << "Hello World";
return 0;
}左值引用与右值引用的区别:
左值引用往往引用的是内存里面的值
右值引用,用的是寄存器里面的值
左值引用
先看一下传统的左值引用。
int a = 10; int &b = a; // 定义一个左值引用变量 b = 20; // 通过左值引用修改引用内存的值
左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
int &var = 10;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:
const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:
const int temp = 10; const int &var = temp;
根据上述分析,得出如下结论:
- 左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
那么C++11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。
右值引用
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
定义右值引用的格式如下:
类型 && 引用名 = 右值表达式;
右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
int &&var = 10;
在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
用C++实现一个简单的顺序栈:
class Stack { public: // 构造 Stack(int size = 1000) :msize(size), mtop(0) { cout << "Stack(int)" << endl; mpstack = new int[size]; } // 析构 ~Stack() { cout << "~Stack()" << endl; delete[]mpstack; mpstack = nullptr; } // 拷贝构造 Stack(const Stack &src) :msize(src.msize), mtop(src.mtop) { cout << "Stack(const Stack&)" << endl; mpstack = new int[src.msize]; for (int i = 0; i < mtop; ++i) { mpstack[i] = src.mpstack[i]; } } // 赋值重载 Stack& operator=(const Stack &src) { cout << "operator=" << endl; if (this == &src) return *this; delete[]mpstack; msize = src.msize; mtop = src.mtop; mpstack = new int[src.msize]; for (int i = 0; i < mtop; ++i) { mpstack[i] = src.mpstack[i]; } return *this; } int getSize() { return msize; } private: int *mpstack; int mtop; int msize; }; Stack GetStack(Stack &stack) { Stack tmp(stack.getSize()); return tmp; } int main() { Stack s; s = GetStack(s); return 0; }
运行结果如下:
Stack(int) // 构造s Stack(int) // 构造tmp Stack(const Stack&) // tmp拷贝构造main函数栈帧上的临时对象 ~Stack() // tmp析构 operator= // 临时对象赋值给s ~Stack() // 临时对象析构 ~Stack() // s析构
为了解决浅拷贝问题,为类提供了自定义的拷贝构造函数和赋值运算符重载函数,并且这两个函数内部实现都是非常的耗费时间和资源(首先开辟较大的空间,然后将数据逐个复制),我们通过上述运行结果发现了两处使用了拷贝构造和赋值重载,分别是tmp拷贝构造main函数栈帧上的临时对象、临时对象赋值给s,其中tmp和临时对象都在各自的操作结束后便销毁了,使得程序效率非常低下。
那么我们为了提高效率,是否可以把tmp持有的内存资源直接给临时对象?是否可以把临时对象的资源直接给s?
在C++11中,我们可以解决上述问题,方式是提供带右值引用参数的拷贝构造函数和赋值运算符重载函数.
// 带右值引用参数的拷贝构造函数 Stack(Stack &&src) :msize(src.msize), mtop(src.mtop) { cout << "Stack(Stack&&)" << endl; /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/ mpstack = src.mpstack; src.mpstack = nullptr; } // 带右值引用参数的赋值运算符重载函数 Stack& operator=(Stack &&src) { cout << "operator=(Stack&&)" << endl; if(this == &src) return *this; delete[]mpstack; msize = src.msize; mtop = src.mtop; /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/ mpstack = src.mpstack; src.mpstack = nullptr; return *this; }
运行结果如下:
Stack(int) // 构造s Stack(int) // 构造tmp Stack(Stack&&) // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象 ~Stack() // tmp析构 operator=(Stack&&) // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s ~Stack() // 临时对象析构 ~Stack() // s析构
程序自动调用了带右值引用的拷贝构造函数和赋值运算符重载函数,使得程序的效率得到了很大的提升,因为并没有重新开辟内存拷贝数据。
mpstack = src.mpstack;
可以直接赋值的原因是临时对象即将销毁,不会出现浅拷贝的问题,我们直接把临时对象持有的资源赋给新对象就可以了。
所以,临时量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。
更多相关内容 - 左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
-
浅谈C++左值引用和右值引用
2020-08-31 17:26:48下面小编就为大家带来一篇浅谈C++左值引用和右值引用。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧 -
C++11中的左值引用和右值引用
2020-12-22 23:11:451.首先区分左值和右值 左值是表达式结束后依然存在的持久对象 右值是表达式结束时不再存在的临时对象 便捷方法:对表达式取地址,如果能,则为左值,否则为右值 举例: int a = 10 int b = 20 ... -
左值引用和右值引用
2022-03-08 10:00:39一、左值和右值 glvalue 可寻址的表达式,即可使用&操作符的表达式 ...二、左值引用和右值引用 引用是一种类型 左值引用:普通的引用 右值引用:可以延长临时对象的生命周期 三、移动构造 四、模版 ...文章目录
一、左值和右值
什么是左值、右值?
- 一般化左值(glvalue): 可寻址的表达式,即可使用&操作符的表达式
- 纯右值(prvalue): 只读表达式,即不可使用&操作符的表达式,一般为临时值
- xvalue: 延长了生命周期的表达式,即右值引用
- lvalue :等号左边的值称为左值
- rvalue :等号右边的值称为右值
总结:
- 左值右值是指一个表达式(当然这个表达式可以仅是一个简单的变量),区分左值和右值是看表达式能否使用&操作符。
- 左值引用和右值引用是指左值或者右值的型别,左值的型别可以为左值引用,也可以为右值引用。右值的型别可以为右值引用,但不能为左值引用,因为左值引用仅能引用左值(const修饰的左值引用可以引用右值,因为函数参数重const修饰的参数,编译器会为传入的右值创建临时变量,所以cosnt修饰的左值引用其实是引用的这个临时变量)。右值引用仅能引用纯右值。
二、左值引用和右值引用
引用是一种类型
左值引用:普通的引用
右值引用:可以延长临时对象的生命周期
注意:1)引用是一种型别的修饰;2)没有引用的指针;3)左值引用一定是左值;4)没有引用的指针(因为引用取地址实际为引用所修饰的型别的地址,而非引用的地址),但有指针的引用;
c++禁止比特的引用。std::vector的operator[]返回的不是bool&是因为std::vector做过特化,用一种压缩形式来表示其特有的bool元素,每个bool元素用一个比特来表示。c++禁止比特的引用,所以std::vector做了一个“隐式”代理三、移动构造
四、型别推导
1. 模版型别推导
模版形参中带引用或者指针符号时说明模版函数期望实参是一个引用或者指针,因此编译器会忽略传入的实参中包含的引用或指针,当左值实参传入万能引用时,编译器又会通过引用折叠规则,为实参添加引用符号。
模版中形参不带引用或指针符号时,说明模版函数期望实参到形参时按值传递,因此,编译器会忽略实参中的cv和引用指针符号。
模版型别推导过程中T不会被推导为右值引用T&&template<typename T> void f(T param); const char* const ptr = "exception"; f(ptr); //ParamType的型别为const char*,T的型别为const char*
请看下述函数模版代码:
template<typename T> void f(ParamType param);
调用语句形式为:
f(expr)
在编译期间,编译器会根据expr推导出T的型别和ParamType的型别。
推导规则如下:- 情况1:ParamType 是个指针或引用,但不是个万能引用
1.若expr具有引用类型,则忽略引用部分;
2.对expr的型别和ParamType的型别进行模式匹配,确定T的型别;
模版形式如下:
//1)ParamType为左值引用,仅能接收左值 templae <typename T> void f(T& param); //2)ParamType为右值引用,仅能接收右值 templae <typename T> void f(const T&& param); templae <typename T> void f(const std::vector<T>&& param); //3)ParamType为常引用 templae <typename T> void f(const T& param); //4)ParamType为引指针 templae <typename T> void f(T* param); int x = 27; const int cx = x; const int& rx = x; int&& rrx = 33; //1)模版为左值引用情形 f(x); //param的型别(ParamType)为int&,T的型别为int f(cx);//param的型别(ParamType)为const int&,T的型别为const int f(rx);//param的型别(ParamType)为const int&,T的型别为const int f(rrx);//param的型别(ParamType)为int&,T的型别为int //2)模版为右值引用情形 f(88);//param的型别(ParamType)为int&&,T的型别为int f(std::move(x));//param的型别(ParamType)为int&&,T的型别为int //3)模版为常引用情形 f(x); //param的型别(ParamType)为const int&,T的型别为int f(cx);//param的型别(ParamType)为const int&,T的型别为int f(rx);//param的型别(ParamType)为const int&,T的型别为int //4)模版为指针情形 const int* px = &x; f(&x); //param的型别(ParamType)为int*,T的型别为int f(px);//param的型别(ParamType)为const int*,T的型别为const int
总结:
- expr忽略引用修饰
- ParamType若不带cv修饰,则expr保留cv修饰,若带cv修饰,则expr忽略cv修饰
- 注意:右值引用形参仅能接受右值,左值引用形参仅能接受左值
- 为什么要忽略引用修饰?
模版形参声明为左值/右值引用说明编程者的意图是该模版函数想要接收一个左值/右值的实参,形参需要为左值/右值引用。所以编译器在在推导型别是会忽略实参中的引用。
情况2:ParamType是个万能引用
1.若expr是个左值,则T和ParamType都会被推导为左值引用。这是在模版型别推导中,T被推导为引用型别的唯一情形。
2.若expr是个右值,则同情况1的右值引用template<typename T> void f(T&& param); int x = 27; const int cx = x; const int& rx = x; //左值 f(x); //param的型别(ParamType)为int&,T的型别为int& f(cx); //param的型别(ParamType)为const int&,T的型别为const int& f(rx); //param的型别(ParamType)为const int&,T的型别为const int& //右值 f(27); //param的型别(ParamType)为int&&,T的型别为int
情况3:ParamType既非指针也非引用
当ParamType既非指针也非引用,则实参到形参按值传递
1.若expr具有引用型别,则忽略其引用部分
2.若expr具有cv修饰,也忽略之template<typename T> void f(T param); int x = 27; const int cx = x; const int& crx = x; f(x); //param的型别(ParamType)为int,T的型别为int f(cx); //param的型别(ParamType)为int,T的型别为int f(crx);//param的型别(ParamType)为int,T的型别为int
数组实参和函数实参
数组的型别:const char name[] = "12345567890";
型别为:const char [10]
函数的型别:void SomeFunc(int,double);
型别为:void (int,double)
当函数或数组作为模版函数的实参传递时,若按值传递,则形参型别会退化为:const char*
和void (*)(int,double)
。若按引用传递形参型别则不会发生型别退化,其型别分别为:const char [10]
和void (&)(int,double)
。2.auto型别推导
- 当不用大括号初始化时,auto型别推导和模版推导规则一致
- 当使用大括号初始化时,auto型别推导会假定用大括号括起的初始化表达式代表一个std::intializer_list,模版型别推导却不会。
- c++11支持在函数返回值或lambda表达式的形参中使用auto,意思是使用模版型别推导而非auto型别推导,因此不能使用大括号作为返回值,或者大括号列表作为实参,会导致编译不通过
c++11使用auto作为返回值时,返回值尾序法指定返回值型别
函数名字之前的auto和型别推导没有任何关系,只是为了说明C++11中的返回值型别尾序语法。返回值型别将在参数列表之后(即->之后)。
尾序语法的好处在于:在指定返回值型别时可以使用函数形参。
返回值型别先序语法会因为形参还未声明导致无法使用函数形参。
template<typename Container,typename Index> auto f(Container&& c,Index i) ->dedltype(std::forward<Container>(c)[i]){ return std::forward<Container>(c)[i]; }
- 优先使用auto,当auto不能满足要求时使用显示指定
1)“隐形”的代理型别可以导致auto根据初始化表达式推导出错误的型别。
2)带显示型别的初始化物习惯用法强制auto推导出你想要的型别
什么是隐形的代理型别?
std::vector的operator[]返回的是一个std::vector::reference型别的对象而非bool&,std::vector的其他实例化的operator[]的返回值都是对应型别的引用。
如何识别隐形的代理型别?
1)接口文档;2)头文件中的函数签名;
3.decltype型别推导
- 绝大多数情况下,decltype会得出变量或表达式的型别而不做任何修改
- 对于型别为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出T&
- c++14支持decltype(auto),和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是decltype的规则。即将auto的模版型别推导规则指定为decltype推导规则
为什么返回值要指定为decltype推导规则?
因为不指定,则auto使用的是模版推导规则,模版推导规则可能会忽略cv或引用修饰
五、引用折叠
组合: 引用有两种:左值引用和右值引用。所以有四种引用组合:左值-左值(& &)、左值-右值(& &&)、右值-左值(&& &)、右值-右值(&& &&)。意思为:左值引用的左值引用、左值引用的右值引用、右值引用的左值引用、右值引用的右值引用。前面的引用符号可以看作一种型别,后面的引用符号看作该型别的引用。
规则: 如果任一引用为左值引用,则结果为左值引用。否则(即两个皆为右值引用),结果为右值引用。
引用的引用允许出现的四种语境: 模版实例化、auto型别生成、创建和运用typedef和别名声明、以及decltype。六、函数的返回值
- 函数若返回左值引用,则函数的返回值一定是左值。因为左值引用一定是左值。
- 函数返回值为其他型别时,如,右值引用或者是非引用,则函数返回值为右值。
- 函数返回值优化(RVO)
编译器若要在按值返回的函数里省略对局部对象的复制(或者移动),则需要满足两个前提条件:1)局部对象型别和函数返回值型别相同;2)返回的就是局部对象本身;
七、std::move和sdt::forward
- std::move
1)std::move强制转换为右值
std::move的一种能够完成任务的实现
template <class T> typename remove_reference<T>::type&& move(T&& param) { typedef typename remove_reference<T>::type _Up; return static_cast<_Up&&>(param); }
推导后的param的类型为T&(实参为左值时)或T(实参为右值时)。返回时将param去除引用后强制转换为T&&型别,最终的返回结果为T&&型别的右值。
std::move为什么要返回右值引用类型而不是非引用类型?
返回右值引用类型可以确保返回值必定是右值,并且移动构造的形参类型为右值引用类型。若返回值为非引用类型,则无法应用于移动构造了。
2)std::move转换为右值时,若保留了常量性const,则不会调用移动构造函数,而是调用复制构造函数,因为移动构造不接受常量型别的参数,复制构造却可以。- std::forward
引用折叠是使std::forward得以运作的关键。
std::forward使用时需要具化:std::forward<T>();
为什么要指定T?
因为std::forward的参数类型为:typename std::remove_reference::type&,移除了引用修饰后变成了确定的左值引用,T的型别不能再由实参推导而来,因此需要在调用函数时指定T。参数的类型为什么不能为T&?因为参数的类型为T&时,涉及到的类型推导,且推导的结果总为T&,不能达到区分左右值的目的。
当模版函数中的参数不能由实参推导时,模版函数调用需要指定T。
template<typename T> void f(typename std::remove_reference<T>::type& param){ //T tRemove = param; T tRemove = int(); if (std::is_same<decltype(tRemove),int>::value) { std::cout << "----: tRemove is int" << std::endl; } else if (std::is_same<decltype(tRemove), int&>::value) { std::cout << "----: tRemove is int&" << std::endl; } else if (std::is_same<decltype(tRemove), int&&>::value) { std::cout << "----: tRemove is int&&" << std::endl; } } //call f int a1; int& a2 = a1; int&& a3 = 888; f<int>(a1); //output : ----: tRemove is int //f<int&>(a2); //output : ----: tRemove is int& f<int&&>(a3); //output : ----: tRemove is int&&
std::forward的一种能够完成任务的实现
template<typename T> T&& std::forward(typename std::remove_reference<T>::type& param) { return static_cast<T&&>(param); } //eg:在万能引用中使用std::forward template<typename T> void f(T&& param){ ... someFunc(std::forward<T>(param));//假设someFunc是一个函数 }
若传递给函数f的实参是个int型左值,则std::forward被推导为:
int& && std::forward(typename std::remove_reference<int&>::type& param) { return static_cast<int& &&>(param); } //推导后的结果为: int& std::forward(int& param) { return static_cast<int&>(param); }
强制转换为左值,其实什么也不做,结果仍为左值。
若传递给函数f的实参是个int型或int&&右值,则std::forward被推导为:
//传递int型别右值 int&& std::forward(typename std::remove_reference<int>::type& param) { return static_cast<int&&>(param); } //推导后的结果为: int&& std::forward(int& param) { return static_cast<int&&>(param); }
八、区分右值引用和万能引用
万能引用的条件:
1) T&&
2)T的型别由推导而来加上const修饰,则称为右值引用
template <typename T> void f(const T&& param); //param是个右值引用
-
左值、右值、左值引用和右值引用
2022-05-04 11:41:40举例说明左值引用和右值引用什么是左值引用和右值引用?左值和右值的转换右值变左值通过右值引用&&通过引用const &通过通用转换 forward左值和右值 什么是左值和右值? 按照我们常规理解左值就是在等号...左值和右值
什么是左值和右值?
按照我们常规理解左值就是在等号左边的值,右值是等号右边的值。如果你要这么理解,你就会发现遇到
++i
或i++
这一类的语句你就开始混乱了!其实左值和右值是我们习惯性的叫法,他的全拼应该是:
左值是:locator value
(可寻址的数据)
右值是:read value
(不可寻址的数据或用来读的数据)我们常规去理解,应该理解为:
在该程序语句之后能再找到该值,就是左值;否则就是右值。就是不可寻址!举例说明
左值:
++x; y*=33; //这里面++x是直接对x自增,我们得到的值就是x的值, //是可以后来直接使用x这个变量去使用这个值的。
右值:
x++; y+3; 123; //y+3这个值会被计算,但是没有被承接到, //后来即便我们再用y+3去获得这个临时的值,大小是一样的, //但不是我上次计算的那个值,我又经过了以此计算的到的。 //x++;这个从底层去分析: //x++会产生一个临时变量,用来保存x+1的值, //等到语句结束,将x+1赋值给x. //但是语句没结束时,这个临时变量时在寄存器中保存的,一个计算结果的临时变量, //此时是不可寻址的!!即右值。
程序分析
#include<iostream> #include<string> #define func(x) _func(x,"func("#x")") using namespace std; void _func(int &x,string str){ //左值值重载_func() cout << str << "left value !"<<endl; return ; } void _func(int &&x,string str){ //右值重载_func() cout << str << "right value !"<<endl; return ; } int main(){ int x = 3; int y = 4; func(123); func(x++); func(++x); func(x*3); func(x*=3); func(y/4); func(y/=4); return 0; }
输出结果为:
func(123)right value ! func(x++)right value ! func(++x)left value ! func(x*3)right value ! func(x*=3)left value ! func(y/4)right value ! func(y/=4)left value !
左值引用和右值引用
什么是左值引用和右值引用?
- 什么是引用?
引用表示为符号“&”;
引用就是用另外的名称来索引到该变量。左值引用
左值引用得到的就是还是一个左值。
#include<iostream> using namespace std; int main(){ int a = 10; int &b = a; cout << b << endl; //输出为10 b = 5; cout << a << endl; //输出为5 cout << &a << " " << &b << endl; //a和b的地址相同。 return 0; }
分析:
其中a和b都是左值,他们都是一个变量,且都是表示相同地址上存放的某个int类型的数。
且此时输出a和b的地址是一样的。右值引用 (important!!!)
右值引用的使用常常在自定义类中,可以查看该博文去理解。
右值引用操作符为 “&&”;
右值引用得到的是一个左值。
右值引用通常将一个临时变量拿过来用。
右值引用最主要的功能是解决的是自定义类重复构造冗余的问题。下面三种情况就告诉你什么时右值引用?为什么要有右值引用?
- 未使用右值引用时且不去承接返回值:
//"test2.cpp" #include<iostream> using namespace std; class A{ int x; public: A(int x = 0):x(x){ cout << this << ":default constructor"<<endl; } A(const A&a):x(a.x){ cout << this << ":copy constructor"<<endl; } A operator+(const A& a){ return A(x+a.x); } ~A(){ cout << this << ":destructor"<<endl; } }; int main(){ A a(1),b(3); a+b; cout << "====================="<<endl; return 0; }
通过关闭返回值优化
g++ test2.cpp -fno-elide-constructor
,可以看到结果:0x7fffbe1e127c:default constructor //A a(1) 0x7fffbe1e1280:default constructor //A b(3) 0x7fffbe1e1244:default constructor //A(a.x+b.x) 0x7fffbe1e1284:copy constructor //return A(a.x+b.x)->返回值会调用拷贝构造给承接a+b的变量A。代码中没有承接到a+b,会立马构析。 0x7fffbe1e1244:destructor 0x7fffbe1e1284:destructor ===================== //程序结束后构析 0x7fffbe1e1280:destructor 0x7fffbe1e127c:destructor
- 使用对象A直接去承接返回值
int main(){ A a(1),b(3); A c = a+b; //此时用A类对象C承接返回值。 cout << "====================="<<endl; return 0; }
关闭返回值优化,得到结果:
0x7ffd08697558:default constructor 0x7ffd0869755c:default constructor 0x7ffd08697524:default constructor //A(a.x+b.x) 0x7ffd08697564:copy constructor //return A(a.x+b.x)的临时对象 0x7ffd08697524:destructor ~A(a.x+b.x) 0x7ffd08697560:copy constructor //A C = (return A(a.x+b.x)) 拷贝构造 0x7ffd08697564:destructor ===================== 0x7ffd08697560:destructor 0x7ffd0869755c:destructor 0x7ffd08697558:destructor //可以看到产生大量的拷贝构造,临时变量我没有救火,卫视拷贝了一个拿来用。
- 使用右值引用且承接返回值
int main(){ A a(1),b(3); A &&c = a+b; //右值引用 cout << "====================="<<endl; return 0; }
关闭返回值优化,得到结果:
0x7ffc22247b74:default constructor 0x7ffc22247b78:default constructor 0x7ffc22247b44:default constructor 0x7ffc22247b7c:copy constructor 0x7ffc22247b44:destructor ===================== 0x7ffc22247b7c:destructor 0x7ffc22247b78:destructor 0x7ffc22247b74:destructor //可以发现return A(a.x+b.x)产生的临时对象直接被C用了,没产生多余的拷贝构造。
右值引用就是把右值变成左值,通常实在C++返回值上,对于自定子类的重复拷贝做了重要改善,大大提高了C++的效率。
右值引用的概念是C++中的重要概念!!!!。左值和右值的转换
左值变右值
- 通过move(class value)函数
move()可以通过man手册查看。
- 通过通用转换 forward<B&&>
-
- 通过引用const &
变成只读,也是不能放在等号左边
右值变左值
-
通过右值引用&&
-
通过通用转换 forward<B&>
-
左值引用、右值引用及移动语义
2021-07-06 11:25:12左值引用、右值引用及移动语义0.什么是左值和右值?1.左值引用『&』2.右值引用『&&』3.移动语义-移动构造函数和移动赋值运算符4.std::move() / forward()5.右值引用与模板函数6.移动构造和移动赋值 0....左值引用、右值引用及移动语义
0.什么是左值和右值?
左值是变量的地址,如变量名或指针。右值则是变量存储的内容。
int a=3; // a变量名,3是变量a存储的内容
当一个对象被用作左值的时候,用的是对象的身份,也就是在内存中的位置。而当对象被用作右值的时候,用的则是对象的值。左值是持久的,它存在于作用域期间。而右值是短暂的,它可以是字面常量或Lambda表达式或者是表达式产生的临时对象。
1.左值引用『&』
左值引用本质上是一个隐式指针,为对象的一个别名。通常说的『引用』指的就是『左值引用』。
int &ref_a = a;
声明引用时需注意:
- 引用在声明时必须初始化。
- 引用作为目标的别名使用,对引用的改动实际就是对目标的改动。
- 引用和变量指向同一地址单元。
引用与指针的差别
- 指针是个变量,可以把它再赋值指向别去的地址。
- 建立引用时必须进行初始化,且不会再关联其他不同变量。
- 因为指针是变量,所以可以有指针的引用。
2.右值引用『&&』
右值引用(Rvalue Referene) 是C++ 新标准(C++11, 11 代表2011 年) 中引入的新特性。左值引用使标识符可以绑定左值,而右值引用可以绑定右值。
针对临时对象的右值引用有什么存在的意义?
这其实就像计算机中拷贝文件,对文件的拷贝总是费时的且占用空间的,但移动文件却是便捷高效的。如果我们想把某个文件从同一个磁碟的A文件夹移动到B文件夹,如果我们复制文件、粘贴文件、删除A文件夹下原文件,这个操作是费时且低效的,移动他是最有效的办法–只是改变文件目录记录文件依然还在磁碟原来的位置。
3.移动语义-移动构造函数和移动赋值运算符
移动语义避免了移动原始数据,而只是修改了记录。这相当于计算机中移动文件,实际文件还留在原来的位置,而只是修改了记录。
实现移动语义:定义两个构造函数。一个是使用const左值引用作为参数的拷贝构造函数—可以实现深拷贝。一个是使用右值引用作为参数,它只在函数内实现所有权的转移—类似浅拷贝,但需要将指针成员指向NULL,而这是修改,因此不能为const。
// 涉及到内存管理,new的对象在堆上 class obj{ public: obj() : _val(0), _name(nullptr){} obj(int v, char* n):_val(v){ int len = strlen(n) + 1; _name = new char[len]; strcpy(_name, n); std::cout<<"constructor!\n"; } // obj(const obj&) = delete;//防止不期望的拷贝 obj(const obj& o1){ _val = o1._val; int len = strlen(o1._name) + 1; _name = new char[len]; strcpy(_name, o1._name); std::cout<<"copy constructor!\n"; } obj(obj&& oo){ _val = oo._val; _name = oo._name; oo._name = nullptr; std::cout<<"move constructor!\n"; } // 重载双目+运算符 obj operator + (const obj& o1) const{//成员函数重载双目运算符参数只有一个,另一个是this指针 int val = this->_val + o1._val; int len = strlen(o1._name) + strlen(this->_name) + 1; char *name = new char[len]; strcpy(name, this->_name); strcpy(name + strlen(this->_name), o1._name); obj tmp(val, name); } obj& operator = (obj&& o){ if(this == &o)//防止自赋值 return *this; delete [] _name; int len = strlen(o._name); _val = o._val; _name = o._name; o._name = nullptr; return *this; } private: int _val; char* _name; }; int main(){ obj o1(1,"abcd"); // 调用constructor obj o2(o1); // 调用copy constructor obj o3(o1 + o2); // std::move()接受一个参数,返回该参数对应的右值引用 obj o4(std::move(o1 + o2)); return 0; }
从示例代码中可以看到,移动构造函数和移动赋值函数实现的功能都是将传入的右值临时对象申请的内存的所有权转让给创建的新对象或者是被赋值对象,而原临时对象指向nullpter。
移动构造在GNU和VS下的区别
obj o3(o1+o2); // GNU下constructor、operator+ obj o3(o1+o2); // VS 下constructor创建临时对象,再move constructor 创建对象o3
o1+o2的结果是一个临时对象,是右值。通常认为上面的语句是必然会调用移动构造函数的,但编译器的解释有所不同.
GNU编译器优化不会调用移动构造函数
这对如上语句,在GNU G++编译器下会进行优化,认为o3是+的受益人,它会将双目运算+返回的临时变量tmp直接转移给o3,因此这个过程在GNU G++中是不会调用移动构造函数。
VS编译器则会调用移动构造函数
步骤是先执行operator+,在执行过程中调用constructor构建临时对象tmp。然后tmp被当做右值通过移动构造函数将tmp申请的内存对象转移给了o3,同时析构临时对象tmp(内存已被转移给了o3,tmp._name=nullptr)。
4.std::move() / forward()
- std::move()接受一个参数,返回该参数对应的右值引用
- forward()接收一个参数,返回该参数本来所对应的类型的引用。(即完美转发)
#include <iostream> //#include <utility> //for std::forward using namespace std; void print(const int& t) { cout <<"lvalue" << endl; } void print(int&& t) { cout <<"rvalue" << endl; } template<typename T> void Test(T&& v) //v是Universal引用 { //不完美转发 print(v); //v具有变量,本身是左值,调用print(int& t) //完美转发 print(std::forward<T>(v)); //按v被初始化时的类型转发(左值或右值) //强制将v转为右值 print(std::move(v)); //将v强制转为右值,调用print(int&& t) } int main() { cout <<"========Test(1)========" << endl; Test(1); //传入右值 int x = 1; cout <<"========Test(x)========" << endl; Test(x); //传入左值 cout <<"=====Test(std::forward<int>(1)===" << endl; Test(std::forward<int>(1)); //T为int,以右值方式转发1 //Test(std::forward<int&>(1)); //T为int&,需转入左值 cout <<"=====Test(std::forward<int>(x))===" << endl; Test(std::forward<int>(x)); //T为int,以右值方式转发x cout <<"=====Test(std::forward<int&>(x))===" << endl; Test(std::forward<int&>(x)); //T为int,以左值方式转发x return 0; } /*输出结果 e:\Study\C++11\16>g++ -std=c++11 test2.cpp e:\Study\C++11\16>a.exe ========Test(1)======== lvalue rvalue rvalue ========Test(x)======== lvalue lvalue rvalue =====Test(std::forward<int>(1)=== lvalue rvalue rvalue =====Test(std::forward<int>(x))=== lvalue rvalue rvalue =====Test(std::forward<int&>(x))=== lvalue lvalue rvalue */
5.右值引用与模板函数
在使用右值引用作为函数模板的参数时,与之前的用法有些不同:如果函数模板参数以右值引用作为一个模板参数,当对应位置提供左值的时候,模板会自动将其类型认定为左值引用;当提供右值的时候,会当做普通数据使用。可能有些口语化,来看几个例子吧。
template<typename T> void foo(T&& t) {} //随后传入一个右值,T的类型将被推导为: foo(42); // foo<int>(42) foo(3.14159); // foo<double><3.14159> foo(std::string()); // foo<std::string>(std::string()) //不过,向foo传入左值的时候,T会被推导为一个左值引用: int i = 42; foo(i); // foo<int&>(i)
因为函数参数声明为T&&,所以就是引用的引用,可以视为是原始的引用类型。那么foo()就相当于:
foo<int&>(); // void foo<int&>(int& t);
这就允许一个函数模板可以即接受左值,又可以接受右值参数;这种方式已经被std::thread的构造函数所使用,所以能够将可调用对象移动到内部存储,而非当参数是右值的时候进行拷贝。6.移动构造和移动赋值
回顾一下如何用c++实现一个字符串类MyString,MyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享,不然一个析构了,另一个也就完蛋了。
#include <iostream> #include <cstring> #include <vector> using namespace std; class MyString{ public: static size_t CCtor; //统计调用拷贝构造函数的次数 static size_t MCtor; //统计调用移动构造函数的次数 static size_t CAsgn; //统计调用拷贝赋值函数的次数 static size_t MAsgn; //统计调用移动赋值函数的次数 public: // 构造函数 MyString(const char* cstr=0){ if (cstr) { m_data = new char[strlen(cstr)+1]; strcpy(m_data, cstr); }else{ m_data = new char[1]; *m_data = '\0'; } } // 拷贝构造函数 MyString(const MyString& str) { CCtor ++; m_data = new char[ strlen(str.m_data) + 1 ]; strcpy(m_data, str.m_data); } // 移动构造函数 MyString(MyString&& str) noexcept:m_data(str.m_data){ MCtor ++; str.m_data = nullptr; //不再指向之前的资源了 } // 拷贝赋值函数=号重载 MyString& operator=(const MyString& str){ CAsgn ++; if (this == &str) // 避免自我赋值!! return *this; delete[] m_data; m_data = new char[ strlen(str.m_data) + 1 ]; strcpy(m_data, str.m_data); return *this; } // 移动赋值函数=号重载 MyString& operator=(MyString&& str) noexcept{ MAsgn ++; if (this == &str) // 避免自我赋值!! return *this; delete[] m_data; m_data = str.m_data; str.m_data = nullptr; //不再指向之前的资源了 return *this; } ~MyString() { delete[] m_data; } char* get_c_str() const { return m_data; } private: char* m_data; }; size_t MyString::CCtor = 0; size_t MyString::MCtor = 0; size_t MyString::CAsgn = 0; size_t MyString::MAsgn = 0; int main(){ vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间 for(int i=0;i<1000;i++){ vecStr.push_back(MyString("hello")); } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; } /* 结果 CCtor = 0 MCtor = 1000 CAsgn = 0 MAsgn = 0 */
可以看到,移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString(“hello” )是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。
不用奇怪为什么可以抢别人的资源,临时对象的资源不好好利用也是浪费,因为生命周期本来就是很短,在你执行完这个表达式之后,它就毁灭了,充分利用资源,才能很高效。
对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。
int main(){ vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间 for(int i=0;i<1000;i++){ MyString tmp("hello"); vecStr.push_back(tmp); //调用的是拷贝构造函数 } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; cout << endl; MyString::CCtor = 0; MyString::MCtor = 0; MyString::CAsgn = 0; MyString::MAsgn = 0; vector<MyString> vecStr2; vecStr2.reserve(1000); //先分配好1000个空间 for(int i=0;i<1000;i++){ MyString tmp("hello"); vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数 } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; } /* 运行结果 CCtor = 1000 MCtor = 0 CAsgn = 0 MAsgn = 0 CCtor = 0 MCtor = 1000 CAsgn = 0 MAsgn = 0 */
-
c++ 左值引用和右值引用
2022-04-15 16:39:01引用 1.1 引用是变量 | 对象的别名,操作引用等同于操作原有对象|变量。 1.2 使用引用的时候,必须先初始化,否则会报错。 1.3 引用不会开辟新的内存空间,没有内存地址。 引用相当于是外号,并不会产生全新的实体,... -
C++ 左值引用和右值引用
2021-06-27 15:33:49左值引用实际上是一种隐式的指针,它为对象建立一个别名,通过操作符&来实现。 // 左值定义格式 数据类型 & 表达式 // 实例 int a=10; int & ia=a; ia=2; 左值引用的特点: (1)一个C++引用被... -
左值、右值、左值引用、右值引用
2021-03-24 17:08:081. 左值 左值(lvalue,left value),顾名思义就是赋值符号左边的值,可以取地址。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象。 可以将左值看作是一个关联了名称的内存位置,允许程序的其他... -
C++: 左值、左值引用、右值、右值引用
2021-04-25 23:50:28引用(reference)分为左值引用和右值引用,通常我们说引用,指的是左值引用。 左值引用 引用为对象起了另外一个名字,引用类型引用另外一种类型,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名: ... -
C++左值和右值,左值引用和右值引用
2021-09-22 14:52:37左值和右值 C++ 中对于左值和右值没有一个标准的定义,通常来说: 可以取得到地址的,有变量名称的,非临时的量就是左值,从硬件结构上看,存储在内存中的量就是左值; 无法取得到地址的,没有变量...定义的左值引用 -
左值引用、右值引用
2021-08-23 20:42:54左值引用、右值引用 标签(空格分隔): C++11 左值引用 右值引用 一、左值右值的判断方法 方法一 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子: int i = 1; //... -
C++面试 左值、右值、左值引用、右值引用
2021-04-02 16:47:211、左值和右值 左值(left-values),缩写:lvalues ,located value 可定位值,其含义是可以明确其存放地址的值,更确切说对其的使用是基于地址 右值(right-values),缩写:rvalues , read value 可读的值,... -
左值右值,左值引用和右值引用及其用途
2020-09-11 01:44:04C和C++中定义了引用类型(reference type),存在左值引用(lvalue reference)。 而在C++11中,新增了右值引用(rvalue reference)这一概念, 虽然个人感觉右值引用用处不大,但在此一并讨论。 1.左值和右值 首先,... -
C++中左值引用和右值引用详解
2021-08-30 22:21:28C++中左值引用和右值引用 C++通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓的拷贝问题,通过move语义来将临时生成的左值中的资源无代价地转移到另外一个对象去。 1.C++中所有值必然属于左值或者... -
C++中的左值、右值、左值引用、右值引用
2022-02-21 08:31:04C++中的左值、右值、左值引用、右值引用 -
c++中的左值引用与右值引用
2021-11-17 16:15:59好吧,还是开始学学吧,否则连基本的面试机会都得不到,呜呜呜,所以提升自己,改变自己就从引用开始吧。 什么是引用 变量名 要想理解什么是引用,那就先要理解什么是变量名。变量名实质上是一段连续存储空间的别名... -
简单剖析C++右值引用和左值引用
2020-12-20 22:56:49简单剖析C++右值引用和左值引用 左值与右值的区别 左值可以有固定的名字,可以在声明所在的作用域内被任意调用; 右值没有固定的名称,匿名变量就是很好的例子,并且由于右值在作用域内没有固定的地址,因此不... -
c++——左值、右值、左值引用、右值引用
2018-10-18 18:17:391、左值和右值 左值(left-values),缩写:lvalues 右值(right-values),缩写:rvalues 直接上官网查,我一向倡导自己去懂得原理,而原理都是老外写的,当然我只是针对c++编程语言这样说。 ... -
面试题:什么是右值引用?右值引用与左值引用的区别
2020-11-20 23:12:37上一篇博客剖析了引用的底层实现原理,文中所述的“引用”默认表示为左值引用。 一、左值与左值引用 什么是左值引用呢? 左值引用,就是绑定到左值的引用,通过&来获得左值引用。 那么,什么是左值呢? 左值,... -
C++左值引用,const左值引用,右值引用,const右值引用
2020-06-02 10:09:04(1)普通(非const)左值引用只能绑定到普通(非const)左值; (2)const左值引用可绑定到const左值、非const左值、const右值、非const右值; (3)普通(非const)右值引用只能绑定到普通(非const)右值; (4)... -
C++中左值引用和右值引用的区别
2021-02-19 13:52:1801 什么是左值和右值? 左值: 就是有确定的内存地址、有名字的变量,可以被赋值,可以在多条语句中使用; 右值: 没有名字的临时变量,不能被赋值,只能在一条语句中出现,如:字面常量和临时变量。 02 如何区分... -
左值、右值,左值引用、右值引用、move
2020-06-04 23:03:17文章目录027左值和右值问题028左值_右值_左值引用_右值引用 027左值和右值问题 #include<iostream> #include<cstdlib> #include<string> #include <vector> using namespace std; int ... -
C++从入门到放弃之:C++ 左值引用与右值引用详解
2020-08-20 15:39:48左值引用2. 万能引用(常引用)3. 右值引用4. 引用型函数返回值5. 引用和指针6. 函数传参传递指针和引用的区别总结 C++ 引用 1. 左值引用 定义 引用即别名,某个变量的别名,对引用的操作就等同于对变量本身进行的操作... -
c++_左值引用, 右值引用, std::move
2021-03-17 17:30:55catalogOverview左右值引用右值引用、Move Overview ' “左值” 又称为 “左值表达式“, 两者一个东西。 ' ' 左值,其结果是一个“地址”,其操作 必须是一个“对象、内存” 有内存地址的东西' ' 左值是一个地址,...