找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2385

积分

0

好友

341

主题
发表于 昨天 04:26 | 查看: 8| 回复: 0

使用 std::shared_ptr 的第一天,大多数人都觉得它实现了“自动垃圾回收”。但用到第三个月,你可能开始怀疑:为什么析构函数永远不被调用?

循环引用从来不是停留在教材里的“理论问题”,而是 C++ 项目中最为隐蔽、最难排查的一类内存泄漏。更麻烦的是,它通常不会导致程序崩溃或告警,只会悄无声息地吞噬内存

下面这 5 种典型场景,几乎都在真实的工程代码中出现过。

一、最经典的双向引用(教科书案例)

这是 shared_ptr 第一个也是最容易被忽略的坑。

struct B;

struct A {
    std::shared_ptr<B> b;
    ~A() { std::cout << "~A\n"; }
};

struct B {
    std::shared_ptr<A> a;
    ~B() { std::cout << "~B\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a;
}

程序运行结束,你将看不到任何析构函数的输出。

原因很直观:AB 对象的引用计数都至少为 1,永远无法归零。

正确做法
在双向关系中,必须有一方是“观察者”,应使用 std::weak_ptr

struct B {
    std::weak_ptr<A> a;
};

这是 C++ 标准库官方文档明确推荐的所有权模型。

二、父子对象模型中“想当然”的 shared_ptr

许多开发者在编写树形结构(如 DOM、AST、场景图)时会这样设计:

struct Node {
    std::shared_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;
};

从数据结构上看很对称,但从语义上就是错误的。

  • children 容器拥有其子节点。
  • parent 指针只是一个用于回溯的导航指针

一旦 parent 也使用 shared_ptr,整棵树将被锁定,无法释放。

行业共识

拥有关系用 shared_ptr,回溯关系用 weak_ptr

struct Node {
    std::weak_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;
};

这不是简单的编码习惯问题,而是对象所有权建模的核心问题。

三、回调或观察者模式中的隐式循环

这是实际项目里最容易被忽略的一类循环引用。

class Service : public std::enable_shared_from_this<Service> {
public:
    void registerCallback() {
        callback_ = [self = shared_from_this()] {
            self->doSomething();
        };
    }
private:
    std::function<void()> callback_;
};

代码看起来很合理,甚至很“现代 C++”。但关系链条是:

  • Service 对象拥有 callback_ 成员。
  • callback_ 的 lambda 表达式捕获了一个 shared_ptr<Service>

对象自己把自己给“锁住”了,析构函数永远不会被调用。

修复方式
在捕获时使用 weak_ptr

void registerCallback() {
    std::weak_ptr<Service> weakSelf = shared_from_this();
    callback_ = [weakSelf] {
        if (auto self = weakSelf.lock()) {
            self->doSomething();
        }
    };
}

这是 GUI 框架、网络库、事件系统中最常见的循环引用来源之一

四、enable_shared_from_this 在构造期间的误用

这个陷阱更为隐蔽,常常在代码重构后才暴露出来。

class Foo : public std::enable_shared_from_this<Foo> {
public:
    Foo() {
        auto self = shared_from_this(); // 未定义行为!
    }
};

shared_from_this() 只能在对象已经被一个 shared_ptr 管理之后调用

虽然这不是传统意义上的“循环引用”,但很多开发者为了修复这个问题,会在外层额外包裹一层 shared_ptr,结果反而制造出真正的循环。

正确模式
将初始化逻辑移出构造函数:

auto foo = std::make_shared<Foo>();
foo->init(); // 在 init() 方法中使用 shared_from_this

构造函数里,应避免执行任何与对象生命周期和所有权相关的操作。

五、容器缓存导致的“永久存活”

最后一个陷阱常见于缓存、对象池、单例管理器等场景。

class Manager {
public:
    std::unordered_map<int, std::shared_ptr<Item>> items;
};

struct Item {
    std::shared_ptr<Manager> mgr;
};

Manager 通过 items 映射拥有 Item,而 Item 又反过来拥有 Manager

这类问题的危险性在于:程序运行一切正常,功能无误,只是内存占用永不下降

解决方案依然是明确语义:

  • 管理器拥有对象 → 使用 shared_ptr
  • 对象需要回指管理器 → 使用 weak_ptr

写在最后:shared_ptr 不是“免死金牌”

C++ 没有垃圾回收机制,std::shared_ptr 只是一个基于引用计数的工具

判断是否该用 shared_ptr,其实只需问一个核心问题:

这个指针,是否真的“拥有”它所指向的对象?

  • 是,表示拥有所有权 → 使用 shared_ptr
  • 否,只是观察、引用或访问 → 使用 weak_ptr 或裸指针。

如果你在项目中遇到析构函数“莫名其妙不执行”,第一反应不应是怀疑编译器或 STL 的 bug,而应该从画出 shared_ptr 的关系图开始。将对象间的引用关系可视化,问题往往就解决了一半。

理解并避免这些常见的 std::shared_ptr 陷阱,是掌握现代 C++ 内存管理的关键一步。想要深入探讨更多 C++ 高级话题与实践,欢迎访问 云栈社区 与更多开发者交流。




上一篇:运维工程师高效工作指南:Vim核心命令速查与实战技巧
下一篇:开源 Rust 实现:媲美 Bitwarden 官方的轻量级自托管密码服务器
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 15:43 , Processed in 0.228550 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表