移动构造函数详解
概念
移动构造函数是c++11引入的特性,用于将资源从一个对象高效的转移到另一个对象,避免不必要的拷贝动作。移动构造函数的基本语法如下:
class MyClass{
private:
std::vector<int> data;
public:
//移动构造函数
MyClass(MyClass&& other) noexcept; //注意移动构造函数的形参类型为&&-右值引用
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
std::cout << "Move assignment: Before move, other.data.size() = "
<< other.data.size() << std::endl;
if (this != &other) {
data = std::move(other.data);
}
std::cout << "Move assignment: After move, other.data.size() = "
<< other.data.size() << std::endl;
return *this;
}
};
这里的重点是“用于将资源从一个对象高效的转移到另一个对象,避免不必要的拷贝动作”。
移动构造函数大多时候与std::move一起使用,因为std::move会将参数值转换为一个右值引用,std::move本身不会发生发生拷贝或者移动的动作,要清楚地认识到这一点。
至于“如何将资源从一个对象高效的转移到另外一个对象,避免不必要的拷贝动作”这句话就是由类的移动构造函数或者移动赋值运算符实现的。具体的对比参见下面的例子:
//传统的拷贝动作
class StringWrapper {
private:
char* data;
size_t length;public:
// 构造函数
StringWrapper(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}// 拷贝构造函数
StringWrapper(const StringWrapper& other) {
length = other.length;
data = new char[length + 1]; // 分配新内存
strcpy(data, other.data); // 复制数据
}~StringWrapper() {
delete[] data;
}
};void demonstrate_copy() {
StringWrapper str1("Hello"); // 原始对象
StringWrapper str2(str1); // 拷贝构造 - 分配新内存并复制数据
}
//移动操作的优化
class StringWrapper {
private:
char* data;
size_t length;public:
// 构造函数
StringWrapper(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
std::cout << "Constructed with " << data << std::endl;
}// 移动构造函数
StringWrapper(StringWrapper&& other) noexcept {
// 直接获取other的资源
data = other.data;
length = other.length;
// 将other置为安全的状态
other.data = nullptr;
other.length = 0;
std::cout << "Moved " << data << std::endl;
}// 移动赋值运算符
StringWrapper& operator=(StringWrapper&& other) noexcept {
if (this != &other) {
// 释放自己的资源
delete[] data;
// 获取other的资源
data = other.data;
length = other.length;
// 将other置为安全的状态
other.data = nullptr;
other.length = 0;
}
return *this;
}~StringWrapper() {
delete[] data;
}void print() const {
if (data) {
std::cout << "String: " << data << std::endl;
} else {
std::cout << "Empty string" << std::endl;
}
}
};
通过对比上面的例子,可以看到对于传统的拷贝比如拷贝构造函数对于一个字符串会重新申请一篇内存,然后将字符串源对象拷贝到新的内存中,这样在内存中就存在了2个同样字符串;移动构造函数或者移动赋值运算符不一样,他只会将原有的字符串对应该指针赋值给新对象,并且将源对象中的指针设置为空,自始至终字符串在内存中仅存在一份。
默认的移动构造函数或者移动赋值运算符实现
编译器是否生成默认的移动构造函数,存在先决条件
- 没有用户声明的拷贝操作(拷贝构造函数和拷贝赋值运算符)
- 没有用户声明的移动操作(移动构造函数和移动赋值运算符)
- 没有用户声明的析构函数
默认的构造函数实现如下所示:
class DefaultMoveExample {
public:
int x; // 基本类型
std::string str; // 具有移动语义的类型
int* ptr; // 原始指针
std::unique_ptr<int> uptr; // 智能指针
std::vector<int> vec; // 容器// 默认移动构造函数等价于:
DefaultMoveExample(DefaultMoveExample&& other) noexcept
: x(other.x) // 基本类型:直接拷贝
, str(std::move(other.str)) // 调用 string 的移动构造函数
, ptr(other.ptr) // 指针:直接拷贝指针值
, uptr(std::move(other.uptr)) // 调用 unique_ptr 的移动构造函数
, vec(std::move(other.vec)) // 调用 vector 的移动构造函数
{
// other.ptr 仍然指向原来的内存位置(可能导致双重释放!)
// other.x 保持原值
// other.str, other.uptr, other.vec 处于有效但未指定状态
}
};
可以看到采用默认的移动构造函数是存在风险的,如果用户确实需要有移动操作的需求,最好显式的将移动构造函数或者移动赋值运算符定义出来。
注意事项
- std::move仅仅是将输入的参数转换为右值引用,并无实际的拷贝或者移动操作
- 对象资源的转移是由类对象实现的,而非std::move实现的,例如std::vector<int> data = std::move(other.data); std::string str = std::move(other.str)都是有std::vector和std::string类实现的,而非std::move实现的。