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”。对象 nodeA 和 nodeB 互相“抓着”对方,导致引用计数永远为 1,它们和它们占用的内存就此滞留。
场景二:在更复杂的架构中识别循环
循环引用往往隐藏在设计复杂的系统中,例如观察者模式或双向链表。请检查你的代码中是否存在以下模式:
- 检查类成员变量是否包含
std::shared_ptr。 - 分析这些
shared_ptr指向的对象类型。 - 判断指向的对象是否也持有回指向原对象的
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 (通用但繁琐)
你可以在代码中重载全局 new 和 delete 运算符,记录所有内存分配和释放。在程序退出时,检查哪些分配没有对应的释放。这种方法灵活但实现复杂,通常用于开发专门的调试版本。
解决方案:打破循环引用
根本原则:在双向关系中,至少一端应该使用 std::weak_ptr。weak_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” 的输出,表明对象被正确释放。
核心规则总结
- 建立这样的编码习惯:当两个对象A和B需要互相引用时,默认为其中一方(通常是“子”对象或“依赖方”)使用
std::weak_ptr。 - 访问
weak_ptr指向的对象前,调用其.lock()方法,并检查返回的shared_ptr是否有效(不为空)。 - 利用
std::enable_shared_from_this让对象安全地将自身的shared_ptr转换为weak_ptr进行传递。

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