一、右值引用和移动语义作用
C++11 中引入了右值引用和移动语义,可以避免无谓的赋值,提高了程序性能
二、什么是左值、右值
int a = 6;
左值可以取地址、位于等号左边。
a
可以通过 &
取地址,位于等号左边,所以 a
是左值。 右值没法取地址,位于等号右边。
6 位于等号右边,6 没法通过 &
取地址,所以 6 是个右值。
struct A {
A ( int a = 0 ) {
a_ = a;
}
int a_;
} ;
A a = A ( ) ;
a
可以通过 &
取地址,位于等号左边,所以 a
是左值。A()
是个临时值,没法通过 &
取地址,位于等号右边,所以 A()
是个右值。有地址的变量就是左值,没有地址的字面值、临时值就是右值 。
三、左值引用
四、右值引用
右值引用的标志是 &&
,右值引用可以指向右值,不能指向左值 。int && ref_a_right = 5 ;
int a = 5 ;
int && ref_a_left = a;
ref_a_right = 6 ;
右值引用指向左值。int a = 5 ;
int & ref_a_left = a;
int && ref_a_right = std:: move ( a) ;
cout << a << endl;
ref_a_left = 10 ;
cout << a << endl;
ref_a_right = 20 ;
cout << a << endl;
std::move
把左值强制转化为右值,让右值引用可以指向左值。 其实现等同于一个类型转换:static_cast<T&&>(lvalue)
,即单纯的 std::move(xx)
不会有性能提升。 被声明出来的左、右值引用都是左值 ,因为被声明出的左、右值引用是有地址的,也位于等号左边。
std::move
会返回一个右值引用 int&&
,它是左值还是右值 ?
从表达式 int&& ref = std::move(a)
来看,右值引用 ref
指向的必须是右值,所以 std::move
返回的 int&&
是个右值。 右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值 。
void change ( int && right_value) {
right_value = 8 ;
}
int main ( ) {
int a = 5 ;
int & ref_a_left = a;
int && ref_a_right = std:: move ( a) ;
change ( std:: move ( a) ) ;
change ( std:: move ( ref_a_left) ) ;
change ( std:: move ( ref_a_right) ) ;
change ( 5 ) ;
cout << & a << endl;
cout << & ref_a_left << endl;
cout << & ref_a_right << endl;
return 0 ;
}
五、结论
从性能上讲,左、右值引用没有区别,传参使用左、右值引用都可以避免拷贝。 右值引用可以直接指向右值,也可以通过 std::move
指向左值,而左值引用只能指向左值(const 左值引用
也能指向右值)。 作为函数形参时,右值引用更灵活,虽然 const 左值引用
也可以做到左、右值都接受,但它无法修改,有一定局限性。
六、右值引用和 std::move 使用场景
右值引用优化性能,避免深拷贝。 浅拷贝重复释放:对于含有对堆内存的类,我们需要提供深拷贝的拷贝构造函数,否则会导致堆内存的重复删除。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 a1;
A a2;
cout << "ready return" << endl;
if ( flag)
return a1;
else
return a2;
}
int main ( ) {
{
A a = Get ( false ) ;
}
cout << "main finish" << endl;
return 0 ;
}
深拷贝构造函数:上面代码的默认拷贝构造函数是浅拷贝,main
函数的 a
和 Get
函数的 a2
会指向同一个指针 m_ptr
,在析构的时候会导致重复删除该指针,正确的做法是提供深拷贝的拷贝构造函数。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 a1;
A a2;
cout << "ready return" << endl;
if ( flag) {
return a1;
} else {
return a2;
}
}
int main ( ) {
{
A a = Get ( false ) ;
}
cout << "main finish" << endl;
return 0 ;
}
移动构造函数:虽然深拷贝构造函数可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的。上面代码中的 Get
函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 a
,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么这个拷贝构造的代价会很大,带来了额外的性能损耗。 下面的代码中没有了拷贝构造,取而代之的是移动构造,从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&
,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&&
用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义,右值引用的一个重要目的是用来支持移动语义的。 移动语义可以将资源(堆,系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。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 ;
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 a1;
A a2;
cout << "ready return" << endl;
if ( flag) {
return a1;
} else {
return a2;
}
}
int main ( ) {
{
A a = Get ( false ) ;
}
cout << "main finish" << endl;
return 0 ;
}
移动语义。
move 语义
是将对象的状态所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要 move 语义
起作用,核心在于需要自己实现移动构造函数。 forward
完美转发。