左值引用和右值引用
左值引用
左值引用就是左值的别名,不是变量的别名,而是地址的别名,是与地址建立的一种映射关系。我们可以通过不同的别名访问同一块地址空间。
左值引用并非对象,只是为一个已经存在的对象所起的另外一个名字。
左值引用必须初始化,因为无法令左值引用重新绑定到另外一个对象。一旦初始化完成,左值引用将和他的初始值对象一直绑定在一起。
#include <iostream>
int main()
{
int x = 1;
int &y = x;
// output: 0x7ffc224801bc 0x7ffc224801bc
std::cout << &x << " " << &y << std::endl;
}
右值引用
右值引用是 C++11 为了实现移动语义(move semantic)和完美转发(perfect forwarding)而设计出来的新的数据类型,它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
右值引用,简单说就是绑定在右值上的引用,右值引用必须立即进行初始化操作,且只能使用右值进行初始化。
int && a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行”修改”:
int && a = 10;
a = 100;
// output: 100
cout << a << endl;
字面常量10
被存储在代码区,代码区对我们来说是只读的,我们不能修改其中的数据,所以编译器先在可写数据区创建只读数据10
的一份拷贝,再把位于可写数据区的拷贝的地址给引用a
,让引用封装这个地址。所以可以通过右值引用修改的数据,只是只读数据的一份拷贝,并不是真正的只读数据。
实现移动语义
移动语义和拷贝语义相对的,移动语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁。
要实现移动语义,需要定义移动构造函数,还可以定义移动赋值操作符。对于右值的拷贝和赋值会调用移动构造函数和移动赋值操作符。
在转移资源后,被移动的对象处于“有效但未定义的状态”(valid but unspecified state)
struct Foo {
Foo() { std::cout << "Constructed" << std::endl; }
Foo(const Foo &) { std::cout << "Copy-constructed" << std::endl; }
Foo(Foo &&) { std::cout << "Move-constructed" << std::endl; }
~Foo() { std::cout << "Destructed" << std::endl; }
};
int main()
{
Foo f1; // 默认构造函数
Foo f2 = f1; // 拷贝构造函数
Foo f3 = std::move(f1); // 移动构造函数
// Foo&& f4 = std::move(f1); // 不调用构造函数?
}
output:
Constructed
Copy-constructed
Move-constructed
Destructed
Destructed
Destructed