C++11右值引用和移动构造函数详解

  • Post author:
  • Post category:其他

对象的拷贝控制

C++11之前,对象的拷贝控制由三个函数决定:拷贝构造函数(Copy Constructor)、拷贝赋值运算符(Copy Assignment operator)和析构函数(Destructor)。

C++11之后,新增加了两个函数:移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment operator)。

 

左值与右值的区别

能出现在赋值号左边的表达式称为“左值”,不能出现在赋值号左边的表达式称为“右值”。一般来说,左值是可以取地址的,右值则不可以。
非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。一般的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。

C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;

例如:

#include <iostream>
using namespace std;

int main()
{
	int num = 10;
	//int && a = num;  //右值引用不能初始化为左值
	int && a = 10;
	a = 100;
	cout << a << endl;//输出100

	system("pause");
	return 0;
}

为什么引入移动构造函数?

引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。 
 

这里引入了移动语义,所谓移动语义(Move语义),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

移动语义的具体实现就是C++移动构造函数。

class A {
public:
	int x;
    //构造函数
	A(int x) : x(x)
	{
		cout << "Constructor" << endl;
	}

    //拷贝构造函数
	A(A& a) : x(a.x)
	{
		cout << "Copy Constructor" << endl;
	}

    //移动构造函数
	A(A&& a) : x(a.x)
	{
		cout << "Move Constructor" << endl;
	}
};

先来一个简单的例子:

#include <iostream>
using namespace std;

class A {
public:
	int x;
	A(int x) : x(x)
	{
		cout << "Constructor" << endl;
	}
	A(A& a) : x(a.x)
	{
		cout << "Copy Constructor" << endl;
	}
	A& operator=(A& a)
	{
		x = a.x;
		cout << "Copy Assignment operator" << endl;
		return *this;
	}
	A(A&& a) : x(a.x)
	{
		cout << "Move Constructor" << endl;
	}
	A& operator=(A&& a)
	{
		x = a.x;
		cout << "Move Assignment operator" << endl;
		return *this;
	}
};


int main()
{
	A a(1);
	A b = a;
	A c(a);
	b = a;
	A e = move(a);

	system("pause");
	return 0;
}

输出结果:

Constructor
Copy Constructor
Copy Constructor
Copy Assignment operator
Move Constructor

A a(1),调用构造函数。
A b = a,创建新对象b,使用a初始化b,因此调用拷贝构造函数。
A c(a),创建新对象c,使用a初始化c,因此调用拷贝构造函数。
b = a,使用a的值更新对象b,因为不需要创建新对象,所以调用拷贝赋值运算符。
A e = move(a),创建新对象e,使用a的值初始化e,但调用move(a)将左值a转化为右值,所以调用移动构造函数。

再来看一个例子:

#include <iostream>
#include <string>
#include <cstring>
using namespace std;

class String
{
public:
	char* str;
	String() : str(new char[1])
	{
		str[0] = 0;
	}

	// 构造函数
	String(const char* s)
	{
		cout << "调用构造函数" << endl;
		int len = strlen(s) + 1;
		str = new char[len];
		strcpy_s(str, len, s);
	}

	// 复制构造函数
	String(const String & s)
	{
		cout << "调用复制构造函数" << endl;
		int len = strlen(s.str) + 1;
		str = new char[len];
		strcpy_s(str, len, s.str);
	}

	// 复制赋值运算符
	String & operator = (const String & s)
	{
		cout << "调用复制赋值运算符" << endl;
		if (str != s.str)
		{
			delete[] str;
			int len = strlen(s.str) + 1;
			str = new char[len];
			strcpy_s(str, len, s.str);
		}
		return *this;
	}

	// 移动构造函数
	// 和复制构造函数的区别在于,其参数是右值引用
	String(String && s) : str(s.str)
	{
		cout << "调用移动构造函数" << endl;
		s.str = new char[1];
		s.str[0] = 0;
	}

	// 移动赋值运算符
	// 和复制赋值运算符的区别在于,其参数是右值引用
	String & operator = (String && s)
	{
		cout << "调用移动赋值运算符" << endl;
		if (str != s.str)
		{
			// 在移动赋值运算符函数中没有执行深复制操作,
			// 而是直接将对象的 str 指向了参数 s 的成员变量 str 指向的地方,
			// 然后修改 s.str 让它指向别处,以免 s.str 原来指向的空间被释放两次。
			str = s.str;
			s.str = new char[1];
			s.str[0] = 0;
		}
		return *this;
	}

	// 析构函数
	~String()
	{
		delete[] str;
	}
};

template <class T>
void MoveSwap(T & a, T & b)
{
	T tmp = move(a);  //std::move(a) 为右值,这里会调用移动构造函数
	a = move(b);  //move(b) 为右值,因此这里会调用移动赋值运算符
	b = move(tmp);  //move(tmp) 为右值,因此这里会调用移动赋值运算符
}

template <class T>
void Swap(T & a, T & b) 
{
	T tmp = a;  //调用复制构造函数
	a = b;  //调用复制赋值运算符
	b = tmp;  //调用复制赋值运算符
}

int main()
{
	String s;
	// 如果没有定义移动赋值运算符,则会导致复制赋值运算符被调用,引发深复制操作。
	s = String("this");  //调用移动赋值运算符
	cout << "print " << s.str << endl;
	String s1 = "hello", s2 = "world";
	//MoveSwap(s1, s2);  //调用一次移动构造函数和两次移动赋值运算符
	Swap(s1, s2);//调用一次复制构造函数,两次复制赋值运算符
	cout << "print " << s2.str << endl;

	system("pause");
	return 0;
}

当执行MoveSwap函数时,打印如下:

当执行Swap函数时,打印如下:


s = String(“this”),如果没有定义移动赋值运算符,则会导致复制赋值运算符被调用,引发深复制操作。临时无名变量String("this")是右值,因此在定义了移动赋值运算符的情况下,会导致移动赋值运算符被调用。移动赋值运算符使得 s 的内容和 String(“this”) 一致,然而却不用执行深复制操作,因而效率比复制赋值运算符高。

虽然移动赋值运算符修改了临时变量 String(“this”),但该变量在后面已无用处,因此这样的修改不会导致错误。

MoveSwap函数

T tmp = move(a),使用了 C++ 11 中的标准模板 move。move 能接受一个左值作为参数,返回该左值的右值引用。因此本行会用以右值引用作为参数的移动构造函数来初始化 tmp。该移动构造函数没有执行深复制,将 tmp 的内容变成和 a 相同,然后修改 a。由于调用 MoveSwap 本来就会修改 a,所以 a 的值在此处被修改不会产生问题。

a = move(b)和b = move(tmp)调用了移动赋值运算符,在没有进行深复制的情况下完成了 a 和 b 内容的互换。

Swap 函数

Swap 函数执行期间会调用一次复制构造函数,两次复制赋值运算符,即一共会进行三次深复制操作。而利用右值引用,使用 MoveSwap,则可以在无须进行深复制的情况下达到相同的目的,从而提高了程序的运行效率。

Move 语义

std::move 是获得右值的方式,通过 move 可以将左值转为右值。

在 C++11,一个 std::vector 的 “move 构造函数” 对某个 vector 的右值引用,可以单纯地从右值复制其内部 C-style 数组的指针到新的 vector,然后留下空的右值。

#include <iostream>
#include <memory>
using namespace std;

int main()
{
	unique_ptr<string> pa(new string("CHN"));
	//unique_ptr没有use_count()方法
	unique_ptr<string> pb(new string("USA"));

	// std::move 是获得右值的方式
	// 通过move可以将把左值转换为右值,同时清空右值
	pb = move(pa);
	//pb=pa;//错误,不能直接用等于号
	if (pa == nullptr)
	{
		cout << "pa现在为空" << endl;
	}

	system("pause");
	return 0;
}

输出结果:

pa现在为空

 


版权声明:本文为qq826364410原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。