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

循环中std::unique_ptr使用是否正确?疑似内存泄漏排查

循环中std::unique_ptr引发内存泄漏问题(VS2019/MSVC/C++17环境)

问题描述

在Visual Studio 2019、MSVC、C++17环境下,循环中使用std::unique_ptr时,每次循环结束堆内存都会小幅增长。通过VS调试工具排查,发现unique_ptr对象task似乎未被释放,引发内存泄漏。原本以为unique_ptr会在代码块(循环或try-catch结束)时自动释放内存,对此存在疑惑。

更新(2023/10/6)

  • 已更新代码如下。
  • 尝试制作最小复现示例但未成功,该示例代码无内存泄漏,仅类头文件和类名与实际代码不同。
  • 调试工具截图显示_container_proxy[]、TaskFactory[]和TaskImplA[]持续堆积。
  • 即使注释task->execute()的执行逻辑,内存泄漏仍会发生。

相关代码

#include <iostream>
#include <future>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#include <ws2tcpip.h>

class MessageTask {
public:
    MessageTask() {};
    MessageTask(char*& message) {};
    virtual ~MessageTask() {};
    void execute() {
        decide_task();
        do_task();
    }
    virtual void decide_task() = 0;
    virtual void do_task() = 0;
};

class TaskImpl : public MessageTask{
private:
    char* message;
public:
    TaskImpl() {};
    TaskImpl(char*& message) { this->message = message; };
    ~TaskImpl() {};
    void decide_task() { std::cout << message << std::endl;  return; };
    void do_task() { return; };
};

class TaskFactory {
public:
    TaskFactory() {};
    ~TaskFactory() {};
    std::unique_ptr<MessageTask> create_message_task(char*& message, int& sender) {
        if (sender == 2000) {
            return std::make_unique<TaskImpl>(message);
        }
        return nullptr;
    };
};

class UdpComm {
private:
    SOCKET m_socket;
    struct sockaddr_in m_address;
    WSADATA m_wsadata;

public:
    UdpComm() {};
    ~UdpComm() {};
    struct sockaddr_in m_address_send_from;

    void initialize_receiver() {
        WSACleanup();
        int startup_result = WSAStartup(MAKEWORD(2, 0), &this->m_wsadata);
        this->m_socket = socket(AF_INET, SOCK_DGRAM, 0);
        this->m_address.sin_family = AF_INET;
        this->m_address.sin_port = htons(5001);
        this->m_address.sin_addr.S_un.S_addr = INADDR_ANY;
        int wsa_result = bind(this->m_socket, (struct sockaddr*)&this->m_address, sizeof(this->m_address));
    };

    int receive_sync(char* buffer_ptr, const size_t buffer_size, size_t& rx_size)
    {
        int sockaddr_in_size = sizeof(this->m_address_send_from);
        int receive_result = recvfrom(this->m_socket, (char*)buffer_ptr, (int)buffer_size, 0, (struct sockaddr*)&m_address_send_from, &sockaddr_in_size);
        rx_size = (size_t)receive_result;
        return 0;
    }
};

class Receiver {
public:
    int udp_receive(UdpComm& uc_rcv) {
        while (true)
        {
            try
            {
                char buffer[1024];
                char* buffer_ptr = buffer;
                size_t buffer_size = sizeof(buffer);
                size_t rx_size = 0;
                uc_rcv.receive_sync(buffer_ptr, buffer_size, rx_size);
                int port = ntohs(uc_rcv.m_address_send_from.sin_port);
                auto factory = std::make_unique<TaskFactory>();
                auto task = factory->create_message_task(buffer_ptr, port);
                task->execute();
            }
            catch (const std::exception& ex)
            {
                std::cerr << "[ERROR] " << ex.what() << std::endl;
            }
        }
    }
};

int main() {
    auto uc_rcv = std::make_unique<UdpComm>();
    uc_rcv->initialize_receiver();
    Receiver receiver;
    receiver.udp_receive(*uc_rcv);
}

问题排查与修复建议

核心排查点

  1. WSA初始化逻辑错误
    UdpComm::initialize_receiver中先调用WSACleanup()再执行WSAStartup,违反了WSA使用规范:WSAStartup与WSACleanup必须成对调用,且WSACleanup应仅在程序退出前调用一次。多次无配对的WSAStartup会导致系统资源泄漏,可能被VS诊断工具误判为堆内存泄漏。

  2. 派生类析构的资源释放
    虽然MessageTask的析构是虚函数,但实际项目中的TaskImplA如果持有动态分配的资源(如new创建的缓冲区、对象),必须在析构函数中显式释放。若TaskImplA的析构未清理所有堆资源,会直接导致内存泄漏。

  3. 临时对象的不必要分配
    每次循环都用std::make_unique<TaskFactory>创建工厂对象,虽然unique_ptr会自动销毁它,但频繁的堆分配可能导致内存池缓存,被工具误判为泄漏。可以将工厂对象移到循环外,减少不必要的内存操作。

  4. VS工具的误判可能性
    VS的内存诊断工具有时会将C++标准库的内存池缓存当作泄漏。可以使用_CrtDumpMemoryLeaks()在程序退出时手动检测,或者通过“内存快照对比”功能,精准定位未释放的对象。

  5. 悬空指针的间接影响
    当前代码中TaskImpl保存了栈缓冲区buffer的指针,虽然task在循环结束时销毁不会直接泄漏,但如果实际项目中task被异步持有(如放到std::future或线程中),会导致悬空指针,可能引发其他内存异常。

修复步骤

  1. 修正WSA初始化:移除initialize_receiver中的WSACleanup(),在UdpComm的析构函数中调用一次WSACleanup(),确保与WSAStartup配对。
  2. 检查TaskImplA的析构:确保所有动态分配的资源都被释放,比如用delete[]清理new char[]创建的缓冲区。
  3. 优化工厂对象创建:将TaskFactory的实例移到循环外,避免每次循环都进行堆分配。
  4. 精准定位泄漏:使用VS的内存快照工具,对比两次循环后的内存状态,确认_container_proxy[]、TaskFactory[]的来源,排查标准库容器或自定义对象的泄漏点。

内容的提问来源于stack exchange,提问作者Hikaru

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 11:39:52