目录
前言
C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
一、什么是左值、右值?
可以从2个角度判断:
- 左值可以取地址、位于等号左边;
- 而右值没法取地址,位于等号右边。
// 案例 1int a = 6 ;
再举个复杂点的例子:
// 案例 2struct A {A ( int a = 0 ) {a_ = a ;}int a_ ;};A a = A ();
二、什么是左值引用、右值引用
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。
1.左值引用
左值引用:能指向左值,不能指向右值的就是左值引用。
代码如下(示例):
// 示例 1int a = 5 ;int & ref_a = a ; // 左值引用指向左值,编译通过int & ref_a = 5 ; // 左值引用指向了右值,会编译失败// 示例 2 ( const左值引用是可以指向右值的)const int & ref_a = 5 ; // 编译通过,const左值引用不会修改指向值// 实例3 ( const & 作为函数参数)void push_back ( const value_type & val ); // 编译通过,const左值引用不会修改指向值
2.右值引用
代码如下(示例):
int && ref_a_right = 5 ; // okint a = 5 ;int && ref_a_left = a ; // 编译不过,右值引用不可以指向左值ref_a_right = 6 ; // 右值引用的用途:可以修改右值
3 对左右值引用本质的讨论
左右值引用的本质。
3.1 右值引用有办法指向左值吗?
int a = 5 ; // a 是个左值int & ref_a_left = a ; // 左值引用指向左值int && ref_a_right = std::move ( a ); // 通过 std::move 将左值转化为右值,可以被右值引用指向cout << a ; // 打印结果: 5
- 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
- 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升。
int && ref_a = 5 ;ref_a = 6 ;等同于以下代码:int temp = 5 ;int && ref_a = std::move ( temp ); // 此时 temp 等于右值ref_a = 6 ;
3.2 左值引用、右值引用本身是左值还是右值?
- 被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。仔细看下边代码:
// 形参是个右值引用
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左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
4、右值引用和std::move使用场景
std::move 只是类型转换工具,不会对性能有好处;
右值引用在作为函数形参时更具灵活性。他们有什么实际应用场景吗?
4.1 func(const T&)优化性能,避免浅拷贝
浅拷贝重复释放
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 运行报错
}
cout << "main finish" << endl;
return 0;
}
// 上面代码运行结果
constructor Aconstructor Aready returndestructor A, m_ptr: 0xf87af8destructor A, m_ptr:0xf87ae8destructor A, m_ptr: 0xf87af8main finish
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
// 上面代码 运行结果constructor Aconstructor Aready returncopy constructor Adestructor A, m_ptr:0xea7af8destructor A, m_ptr:0xea7ae8destructor A, m_ptr:0xea7b08main finish
移动构造函数
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
// 移动构造函数,可以浅拷贝
A(A&& a) :m_ptr(a.m_ptr) {
a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
cout << "move constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
if(m_ptr)
delete m_ptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
// 上面代码 运行结果constructor Aconstructor Aready returnmove constructor Adestructor A, m_ptr:0destructor A, m_ptr:0xfa7ae8destructor A, m_ptr:0xfa7af8main finish
4.2 移动(move )语义
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
private:
char* m_data;
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
MyString() {
m_data = NULL;
m_len = 0;
}
MyString(const char* p) {
m_len = strlen (p);
copy_data(p);
}
MyString(const MyString& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor is called! source: " << str.m_data <<
std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Copy Assignment is called! source: " << str.m_data <<
std::endl;
return *this;
}
// 用c++11的右值引用来定义这两个函数
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str.m_data <<
std::endl;
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str.m_data <<
std::endl;
if (this != &str) {
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
virtual ~MyString() {
if (m_data) free(m_data);
}
};
int main()
{
MyString a;
a = MyString("Hello"); // Move Assignment
MyString b = a; // Copy Constructor
MyString c = std::move(a); // Move Constructor is called! 将左值转为右值
std::vector<MyString> vec;
vec.push_back(MyString("World")); // Move Constructor is called!
return 0;
}
4.3 forward 完美转发
Template < class T >void func ( T && val );
看下面例子
int && a = 10 ;int && b = a ; // 错误int && b = std::forward < int > ( a ); //正确
4.4 emplace_back 减少内存拷贝和移动
对于STL容器,C++11后引入了emplace_back接口,emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。
考虑这样的语句:
vector < string > testVec ;testVec . push_back ( string ( 16 , 'a' ));
上述语句足够简单易懂,将一个string对象添加到testVec中。底层实现:
- 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
- 其次,vector内会创建一个新的string对象,这是第二次构造。
- 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次 string构造和一次析构。