最近接触了c++很多的概念,什么拷贝构造函数、移动构造函数、左值、右值、左值引用、右值引用等等,学习的过程好像渡劫,网上的很多博客要么就是介绍的很模糊,要么就是知识点不全。不过好在读过了这篇文章之后
https://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html
对于这些概念有了一个较为清晰的了解。下面就记录一下自己的学习历程
首先介绍一个很低效的c++程序:
#include <iostream>
using namespace std;
vector<int> doubleValues (const vector<int>& v)
{
vector<int> new_values;
new_values.reserve(v.size());
for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr )
{
new_values.push_back( 2 * *itr );
}
return new_values;
}
int main()
{
vector<int> v;
for ( int i = 0; i < 100; i++ )
{
v.push_back( i );
}
v = doubleValues( v );
}
程序把一个vector里面的所有元素都扩大一倍。
但是这个程序非常低效,首先,在函数中建立一个新的vector:new_values。接下来,会有两次拷贝:第一次拷贝是new_values的所有成员拷贝给临时的vector对象,返回这个临时的vector对象。第二次拷贝是这个临时的vector对象的值拷贝给v,相当于又要遍历一遍,有可能v还要重新分配内存空间。第一次拷贝有可能会经过编译器优化而不进行,但是第二次拷贝是不可避免的。如果我们要进行优化的话,有没有可能是这样子呢?就是临时的vector对象中,指向临时vector对象成员的指针赋值给v,这样的话,v的指针就指向了这些临时vector对象的成员,这些成员就变成了v的成员,相当于临时vector对象的成员直接移动到了v那里,就避免了临时对象的成员拷贝一份给v,提升了整个程序的效率!
c++11 的右值引用和move就是干这件事的,减少不必要的拷贝。
首先先简要说一下左值和右值的区别:
上面那篇文章里对于左值的定义是在内存中有一个半永久的位置(provides a (semi)permanent piece of memory),一个左值可以通过&获取地址。而右值不可以,例如我们定义了一个类A,A()就是一个右值。再举个例子:
string getName ()
{
return "Alex";
}
string name = getName();
上面的程序,首先通过"Alex"构造了一个临时的string对象返回,这个临时的string对象再把自己的字符成员拷贝给name对象(赋值)。name对象的赋值来源于一个临时对象,getName()是一个右值。
一般的string &a这种属于左值引用,绑定一个左值。
而string&& a属于右值引用,绑定一个右值(一般是临时对象)。
接下来介绍一下,基于左值的拷贝构造函数和基于右值的移动构造函数:
在c++11 之前,程序一般是这种写法:
class ArrayWrapper
{
public:
ArrayWrapper (int n)
: _p_vals( new int[ n ] )
, _size( n )
{}
// copy constructor
ArrayWrapper (const ArrayWrapper& other)
: _p_vals( new int[ other._size ] )
, _size( other._size )
{
for ( int i = 0; i < _size; ++i )
{
_p_vals[ i ] = other._p_vals[ i ];
}
}
~ArrayWrapper ()
{
delete [] _p_vals;
}
private:
int *_p_vals;
int _size;
};
当我通过一个ArrayWrapper对象构造一个新的ArrayWrapper对象的时候,会调用拷贝构造函数,新的ArrayWrapper对象的_p_vals指向一个数组,随后,other的_p_vals所指数组的所有元素拷贝给新的ArrayWrapper对象的_p_vals数组,从而初始化了新建的ArrayWrapper对象。这样其实是非常耗时的操作,我们有了这样的想法,如果我们之后不再使用other对象,我们可不可以把other的_p_vals数组移动给新建的ArrayWrapper对象?
所以我们有了接下来的移动构造函数:
class ArrayWrapper
{
public:
// default constructor produces a moderately sized array
ArrayWrapper ()
: _p_vals( new int[ 64 ] )
, _size( 64 )
{}
ArrayWrapper (int n)
: _p_vals( new int[ n ] )
, _size( n )
{}
// move constructor
ArrayWrapper (ArrayWrapper&& other)
: _p_vals( other._p_vals )
, _size( other._size )
{
other._p_vals = NULL;
other._size = 0;
}
// copy constructor
ArrayWrapper (const ArrayWrapper& other)
: _p_vals( new int[ other._size ] )
, _size( other._size )
{
for ( int i = 0; i < _size; ++i )
{
_p_vals[ i ] = other._p_vals[ i ];
}
}
~ArrayWrapper ()
{
delete [] _p_vals;
}
private:
int *_p_vals;
int _size;
};
当我们用一个临时的ArrayWrapper对象(右值)去构造一个新的ArrayWrapper对象的时候,会调用移动构造函数,other是临时的ArrayWrapper对象。我们我们让新对象的_p_vals指向other的数组,接着把other的_p_vals变成NULL,这就好像是other的成员移动到了新建的ArrayWrapper成员,避免了拷贝的开销。之后临时对象也不会在有用,直接销毁就好。可以说移动构造函数让成员移动到了新建的对象那里,避免了大的开销。
ps:拷贝构造和移动构造让我想起了大一的时候刚学C语言的时候,老师说函数形参尽量不要写结构体而要写结构体指针,我觉得二者有相似的地方。
但是需要注意的是,在移动构造函数中,other绑定了一个临时对象(右值),但是other是一个左值,&other是合法的,编译没问题的。
C++11中的move关键字功能就是把一个左值转换成一个右值,调用移动构造函数,move的实现用到了static_cast,这里不做过多讨论。我们只需要知道move关键字实现左值向右值转移。例如这样一段程序:
vector<string> v;
string s="abc";
v.push_back(s);
cout<<"s="<<s<<endl;
vector<string> v1;
string s1="abc";
v1.push_back(move(s1));
cout<<"s1="<<s1<<endl;
上半部分程序调用拷贝构造函数,把s的字符拷贝给新的string对象,push到v中,下半部分程序把s1转换成一个右值,调用移动构造函数,s1的字符移动到了新的string对象,所以再输出s1,s1没有字符了。
目前STL中的对象都支持移动构造和移动赋值运算符,避免了大的开销,所以开头的那个vector的例子,可以使用移动赋值运算府把临时的vector对象的成员移动到v那里。