对象的拷贝控制
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现在为空