【ChernoC++笔记】移动语义
❓为什么使用移动语义(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。如果不进行置空,打印如下,可以看到PrintName时由于旧实例已经被销毁,无法打印出新实例的m_Data:Created! Moved! Destroyed! Destroyed!
- 这么做实际上是接管了旧的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!
🌟总结#
在通过复制构造函数来实例化对象的时候,每次构造都需要复制大量数据来传递对象,对程序性能造成极大影响。移动语义可以将临时对象的内存直接转移,而不是复制。
- 复制构造函数:形参是一个左值引用,也就是实参必须是左值,在复制构造函数中一般进行的是深复制,即在不能破坏实参对象的前提下复制目标对象,一般要重新分配内存。
- 移动构造函数:形参是一个右值引用,接受一个右值,通过转移实参对象的数据以构造目标对象,也就是说实参对象是会被修改的。
一般通过指针替换操作实现,然后要置空实参对象,以保证实参对象析构的时候不会影响目标对象内存。在移动构造函数中使用了指针转移的方式构造目标对象,整个程序的运行效率得到大幅提升。
*移动语义的风险来自异常。在一个移动构造函数中,如果当一个对象的资源移动到另一个对象时发生了异常,也就是说对象的一部分发生了转移而另一部分没有,这就会造成源对象和目标对象都不完整的情况发生,这种情况的后果是无法预测的。所以在编写移动语义的函数时建议确保函数不会抛出异常,与此同时,如果无法保证移动构造函数不会抛出异常,可以使用noexcept说明符限制该函数。这样当函数抛出异常的时候,程序不会再继续执行而是调用terminate中止执行以免造成其他不良影响。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!