You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 15:45:56