您的位置:首页 > 其它

新概念“右值引用”和 ”移动构造函数”是怎么回事?

2017-01-13 17:05 501 查看

左值与右值

每一个C++表达式,要么是左值(lvalue),要么是右值(rvalue)。左值是生存期超过此表达式的对象,所有分配了名称的变量(包括const类型的变量)都是左值;右值基本都是临时变量,只在该表达式有效,它主要包括以下一些情况:

数值型的常量表达式,如:1+2。

字面字符串(Literal),如:”hello”。

在表达式中临时构造的对象。

返回类型非引用的函数的返回值。

左值引用与右值引用

在以前的C++中,引用操作符&只能对左值表达式使用,不能对右值表达式使用,如果非要使用引用指向一个右值表达式,只能用const修饰,如:

[cpp] view plain copy
int  b = 1;
int  &r1 = b;
int  &r2 = 1;             //非法语句,非const引用不能指向右值表达式
const int  &r3 = 1;


如果想将一个右值表达式作为参数传入函数,则该参数的形参类型必须为一个常量引用,实参在函数内部是无法区分右值引用与常引用的,因此右值表达式在函数内部是无法被修改的,如:

`[cpp] view plain copy

void DoSomething(int &x)

{

x = x + 1;

printf(“%d/n”,x);

}

int main()

{

int x = 1;

DoSomething(x); // 编译通过,x变量为左值,函数参数为左值引用?

DoSomething(1); // 编译不通过,1为右值,不能赋3给左值引用

return 0;

}

上`面这种情况在以前的C++语言中是无法改进,如果非要传入一个右值,只能重新写一个其他名称的函数。

如果重载函数签名为:

引用的函数区分而报错

` void DoSomething(int x);      //C++编译器会由于无法与参数为左值`


如果重载函数签名为:

void DoSomething(const int& x);  //const int& x = 1是合法的,这样虽然可以接受右值参数,但却无法在再修改x的值


有人可能会想,真的有人会需要像上面这样使用引用吗?是的,通常情况下我们也许不会,但是在模板类中,一个对象经过几重传递后,便极有可能发现这样的情况,而且我们有时会希望能够在函数改变右值实参的值,以提升效率。

为了解决这些问题,在VC++ 2010以后,C++支持一种新的语意:右值引用符“&&”(注意,中间不能有空格),它将允许我们对右值进行操作。现在我们可以重载一个参数为右值引用函数:

[cpp] view plain copy
void DoSomething(int&& x)
{
x = x + 1;
printf("%d/n",x);
}


这样,前面的代码便可以编译通过,当参数为一个右值时,会调用void DoSomething(int&& x)进行处理。

最好不要连续使用多个将左值引用“&”或右值引用“&&”,也不建议将二者混合使用,它们连在一起时会发生一定的退化:

展开的类型 退化后的类型

T& & T&

T& && T&

T&& & T&

T&& && T&&

复用右值对象减少内存分配次数

右值引用最大作用在于它可以对一些临时对象动态分配的资源进行处理,减少二次拷贝,显著提高效率,例如下面的场景:

string s = string("h") + "e" + "ll" + "o";


根据前面的分析,我们知道:string(“h”)其实是构造一个std::string的右值对象,它在内部会动态分配一些内存空间,以容纳字符数组。在以前C++的STL中,string的operator+可能存在以下几种重载(下面是简化过的函数形式,其实在stl中string是basic_string的一种模板):

[cpp] view plain copy
string operator+(const string& s1, const& string& s2);
string operator+(const string& s1, const char * s2);
string operator+(cons char * s1, const& string& s2);


很明显,因为string(“h”)是一个右值,只能适用上面的第二种重载函数,但是这样s1中的内容便不能被修改了,当它再加上一个字符串时,即使s1的剩余容量足够容纳下s2中的内容,也只能重新分配内存空间,然后将s1、s2中的内容拷贝进去,原来s1中分配的空间被废弃,随着右值对象的析构被回收。

当我们能够区分右值引用与常量引用时,便可以新增一种重载来减少内存的分配次数,如:

[cpp] view plain copy
string operator+(string && s1, const string& s2);     // 右值字符串加上一个常量字符串
string operator+(const string & s1, string&& s2);     // 常量字符串加上一个右值字符串
string operator+(string && s1, const char* s2);       // 右值字符串加上一个常量字符串
string operator+(const char* s1, string&& s2);        // 常量字符串加上一个右值字符串
string operator+(string && s1, string&& s2);          // 右值字符串加上一个右值字符串


当s1是一个右值引用时,将s2添加到自身末尾;当s1的剩余容量能够容纳下s2时,可以减少一次内存分配,这还不是结束,因为它返回的对象又是一个右值对象,当它还要加下一个字符串时,又可能减少一次内存分配。

移动构造函数与赋值

上面的例子中,是一般函数区分了常引用与右值引用后对效率的提升作用。而VC2010之后,新增了一种迁移语义(Move Semantic),允许为类编写迁移构造函数(Move Constructor),与重载迁移赋值号(Move Assignment)。它们与拷贝构造函数及缺省赋值号类似,但是拷贝构造函数与赋值函数的参数是一个常引用,而迁移构造函数与迁移赋值的参数是一个右值引用,且编译器不会自动生成迁移构造函数与迁移赋值号。MSDN上有一个非常好的例子:

[cpp] view plain copy
class MemoryBlock
{
public:

// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In MemoryBlock(size_t). length = "
<< _length << "." << std::endl;
}

// Destructor.
~MemoryBlock()
{
std::cout << "In ~MemoryBlock(). length = "
<< _length << ".";

if (_data != NULL)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}

std::cout << std::endl;
}

MemoryBlock(MemoryBlock&& other)
: _data(NULL)
, _length(0)
{
std::cout << "In MemoryBlock(MemoryBlock&&). length = "
<< other._length << ". Moving resource." << std::endl;

// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;

// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = NULL;
other._length = 0;
}

// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
std::cout << "In operator=(MemoryBlock&&). length = "
<< other._length << "." << std::endl;

if (this != &other)
{
// Free the existing resource.
delete[] _data;

// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;

// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = NULL;
other._length = 0;
}
return *this;
}

// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: _length(other._length)
, _data(new int[other._length])
{
std::cout << "In MemoryBlock(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;

std::copy(other._data, other._data + _length, _data);
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In operator=(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;

if (this != &other)
{
// Free the existing resource.
delete[] _data;

_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}

// Retrieves the length of the data resource.
size_t Length() const
{
return _length;
}

private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};

int main()
{
vector<MemoryBlock> v;
v.reserve(100);
v.push_back(MemoryBlock(25));
v.push_back(MemoryBlock(75));
return 0;
}


在上面的例子中,往vector中添加的MemoryBlock对象是一个右值,它在构造时分配了内存空间,当它被插入数组时,数组中会调用迁移构造函数重新构造一个MemoryBlock对象,但是这时它并不新分配内存,而直接“剥夺”了右值引用参数中已分配了的内存空间作为自己的缓冲,同时将右值的缓冲地址置为空,使其析构时不回收内存,轻松实现了资源所有权的转移。

但是如果使用拷贝构造函数,则因为函数内不能修改常引用参数中的成员,所以只能重新申请一块内存,然后将参数中的内容复制过来,而参数中原有的内存在临时对象被析构时回收,明显多了一次无意义的内存分配与复制。由此可见,使用好迁移构造函数可以显著减少资源分配次数,提升程序效率。

另外,有时为了利用右值引用的这些特性,可以将左值引用转换为右值引用。如:

[cpp] view plain copy
int main()
{
MemoryBlock block(100);   // block的初始容量为100
v.pusk_back(static_cast<MemoryBlock&&>(block));   // 此后block的容量为0
}


全文转载于:

http://blog.csdn.net/icefireelf/article/details/5724392
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: