右值引用
再说起右值引用之前,我们先了解一下左值引用
int main() { int a = 10; int &b = a; // 定义一个左值引用变量 b = 20; // 通过左值引用修改引用内存的值 return 0; }
上面一段代码的汇编语言如下
int a = 10; // 这条mov指令把10放到a的内存中 00 mov dword ptr [a],0Ah int &b = a; /* 下面的lea指令把a的地址放入eax寄存器 mov指令把eax的内容放入b内存里面 */ 0035421F lea eax,[a] 00 mov dword ptr [b],eax b = 20; /* 下面的mov指令把b内存的值放入eax寄存器(就是a的地址) mov指令再把20放入eax记录的地址的内存里面(就是把20赋值给a) */ 00 mov eax,dword ptr [b] 00 mov dword ptr [eax],14h
从上面的指令可以看出,定义一个左值引用在汇编指令上和定义一个指针是没有任何区别的,定义一个引用变量int &b=a,是必须初始化的,因为指令上需要把右边a的地址放入一个b的内存里面(相当于定义了一个指针的内存),当给引用变量b赋值时,指令从b里面取出a的地址,并把20写入该地址,也就是a的内存中(相当于给指针解引用赋值),所以也说,使用引用变量时,汇编指令会做一个指针自动解引用的操作。
所以在汇编指令层面,引用和指针的操作没有任何区别!
int &b = 20;
上面的代码是无法编译通过的,现在你应该知道原因,因为定义引用变量,需要取右边20的地址进行存储,但是20是立即数字,没有在内存上存储,因此是无法取地址的,但是解决这个问题还是有办法的,如下:
const int &b = 20;
用常引用可以引用20这个常量数字,难道此时20就能取地址了吗?当然不是,因为现在在内存上产生了一个临时量保存了20,b现在引用的是这个临时量,相当于下面的操作:
/* 这里temp是在内存上产生的临时量 const int temp = 20; const int &b = temp; */ const int &b = 20;
汇编指令如下:
onst int &b = 20; 010517C8 mov dword ptr [ebp-14h],14h 《= ebp-14h就是内存栈上产生的临时量的内存地址 010517CF lea eax,[ebp-14h] 《= 取临时量的内存地址放入寄存器eax 010517D2 mov dword ptr [b],eax 《= 再把eax寄存器的值(放的是临时量地址)存入b中
因此到我们可以得到这样一个结论,上面的C++引用就是我们常用的左值引用,左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用,如const int &b = 20;但是这样一来,我们只能通过b来读取数据,无法修改数据,因为b被const修饰成常量引用了,怎么办?
此时就需要使用C++的右值引用
int &&b = 20; // 通过指令可以看到,原来const int &b=20和int &&b=20一模一样!!! 这里mov指令相当于是产生了临时量,起始地址ebp-14h 00CA18B8 mov dword ptr [ebp-14h],14h 把临时量的地址放入eax寄存器当中 00CA18BF lea eax,[ebp-14h] 再把eax的值(临时量的地址)放入b内存中(一个指针大小的内存) 00CA18C2 mov dword ptr [b],eax b = 40; 00CA18C5 mov eax,dword ptr [b] 00CA18C8 mov dword ptr [eax],28h
上面代码,定义一个右值引用变量是这样的int &&b=20,从汇编指令来看,依然要产生临时量,然后保存临时量的地址,也就是说const int &b=20和int &&b=20在底层指令上是一模一样的,没有任何区别,不同的是,通过右值引用变量,可以进行读操作,也可以进行写操作。
因此,可以给一个这样的结论可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值。
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
实现一个简单的栈
class Stack { public: // size表示栈初始的内存大小 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]; memcpy(mpstack, src.mpstack, sizeof(int)*mtop); } // 栈的赋值重载函数 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]; memcpy(mpstack, src.mpstack, sizeof(int)*mtop); return *this; } // 返回栈的长度 int getSize()const { return msize; } private: int *mpstack; int mtop; int msize; }; Stack GetStack(Stack &stack) { // 这里构造新的局部对象tmp Stack tmp(stack.getSize()); /* 因为tmp是函数的局部对象,不能出函数作用域, 所以这里tmp需要拷贝构造生成在main函数栈帧上 的临时对象,因此这里会调用拷贝构造函数,完成 后进行tmp局部对象的析构操作 */ return tmp; } int main() { Stack s; /* GetStack返回的临时对象给s赋值,该语句结束,临时对象 析构,所以此处调用operator=赋值重载函数,然后调用 析构函数 */ s = GetStack(s); return 0; }
运行结果如下:

我们为了解决浅拷贝问题,为类提供了我们自定义的拷贝构造函数和赋值运算符重载函数,并且这两个函数内部实现都是非常的耗费时间和资源(首先开辟较大的空间,然后将数据逐个复制),我们通过上述运行结果发现了两处使用了拷贝构造和赋值重载,分别是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)
Stack(int)
Stack(Stack&&) =》对应return tmp; 自动调用带右值引用参数版本的拷贝构造
~Stack()
operator=(Stack&&) =》 s = GetStack(s); 自动调用带右值引用参数的赋值重载函数
~Stack()
~Stack()
从上面的打印可以清晰的看到,上面两处的拷贝构造函数和赋值重载函数的调用,自动使用了带右值引用参数的版本,效率大大提升,因为没有涉及任何的内存开辟和数据拷贝。因为临时对象马上就要析构了,直接把临时对象持有的资源拿过来就行了。
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/207116.html原文链接:https://javaforall.net
