记一次由于智能指针shared_ptr循环引用而产生的C++内存泄漏

本文通过实例解析了C++中shared_ptr因循环引用而导致的内存泄漏问题,并介绍了如何利用weak_ptr来解决该问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自从 C++ 11 以来,boost 的智能指针就被加入了 C++ 新标准之中。其中,广为人知的 shared_ptr 被用的最多,以引用计数的方式来管理指针指向资源的生命周期。看起来有了智能指针后,C++ 程序再也不用担心内存泄漏了,就可以像 Java 一样愉快的创建堆上对象了。但事实并非如此,C++ 的智能指针和 Java 的引用实现原理上有本质的区别。在“循环引用”这个问题上,Java 可以很好地处理,而C++ 的智能指针 shared_ptr 在处理“循环引用”时便捉襟见肘。

1-一次由于循环引用产生的内存泄漏

鄙人最近在写实验代码,正好使用了 shared_ptr 来管理内存,但批量运行实验的时候发生了内存泄漏,导致我只跑了十几组实验程序便崩溃了。虽然可以将原来的 x86 改成 x64 扩大地址空间来解决这个问题,但终究不是长久之计,所以必须要解决之。抽取的代码如下:

class Node {
    Node(const Point &p);
    ...
private:
    shared_ptr<Node> neighbors;
};

这个代码写得很自然,如果是写 Java 程序自然没什么问题,但在使用引用计数的 C++ 智能指针上便内存泄漏了。其中 shared_ptr<Node> neighbors; 这个声明便是万恶之源,在所有的 Node 对象创建完成之后,每个 Node 对象遍历所有的 Node 的列表,在这个列表中找到自己的邻居,并将它们的智能指针加入到自己的邻居表中,在此产生了循环引用。

2-解决方法

在怀疑 Bug 是因为 shared_ptr 导致的内存泄漏后,我写了一个 Demo,测试了一下,发现也存在同样的问题,可以确定确实是这个原因产生的内存泄漏。解决这个内存泄漏的方法就是使用 weak_ptr 来指向其邻居,这样不会增加引用计数,在需要访问其指向变量的内存时使用 lock 方法获得 shared_ptr 对象来访问之。最后程序是这样的:

class Node {
public:
    Node(const Point &p);
    ...
private:
    weak_ptr<Node> neighbors_;
};

class Simulator {
public:
    ...
private:
    shared_ptr<Node> sens_nodes_;
};
3-循环引用为什么会导致内存泄漏

自智能指针的原理说起,一个智能指针在创建一个对象的时候初始化引用计数为 1,并把自己的指针指向创建的对象。但这个引用计数在何处?在智能指针内部?非也,这个计数是一个单独的对象来实现的,如图1,当另外一个智能指针指向这个对象的时候,便找到与这个对象对应的计数对象,并加一个引用,即 use_count++。这样多个智能指针对象便可以使用相同的引用计数。

引用计数原理

而如果产生相互引用的情况,类似以下代码:

class Person {
public:
    ...
    shared_ptr<Person> best_friend;
};
Person pa = make_shared<Person>();
Person pb = make_shared<Person>();
pa->best_friend = pb;
pb->best_friend = pa;

即使 pa 和 pb 均离开作用域析构掉了,内存也不会释放。为何?在 pa 离开作用域后,pa 这个只能指针对象实际上已经死亡了,但是 pa 所指向的对象并没有死亡,因为此时 pb 中的一个只能指针指向了这个对象。在 pb 离开作用域后,pb 这个智能指针实际上也完了,但是 pb 所指向的对象并没有死亡,因为 pa 曾经指向的对象中还有着一个指向它的指针。最后,由于两个对象保存着指向对方的指针,它们的引用计数均为 1,导致了内存无法释放。
循环引用

在理解了智能指针的实现原理和循环引用导致内存泄漏的原理后,使用 weak_ptr 这种不增加引用计数的弱指针便可以很好地解决这个问题。

### C++ `shared_ptr` 循环引用解决方案 #### 使用 `weak_ptr` 为了有效解决 `shared_ptr` 导致循环引用问题,推荐使用 `std::weak_ptr`。作为一种非拥有型指针,`std::weak_ptr` 不会增加引用计数,因此能够避免因相互持有对方而导致无法释放的情况发生[^1]。 当两个或更多对象通过 `shared_ptr` 形成互相持有的关系时,它们之间的引用计数永远不会降为零,这就会造成内存泄漏。引入 `weak_ptr` 后,在某些情况下可以只保持弱引用而不需要实际控制对象的生命期,从而打破了这种恶性循环。 具体来说: - **创建弱指针**:可以从现有的 `shared_ptr` 创建一个新的 `weak_ptr` 实例。 ```cpp std::shared_ptr<Node> nodeA(new Node()); std::weak_ptr<Node> weakNode(nodeA); ``` - **检测有效性并获取强指针**:在真正需要访问目标对象的时候,可以通过调用 `lock()` 函数尝试将其转换回 `shared_ptr` 来确认对象是否依然存在。如果原对象已经被销毁,则 `lock()` 返回的是一个空的 `shared_ptr`。 ```cpp auto lockedPtr = weakNode.lock(); // 获取临时的 shared_ptr if (lockedPtr) { // 对象还活着... } else { // 对象已死掉... } ``` 此外,还可以利用 `expired()` 成员函数快速判断当前 `weak_ptr` 所关联的对象是否已被删除而不必每次都执行开销较大的 `lock()` 操作[^2]。 对于双向链表或其他可能存在自引用结构的数据类型而言,通常建议将其中至少一侧的关系定义为 `weak_ptr` 类型,以此来预防潜在的风险。 #### 示例代码展示 下面给出一段简单的例子说明如何运用上述原理处理可能出现的循环依赖情形: ```cpp #include <iostream> #include <memory> class B; class A { public: ~A() { std::cout << "Destroying A\n"; } private: std::weak_ptr<B> b_; // 弱引用B friend void setBA(A&, const std::shared_ptr<B>&); }; void setBA(A& a, const std::shared_ptr<B>& sptr){ a.b_ = sptr; } class B { public: ~B() { std::cout << "Destroying B\n"; } private: std::shared_ptr<A> a_; friend void setAB(B&, const std::shared_ptr<A>&); }; void setAB(B& b, const std::shared_ptr<A>& sptr){ b.a_ = sptr; } int main(){ { auto pa = std::make_shared<A>(); auto pb = std::make_shared<B>(); setBA(*pa,pb); setAB(*pb,pa); // 此处不会形成循环引用 } return 0; } ``` 在这个案例里,类 `A` 中保存了一个指向另一个实例 (`B`) 的弱引用而非强引用,有效地阻止了两者之间建立永久性的连接,进而解决了可能引发的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值