目录
C和C++中定义了引用类型(reference type),存在左值引用(lvalue reference)。
而在C++11中,新增了右值引用(rvalue reference)这一概念。
1.左值和右值
左值(lvalue):
非临时性对象的表达式。有名字,可以取地址。如:非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
右值(rvalue):
临时性对象的表达式,没有名字,临时生成的,不可取地址。如:立即数,函数的返回值。
vector<string> arr(3);
const int x = 2;
int y;
int z = x + y;
string str = "foo";
在上述代码中,arr, str, y, z等都是左值,x也是一个左值,且他不是一个可修改的左值;
而类似于2, x+y这类临时(没有专属变量名)的值则是右值。
左右值转换:
右值转化为左值 :直接新建变量然后赋值就可以了。
int b=a+1;将a+1这个右值转变为左值
左值转化为右值: std::move()
move (a) ; 将a这个左值转变为了右值
2.引用
(1)左值引用
左值引用本质是指针常量,左值引用只能引用左值。
int a = 10;
int &b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
int &b=a; 实际执行的是 int* const b=&a;
(2)常引用
int &var = 10;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过常引用解决:
const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:
const int temp = 10;
const int &var = temp;
(3)右值引用
左值引用只能引用左值,而常引用只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
所以增加右值引用专门用来引用右值。
声明:通过两个&&来声明。如: int && x = 5;
右值引用绑定到右值,绑定后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
3.左值引用的用途
(1)作为复杂名称变量的别名
auto & whichList = theList[myHash(x, theList.size())];
可以看到,我们用简短的whichList
代替了其原本复杂的名称,这能够简化我们的代码书写。
(2)用于自身调用
设想我们希望通过rangeFor循环使一个vector对象所有值都增加1,下面的rangeFor循环是做不到的
for (auto x : arr) // x仅相当于每个元素的拷贝
++x;
但我们可以通过使用引用达到这一目的。
for (auto & x : arr)
++x;
(3)函数的返回引用
假定有一个findMax函数,它返回一个vector中最大的元素。若给定vector存储的是某些大的对象时,
下述代码中的x拷贝返回的最大值到x的内存中:
auto x = finaMax(vector);
在大型的项目中这显然会增大程序的开销,这时我们可以通过引用来减小这类开销:
auto & x = findMax(vector);
类似的,我们在处理函数返回值的时候也可以使用传引用返回。
但是要注意,当返回的是类中私有属性时,传回的引用会导致外界能够对其修改。
函数返回值时会产生一个临时变量作为函数返回值的副本,而返回引用时不会产生值的副本。
T f(); 返回一般的类类型,返回的类类型不能作为左值,但可以直接调用成员函数来修改,返回类类型调用复制构造函数。
const T f(); 此种类型与上述第一种相同,唯一不同的是返回的类类型不能调用成员函数来修改,因为有const限定符。
T& f(); 返回类的引用可以作为左值,并且返回的类类型引用可以直接调用成员函数来修改,返回的类类型调用移动构造函数。
const T& f(); 不能作为左值,不能调用成员函数修改,不会调用复制构造函数。
(4)参与函数中的参数传递
在C和C++的函数中,函数对直接传入的形参进行修改并不会改变实参的值。而有时我们希望能够修改实参,例如经典的Swap()函数。
要想实现修改实数,我们可以通过传入指针和传入引用实现。
对直接传入参数和传入指针不了解的可以看这篇:C++实现swap功能的常见几种错误【值传递、地址传递】
4.右值引用的用途
(1)避免拷贝,提高性能,实现move()
右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。
(2)避免重载参数的复杂性,实现forward()
一个很详细的解释:C++11 的右值引用——细微却最重要的改动!
5.std::move和std::swap
前面我们谈到可以使用引用来减少复制产生的内存开销。
考虑这样一种情况,在进行元素交换时,我们通常使用一个缓存变量temp
来临时保存数据;而对temp
直接进行=的赋值操作时,实际上temp复制了一次原有对象的内存,但我们需要只是对象之间的移动而不是复制,而C++STL中的std::move函数便可以达成这一操作。
这可能有些抽象,让我们通过例子来看看:
void swap(vector<string> & x, vector<string> & y)
{
vector<string> temp = std::move(x);
x = std::move(y);
y = std::move(temp);
}
上述例子是C++STL中std::swap的源码之一,相信它很好的示范了std::move是如何使用的,而在源码中使用,也足以说明它的高效。
C++引入移动构造函数,专门处理像这种,用a初始化b后,就将a析构的情况。
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。这意味着,移动构造函数参数是一个右值或者将亡值的引用。也就是说,用一个右值或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而move语句,就是将一个左值变成一个将亡值。
移动构造函数应用最多的地方就是STL中。