1. 说一下理解的右值引用
1> 说明右值引用的概念
右值引用其实就是以引用传递的方式使用右值,表现为使用&& 修饰的变量;
那么什么是右值?
2> 延申右值的概念
要说明右值引用,首先需要说明一下什么是右值:(参照 c++ primer 471页)
- 右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。 也分为非常量右值和常量右值;右值一般是一个常量或者表达式求值过程中产生的临时对象
与之相对应的还有左值:
- 左值,就是在内存中有确定的存储地址、有变量名,表达式结束依然存在的值;分为非常量左值和常量左值;
左值引用举例说明:
int a=10; //非常量左值(有确定存储地址,也有变量名)const int a1=10; //常量左值(有确定存储地址,也有变量名)const int a2=20; //常量左值(有确定存储地址,也有变量名)//非常量左值引用int &b1=a; //正确,a是一个非常量左值,可以被非常量左值引用绑定int &b2=a1; //错误,a1是一个常量左值,不可以被非常量左值引用绑定int &b3=10; //错误,10是一个非常量右值,不可以被非常量左值引用绑定int &b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量左值引用绑定//常量左值引用const int &c1=a; //正确,a是一个非常量左值,可以被非常量右值引用绑定const int &c2=a1; //正确,a1是一个常量左值,可以被非常量右值引用绑定const int &c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定const int &c4=a1+a2; //正确,(a1+a2)是一个常量右值,可以被非常量右值引用绑定
(此处不对左值与右值做深入剖析)
3> 延申右值引用的引入原因
追溯到 c98/03 标准,其中已经有引用这个概念,使用 & 表示,但是该引用方式有一个缺陷,即正常情况下,只能操作 c++ 左值,无法对右值添加引用;举例:
int num = 20;int &lReference1 = num; // 正确int &lReference2 = 20; // 错误
(c++98 标准下)编译器允许为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。
但是,其支持使用常量左值引用操作右值:
int num = 20;const int &lReference1 = num;const int &lReference2 = 20; // 正确
我们知道右值一般是没有名称的,要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改操作(例如移动语义),显然左值引用的方式行不通。
因此 c++11标准引入了右值引用,能够操作右值;
4> 延申右值引用的应用场景:函数传参 + 移动语义
右值引用的应用场景,主要是支持移动语义和完美转发;
1> 函数传参
举个例子,有时候函数传参需要传递一个类对象:
class TestClass{public:TestClass(std::string str): m_str(str){ printf("construct TestClass: %s\n", str.c_str()); }TestClass(const TestClass& t){ printf("copy TestClass\n"); }~TestClass(){ printf("destruct TestClass\n"); }std::string m_str;};void print(TestClass && t) { printf("print %s\n", t.m_str.c_str()); }void print1(TestClass t) { printf("print1 %s\n", t.m_str.c_str()); }void main(){TestClass t("c++");print1(t); // 传递左值的方式会触发拷贝构造函数, 多一次拷贝}
上面代码的输出如下:
construct TestClass: c++ copy TestClass print1 destruct TestClass destruct TestClass
即传递左值的方式触发了一次拷贝构造函数,我们将主函数代码修改如下:
print(std::move(t)); // 传递右值引用的方式不会触发拷贝构造函数
输出如下:
construct TestClass: c++ print c++ destruct TestClass
可以看出将右值引用作为函数实参传递能够避免多余的拷贝;
2> 移动语义
其实就是实现类的移动构造函数。举个例子,先实现如下未添加移动构造函数的类:
class WithoutMoveConstructClass{public:WithoutMoveConstructClass(){ printf("construct WithoutMoveConstructClass \n"); }WithoutMoveConstructClass(const WithoutMoveConstructClass& w){ printf("copy WithoutMoveConstructClass \n"); }~WithoutMoveConstructClass(){ printf("destruct WithoutMoveConstructClass \n"); }};WithoutMoveConstructClass get_wclass() { return WithoutMoveConstructClass(); }int main(){WithoutMoveConstructClass w = get_wclass();}
在未经优化的条件下编译通过,这样会打印日志:
construct WithoutMoveConstructClass < -- 执行 WithoutMoveConstructClass()copy WithoutMoveConstructClass < -- 执行 return WithoutMoveConstructClass()destruct WithoutMoveConstructClass < -- 销毁 WithoutMoveConstructClass() 产生的匿名对象copy WithoutMoveConstructClass < -- 执行 w = get_wclass()destruct WithoutMoveConstructClass < -- 销毁 get_wclass() 返回的临时对象destruct WithoutMoveConstructClass < -- 销毁 w
整个初始化的流程包含以下几个阶段:
- 1> 执行
get_wclass()函数体,调用WithoutMoveConstructClass类默认构造函数生成一个匿名对象; - 2> 执行
return WithoutMoveConstructClass(),调用拷贝构造函数复制一份之前生成的匿名对象,将其作为函数返回值;函数体执行完毕前,匿名对象会被析构销毁 - 3> 执行
w = get_wclass();,再次调用拷贝构造函数,将之前拷贝得到的临时对象复制给w,(该行代码执行完毕,get_wclass()函数返回的对象会被析构) - 4> 程序执行结束前,会自行调用
WithoutMoveConstructClass类的析构函数销毁 w
若代码编译选项不执行优化,上述代码中, return WithoutMoveConstructClass() 会因为创建临时对象而调用拷贝构造函数;
下面添加移动构造函数:
MoveConstructClass(MoveConstructClass&& t){ printf("move MoveConstructClass\n"); }
然后再次运行代码:
construct MoveConstructClassmove MoveConstructClassdestruct MoveConstructClassmove MoveConstructClassdestruct MoveConstructClassdestruct MoveConstructClass
可以发现,两次操作均触发了移动构造函数;
总结:当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
移动语义,即移动构造函数的方式,能够省去不必要的拷贝,大大提升程序的性能。
引申 深拷贝与浅拷贝
3> 完美转发
实际开发中使用较少,
