如何避免C++类实例额外复制?修复移动语义导致的double free崩溃
问题:手动管理外部内存的RAII类因移动语义导致double free崩溃
我有一个Child类,持有外部管理内存的引用(实际场景为通过Vulkan调用创建/销毁的Vulkan对象,示例用手动内存分配模拟)。希望借助RAII机制管理C++对象,并在Child的析构函数中释放外部分配的内存。实际代码中Parent类包含多个Child实例,示例也包含该类。
以下是最小可复现示例(MRE):
#include <cstdlib> #include <memory> #include <iostream> class Child { public: Child(void * memoryRegion): mMemoryRegion(memoryRegion){} ~Child(){ std::cout << "Destroying" << std::endl; free(mMemoryRegion); } Child(const Child&) = delete; Child(Child&&) = default; Child& operator=(const Child&) = delete; Child& operator=(Child&&) = default; private: void * mMemoryRegion; }; Child createChild(){ void * memory = malloc(100); return Child(memory); } class Parent { public: Parent (Child &c1) : child1(std::move(c1)){} private: Child child1; }; std::unique_ptr<Parent> createParent(){ Child c1 = createChild(); return std::make_unique<Parent>(c1); } int main(){ auto parent = createParent(); return 0; }
程序运行崩溃,报错信息如下:
Destroying Destroying free(): double free detected in tcache 2 [1] 3568506 IOT instruction ./main
确定是移动语义使用有误,但尚未找出问题所在。想知道正确的实现方式,同时好奇不使用std::unique_ptr是否能修复该问题。
问题原因分析
- 默认生成的移动构造/赋值函数仅做浅拷贝指针,移动操作完成后,原对象的
mMemoryRegion仍指向原内存地址。当原对象和移动后的对象先后析构时,会对同一块内存执行两次free,触发double free错误。 createParent函数中传递的是Child c1的左值引用,即便使用了std::move,原对象c1在函数结束时仍会正常析构,而Parent中的child1也会在Parent生命周期结束时释放同一块内存,导致重复释放。
修复方案:不使用std::unique_ptr
需要手动实现移动构造和移动赋值运算符,在转移内存所有权后将原对象的指针置为nullptr,确保原对象析构时不会释放已转移的内存:
修改Child类的移动相关函数
class Child { public: Child(void * memoryRegion): mMemoryRegion(memoryRegion){} ~Child(){ std::cout << "Destroying" << std::endl; if (mMemoryRegion != nullptr) { // 仅当指针非空时释放 free(mMemoryRegion); } } Child(const Child&) = delete; Child& operator=(const Child&) = delete; // 手动实现移动构造函数 Child(Child&& other) noexcept : mMemoryRegion(other.mMemoryRegion) { other.mMemoryRegion = nullptr; // 转移所有权后,原对象指针置空 } // 手动实现移动赋值运算符 Child& operator=(Child&& other) noexcept { if (this != &other) { // 先释放当前对象持有的内存 if (mMemoryRegion != nullptr) { free(mMemoryRegion); } // 转移所有权 mMemoryRegion = other.mMemoryRegion; other.mMemoryRegion = nullptr; } return *this; } private: void * mMemoryRegion; };
调整Parent构造函数参数
将Parent的构造函数参数改为右值引用,更清晰地表达“转移所有权”的语义:
class Parent { public: Parent(Child&& c1) : child1(std::move(c1)){} private: Child child1; };
调整createParent函数调用方式
传递右值引用给Parent构造函数:
std::unique_ptr<Parent> createParent(){ Child c1 = createChild(); return std::make_unique<Parent>(std::move(c1)); }
修改后,c1的内存所有权被转移到Parent::child1,原c1的指针被置空,析构时不会释放内存,仅Parent中的child1会释放内存,避免了double free。
推荐方案:使用std::unique_ptr
虽然你想了解不使用它的修复方式,但std::unique_ptr可以更安全、简洁地管理内存所有权,无需手动实现移动语义,它内置了指针置空和避免重复释放的逻辑。针对malloc分配的内存,我们可以自定义删除器:
#include <cstdlib> #include <memory> #include <iostream> // 自定义删除器,用于释放malloc分配的内存 struct MallocDeleter { void operator()(void* ptr) const { free(ptr); std::cout << "Destroying" << std::endl; } }; class Child { public: // 构造时直接分配内存,用unique_ptr管理 Child() : mMemoryRegion(malloc(100), MallocDeleter()) {} // 禁用拷贝,移动操作默认生成即可 Child(const Child&) = delete; Child& operator=(const Child&) = delete; Child(Child&&) = default; Child& operator=(Child&&) = default; private: std::unique_ptr<void, MallocDeleter> mMemoryRegion; }; Child createChild(){ return Child(); } class Parent { public: Parent(Child c1) : child1(std::move(c1)) {} private: Child child1; }; std::unique_ptr<Parent> createParent(){ Child c1 = createChild(); return std::make_unique<Parent>(std::move(c1)); } int main(){ auto parent = createParent(); return 0; }
这种方式不仅避免了手动实现移动语义的繁琐,还能在复杂场景下更可靠地管理内存所有权,减少出错概率。
内容的提问来源于stack exchange,提问作者martinarroyo
相关产品推荐
相关产品推荐

