C++11中引用了右值引用和移动语义,可以避免无谓的复制
什么是左值、右值
左值可以取地址,位于等号左面
右值不可以取地址,位于等号右面
左值有一个持久的状态,右值要么是一个字面常量,要么是在表达式求值过程中的一个临时对象
左值引用与右值引用
引用的本质就是别名,可以通过引用修改变量的值,传参时避免拷贝
左值引用
能指向左值,不能指向右值的就是左值引用
int a = 5; int &ref_a = a; //左值引用指向左值,编译通过 int &ref_a = 5; //左值引用指向右值,编译失败 |
引用是变量的别名,由于右值没有地址,没有办法修改,所以左值引用不能指向右值
但是,const左值引用可以指向右值
const int &ref = 5; //可以通过 |
原因在于,const左值引用不会修改指向值,所以可以指向右值。这也是为什么要使用const& 作为函数参数的原因。如果不使用const,常数传入函数就无法通过。
void func(int & a); func(5); //编译失败,因为不能传入右值
void func2(const int & a); func2(5); //编译成功,因为可以指向右值 |
右值引用
右值引用的标志为 &&, 可以指向右值,不能指向左值
int && refR = 5; //work int a = 5; int && refR = a; //failed |
右值可以指向左值嘛?
通过std::move,可以将左值转化为右值,从而被右值引用所指向
int a = 5; int && refR = std::move(a); refR = 6; cout<<a; //输出结果是6 |
move的本质,其实就是把一个左值强制转化为右值,相当于一个强制类型转换。
左值引用、右值引用本身是左值还是右值?
被声明出来的左、右值引用都是左值。因为被声明出来的左右值都是有地址的,也位于等号左面。
// 形参是个右值引用 void change(int&& right_value) { right_value = 8; } int main() { int a = 5; // a是个左值 int &ref_a_left = a; // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 change(a); // 编译不过,a是左值,change参数要求右值 change(ref_a_left;) // 编译不过,左值引用ref_a_left本身也是个左值 change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 change(std::move(a)); // 编译通过 change(std::move(ref_a_right)); // 编译通过 change(std::move(ref_a_left)); // 编译通过 change(5); // 当然可以直接接右值,编译通过 cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; // 打印这三个左值的地址,都是一样的 } |
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝
- 右值引用可以通过std::move指向左值;左值引用只能指向左值(const左值引用可以指向右值)
- 作为函数形参时,右值引用更灵活。虽然const左值可以做到左右值都接受,但是它无法被修改,有局限性。
右值引用和std::move适用场景
右值引用作为函数形参更具灵活性。std::move只是一个类型转换工具,本身对性能没有影响
右值引用优化性能,避免深拷贝(移动构造函数)
对于含有堆内存的类,默认的构造函数是浅拷贝的,也就是简单的将地址进行复制。而我们需要提供深拷贝的拷贝构造函数,否则会导致堆内存的重复删除。
move constructor提供了浅拷贝,避免了对临时对象的深拷贝,提高了性能。A&&通过分析参数是左值还是右值建立分支。如果是临时值,会选择移动构造函数,只进行浅拷贝,从而避免了额外的拷贝,提高性能。这也就是移动语义
移动语义可以将资源通过浅拷贝的方式从一个对象转移到另一个对象,减少不必要的临时对象的创建和销毁,提高程序的性能.
完美转发
forward完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递后仍然是左值,若是右值传递后仍然是右值。
例如,右值引用a本身是个左值
int && a = 10; int && b = a; //错误,a本身是左值 |
因此,有了forward完美转发,可以保持其值类型
int && a = 10; int && b = std:: forward<int>(a); |
func(T && t);
func(1); //传入以后,t本身是左值 int y = 10; func(std::forward<int>(y)); //以右值的方式转发y func(std::forward<int&>(y)); //以左值方式转发y |
emplace back减少内存拷贝和移动
stl容器中,加入了emplace_back接口。emplace_back是就地构造,不需要构造后再次复制到容器中,因此效率更高
vector<string> vec; vec.push_back(string(16, 'a')); |
传统的push_back,有三大步骤:
- string(16, 'a')会创建一个string的临时对象,涉及一次string构造过程
- vector内部会创建一个新的string对象,第二次构造
- 最开始的临时string对象被析构
emplace_back则可以直接在vector中创建一个对象,省略了一次构造和一次析构
第一种办法耗时最长。调用的是左值引用,需要一次string的拷贝构造函数
第2,3,4中耗时差距不大,调用的都是右值引用,移动构造函数的耗时更少,因为不需要重新分配内存空间
第5种办法,emplace_back耗时最少,因为emplace_back只调用构造函数,不需要移动构造函数或者拷贝构造函数