文章目录

C++智能指针循环引用导致内存泄漏被忽视的场景

发布于 2026-06-08 09:50:15 · 浏览 8 次 · 评论 0 条

C++智能指针循环引用导致内存泄漏被忽视的场景

你是否遇到过程序运行一段时间后,内存占用持续攀升,但检查所有 new 操作都配有 delete,却找不到泄漏点?一个隐蔽的元凶可能是智能指针的循环引用。本文将直接指导你识别、分析并修复这类问题。

场景一:理解问题根源

在使用 std::shared_ptr 时,它通过引用计数来管理对象生命周期。当两个或多个对象相互持有指向对方的 shared_ptr 时,就会形成循环。每个对象的引用计数都无法降至零,导致它们永远不会被销毁,内存就这样悄无声息地泄漏了。

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> partner; // 指向伙伴的指针
    ~Node() { std::cout << “Node destroyed” << std::endl; }
};

int main() {
    // 创建两个节点
    std::shared_ptr<Node> nodeA = std::make_shared<Node>();
    std::shared_ptr<Node> nodeB = std::make_shared<Node>();

    // 互相指向对方,形成循环引用
    nodeA->partner = nodeB;
    nodeB->partner = nodeA;

    // 函数结束时,nodeA 和 nodeB 离开作用域
    // 它们各自的引用计数从 1 降为 1(因为被对方持有),不会调用析构函数
    return 0;
}

这段代码的输出不会显示 “Node destroyed”。对象 nodeAnodeB 互相“抓着”对方,导致引用计数永远为 1,它们和它们占用的内存就此滞留。

场景二:在更复杂的架构中识别循环

循环引用往往隐藏在设计复杂的系统中,例如观察者模式或双向链表。请检查你的代码中是否存在以下模式:

  1. 检查类成员变量是否包含 std::shared_ptr
  2. 分析这些 shared_ptr 指向的对象类型。
  3. 判断指向的对象是否也持有回指向原对象的 shared_ptr

一个典型的、容易忽视的场景是观察者模式中的“宿主”和“观察者”互相引用。

class Event; // 前向声明

class Observer {
public:
    std::shared_ptr<Event> observed_event; // 观察者关注某个事件
    ~Observer() { std::cout << “Observer destroyed” << std::endl; }
};

class Event {
public:
    std::vector<std::shared_ptr<Observer>> observers; // 事件持有所有观察者的引用
    ~Event() { std::cout << “Event destroyed” << std::endl; }
};

void setup() {
    auto event = std::make_shared<Event>();
    auto observer = std::make_shared<Observer>();

    // 互相引用:观察者 -> 事件,事件 -> 观察者
    observer->observed_event = event;
    event->observers.push_back(observer);

    // 当 event 和 observer 离开作用域,析构函数同样不会被调用。
}

场景三:使用工具发现泄漏

当怀疑有内存泄漏时,不要仅凭肉眼扫描代码。使用工具进行验证。

方法一:使用 valgrind (Linux/macOS)
这是最权威的工具之一。

# 编译你的程序,确保包含调试信息(-g)
g++ -g your_program.cpp -o your_program
# 运行内存检测
valgrind --leak-check=full ./your_program

在输出中,查找以 “definitely lost” 或 “possibly lost” 开头的行,这些就是确认的内存泄漏。堆栈跟踪会指向创建这些未释放对象的位置。

方法二:自定义分配器/重载 new/delete (通用但繁琐)
你可以在代码中重载全局 newdelete 运算符,记录所有内存分配和释放。在程序退出时,检查哪些分配没有对应的释放。这种方法灵活但实现复杂,通常用于开发专门的调试版本。

解决方案:打破循环引用

根本原则:在双向关系中,至少一端应该使用 std::weak_ptrweak_ptr 是一种“弱引用”,它不增加对象的引用计数,因此不会阻止对象被销毁。

#include <iostream>
#include <memory>
#include <vector>

class Observer; // 前向声明

class Event {
public:
    // 事件持有观察者的“弱引用”
    std::vector<std::weak_ptr<Observer>> observers;
    void notify();
    ~Event() { std::cout << “Event destroyed” << std::endl; }
};

class Observer : public std::enable_shared_from_this<Observer> {
public:
    // 观察者仍然需要知道事件,可以持有强引用
    std::shared_ptr<Event> observed_event;

    void subscribe(std::shared_ptr<Event> event) {
        observed_event = event;
        // 将自身的弱引用注册给事件,避免循环
        event->observers.push_back(shared_from_this());
    }

    void respond() { std::cout << “Observer is responding.” << std::endl; }
    ~Observer() { std::cout << “Observer destroyed” << std::endl; }
};

void Event::notify() {
    // 遍历弱引用列表
    for (auto it = observers.begin(); it != observers.end(); /* 注意迭代器在循环内管理 */) {
        // 尝试将 weak_ptr “升级” 为 shared_ptr
        if (std::shared_ptr<Observer> observer = it->lock()) {
            observer->respond(); // 对象仍然存活,安全调用
            ++it;
        } else {
            // 升级失败,说明观察者已被销毁,从列表中移除这个失效的引用
            it = observers.erase(it);
        }
    }
}

int main() {
    auto event = std::make_shared<Event>();
    auto observer = std::make_shared<Observer>();

    observer->subscribe(event);

    event->notify(); // 正常工作

    // 当 main 函数结束时,event 和 observer 的强引用计数均可降为 0
    // 它们会按正确的顺序被销毁,输出析构信息
    return 0;
}

执行上述修复后的代码,你将看到 “Observer destroyed” 和 “Event destroyed” 的输出,表明对象被正确释放。

核心规则总结

  1. 建立这样的编码习惯:当两个对象A和B需要互相引用时,默认为其中一方(通常是“子”对象或“依赖方”)使用 std::weak_ptr
  2. 访问 weak_ptr 指向的对象前,调用.lock() 方法,并检查返回的 shared_ptr 是否有效(不为空)。
  3. 利用 std::enable_shared_from_this 让对象安全地将自身的 shared_ptr 转换为 weak_ptr 进行传递。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文