❓为什么使用移动语义(moving semantics)?
很多时候,我们需要通过复制来传递对象:
- 例如,把一个对象传递给一个函数,这个函数需要得到这个对象的所有权,我们需要在当前stack frame中构造一个一次性对象,然后复制到调用的函数中。
- 同样的,想从函数返回一个对象,需要在函数中创建那个对象,然后返回,也就是需要复制。(可以通过*返回值优化来解决)
但是有些情况下,我们不想把一个对象通过复制传递。当要传递的对象需要堆分配内存,比如一个字符串,如果需要复制,必须创建一个全新的堆分配。如果我们只是移动对象而不是复制对象,将大大地提高性能。
▶️一个拷贝构造的例子
class String {
public:
String() = default;
String(const char* string) {
printf("Created!\\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
// 将源字符串string移动到数据缓冲区m_Data
memcpy(m_Data, string, m_Size);
}
// 拷贝构造函数
String(const String& other) {
printf("Copied!\\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
~String() {
printf("Destroyed!\\n");
delete m_Data; // char是简单类型,不需要delete[]析构
}
void Print() {
for(uint32_t i = 0; i < m_Size; i++) {
printf("%c", m_Data[i]);
}
printf("\\n");
}
private:
char* m_Data; // 实际的字符串数据
uint32_t m_Size;
};
class Entity {
public:
Entity(const String& name) : m_Name(name) {}
void PrintName() {
m_Name.Print();
}
private:
String m_Name;
};
int main() {
Entity entity(String("Cherno"));
entity.PrintName();
}
运行代码打印如下:
Created! // main函数中构造String实例
Copied! // 拷贝构造entity中的String实例
Destroyed! // other实例析构
Cherno
Destroyed! // entity中的m_Name实例析构
可以看到,为了构造一个“Cherno”Entity对象,我们为它分配了两次内存:首先要在main函数的作用域创建了一个String对象(”Created!”),即第一次分配内存;再传递给Entity构造函数,使用拷贝构造函数再次分配空间,创建entity.m_Name这个String对象(”Copied!”)。
为什么不能在main函数中分配,然后直接移动到entity.m_Name中呢?➡️➡️移动语义
▶️利用移动语义来避免拷贝
- 将Entity的构造函数传入参数改为一个右值引用,以传入临时值。并且需要将name显示转换为临时值,即使用
(String&&)name或std::move(name),因为将一个临时变量绑定到右值引用上时,这个右值引用是一个左值。 - String类的move构造函数:不再需要new一个新的buffer,进行memcpy逐个复制数据。只需要给指针赋值,把新的字符串实例中创建的指针,指向
other.m_Data指向的同一块数据。 - 但是,旧的那个String实例(other)被删除后,会将它的数据带走。所以我们需要将other指向nullptr,即置空。当旧的String实例被销毁时,
delete m_Data;实际上删除了nullptr。 - 这么做实际上是接管了旧的String实例,重新连接了指针,即浅拷贝。而不是通过复制所有的数据并分配新内存来进行深拷贝。
class String {
public:
String() = default;
String(const char* string) {
printf("Created!\\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
// 将源字符串string移动到数据缓冲区m_Data
memcpy(m_Data, string, m_Size);
}
// 拷贝构造函数
String(const String& other) {
printf("Copied!\\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
// move构造函数
String(String&& other) noexcept {
printf("Moved!\\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
// 不再需要分配新的数据缓冲区
// 偷取了other的资源,放回一个空对象,析构时只delete nullptr
other.m_Data = nullptr;
other.m_Size = 0;
}
~String() {
printf("Destroyed!\\n");
delete m_Data; // char是基本类型,不需要delete[]析构
}
void Print() {
for(uint32_t i = 0; i < m_Size; i++) {
printf("%c", m_Data[i]);
}
printf("\\n");
}
private:
char* m_Data; // 实际的字符串数据
uint32_t m_Size;
};
class Entity {
public:
Entity(const String& name) : m_Name(name) {}
// 传入右值,会调用该构造函数
// Entity(String&& name) : m_Name(name) {}
// 需要将name显示转换为临时值,因为将一个临时变量绑定到右值引用上时,这个右值引用是一个左值
// Entity(String&& name) : m_Name((String&&)name) {}
// 或者,使用std::move也可以做到
Entity(String&& name) : m_Name(std::move(name)) {}
void PrintName() {
m_Name.Print();
}
private:
String m_Name;
};
int main() {
// String("Cherno")是一个临时变量,调用Entity的move构造函数
Entity entity(String("Cherno"));
entity.PrintName();
std::cin.get();
}
运行代码打印如下:
Created!
Moved! // 成功使用了move构造函数
Destroyed!
Cherno
Destroyed!