左值和右值
C++ 中对于左值和右值没有一个标准的定义,通常来说:
- 可以取得到地址的,有变量名称的,非临时的量就是左值,从硬件结构上看,存储在内存中的量就是左值;
- 无法取得到地址的,没有变量名称的,临时的量就是右值,从硬件结构上看,存储在寄存器中的量就是右值。
左值引用
常见的左值引用如下:
int a = 1;
int &b = a;
b = 2;
左值引用在汇编层面其实和普通的指针是一样的;定义的左值引用变量必须已经完成初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
int &c = 10;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中。如果必须定义一个立即数的左值引用,可以如下操作:
const int &c = 10;
使用常引用来使用常量10,此时,在内存上产生了临时变量保存常量10,因此,此时可以对变量10进行取地址的操作,上述操作就相当于:
const int tmp = 10;
const int &c = tmp;
总结:
- 左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
- 使用常引用后,只能通过引用来读取数据,无法修改数据,因为其被const修饰成常量引用了。
右值引用
定义右值引用的格式一般为:
类型 && 引用名 = 右值表达式;
右值引用是C++ 11新增的特性,C++ 98的引用为左值引用。右值引用用来绑定到右值,绑定到右值以后,本来会被销毁的右值的生存期会被延长,与绑定到它的右值引用的生存期相同。 在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
下面举个例子:
#include <iostream>
using namespace std;
class Stack {
public:
Stack(int size = 100) : msize(size), mtop(0) {
cout << "Stack Construct" << endl;
mpstack = new int[size];
}
~Stack() {
cout << "~Stack() Deconstruct" << endl;
delete[]mpstack;
mpstack = nullptr;
}
Stack(const Stack& src) : msize(src.msize), mtop(src.mtop) {
cout << "Stack(const Stack&) Copy" << endl;
mpstack = new int[src.msize];
for (int i = 0; i < msize; ++i) {
mpstack[i] = src.mpstack[i];
}
}
Stack& operator=(const Stack& src) {
cout << "Stack(const Stack&) Assign" << endl;
if (this == &src) {
return *this;
}
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
mpstack = new int[src.msize];
for (int i = 0; i < msize; ++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;
}
编译(禁用编译器优化)
g++ test.cpp -o a -fno-elide-constructors
输出:
Stack Construct
Stack Construct
Stack(const Stack&) Copy
~Stack() Deconstruct
Stack(const Stack&) Assign
~Stack() Deconstruct
~Stack() Deconstruct
可以看出,执行 s = GetStack(s); 时,包含了两个操作,先把tmp拷贝给一个临时变量,然后再把这个变量赋值给s。
在类中包含有 new 分配的成员时,一般要采用深拷贝,因此在上面为类提供了自定义的拷贝构造函数和赋值运算符重载函数,并且这两个函数内部实现都是非常的耗费时间和资源(首先开辟较大的空间,然后将数据逐个复制),我们通过上述运行结果发现了两处使用了拷贝构造和赋值重载,分别是tmp拷贝构造main函数栈帧上的临时对象、临时对象赋值给s,其中tmp和临时对象都在各自的操作结束后便销毁了,使得程序效率非常低下。
为了提高效率,现有的思路是将tmp持有的内存资源直接给临时对象,然后把临时对象的资源直接赋值给s,这样做的方式也就是右值引用,现在将上述例子修改为带右值引用参数的拷贝构造函数和赋值运算符重载函数。
Stack(Stack&& src) : msize(src.msize), mtop(src.mtop) {
cout << "Stack(const Stack&) Copy" << endl;
mpstack = src.mpstack;
src.mpstack = nullptr;
}
Stack& operator=(Stack&& src) {
cout << "Stack(const Stack&) Assign" << endl;
if (this == &src) {
return *this;
}
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
mpstack = src.mpstack;
src.mpstack = nullptr;
return *this;
}
程序自动调用了带右值引用的拷贝构造函数和赋值运算符重载函数,使得程序的效率得到了很大的提升,因为并没有重新开辟内存拷贝数据。
可以直接赋值的原因是临时对象即将销毁,不会出现浅拷贝的问题,我们直接把临时对象持有的资源赋给新对象就可以了。
所以,临时量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝,从而减少资源使用,提高了效率。
|