0

0

C++shared_ptr循环引用检测与解决技巧

P粉602998670

P粉602998670

发布时间:2025-09-11 09:42:01

|

1012人浏览过

|

来源于php中文网

原创

C++中shared_ptr循环引用因相互持有导致引用计数无法归零,引发内存泄漏;解决方法是使用std::weak_ptr打破循环,如子节点用weak_ptr引用父节点,避免增加引用计数,从而确保对象可正常析构。

c++shared_ptr循环引用检测与解决技巧

C++中

shared_ptr
的循环引用问题,其核心在于相互持有的
shared_ptr
对象阻止了彼此的析构,导致资源泄露。解决这一问题的关键技巧,几乎无一例外地指向了
std::weak_ptr
,它提供了一种非拥有性的引用机制,能够打破这种循环依赖。

解决方案

解决

shared_ptr
循环引用最直接且标准的方式是引入
std::weak_ptr
。当两个或多个对象需要相互引用,但又不能形成相互拥有关系时,将其中一个方向的
shared_ptr
替换为
weak_ptr
weak_ptr
不增加对象的引用计数,因此不会阻止对象的析构。

考虑一个典型的父子关系或节点关系:

#include 
#include 
#include 

class Child; // 前向声明

class Parent {
public:
    std::string name;
    std::vector> children;

    Parent(std::string n) : name(n) {
        std::cout << "Parent " << name << " created." << std::endl;
    }

    ~Parent() {
        std::cout << "Parent " << name << " destroyed." << std::endl;
    }

    void addChild(std::shared_ptr child);
};

class Child {
public:
    std::string name;
    std::weak_ptr parent; // 使用 weak_ptr 引用父对象

    Child(std::string n) : name(n) {
        std::cout << "Child " << name << " created." << std::endl;
    }

    ~Child() {
        std::cout << "Child " << name << " destroyed." << std::endl;
    }

    void setParent(std::shared_ptr p) {
        parent = p;
    }

    void greetParent() {
        if (auto p_locked = parent.lock()) { // 尝试锁定 weak_ptr 为 shared_ptr
            std::cout << "Child " << name << " greets Parent " << p_locked->name << std::endl;
        } else {
            std::cout << "Child " << name << ": Parent is gone." << std::endl;
        }
    }
};

void Parent::addChild(std::shared_ptr child) {
    children.push_back(child);
    child->setParent(std::shared_ptr(this, [](Parent*){})); // 注意这里,避免创建新的 shared_ptr 拥有 Parent
    // 更安全的做法是:在创建 Parent 时就使用 shared_ptr,然后将 Parent 的 shared_ptr 传递给 Child
    // 例如:std::shared_ptr p = std::make_shared("Father");
    //      std::shared_ptr c = std::make_shared("Son");
    //      p->addChild(c); // 此时 Child 内部可以通过 p 构造 weak_ptr
}

// 修正后的 Parent::addChild,更符合实际场景,需要 Parent 自身也是 shared_ptr
void Parent_Corrected_AddChild(std::shared_ptr self, std::shared_ptr child) {
    self->children.push_back(child);
    child->setParent(self); // Child 现在通过 weak_ptr 引用这个 shared_ptr
}


int main() {
    std::cout << "--- Scenario with cyclic reference (if not using weak_ptr) ---" << std::endl;
    // 如果 Child::parent 也是 shared_ptr,这里会发生内存泄漏
    // std::shared_ptr p1 = std::make_shared("P1");
    // std::shared_ptr c1 = std::make_shared("C1");
    // p1->children.push_back(c1);
    // c1->parent = p1; // 此时 p1 和 c1 互相持有,引用计数永远不为0

    std::cout << "--- Scenario with weak_ptr ---" << std::endl;
    std::shared_ptr p2 = std::make_shared("P2");
    std::shared_ptr c2 = std::make_shared("C2");

    // Parent_Corrected_AddChild(p2, c2); // 假设我们有这样一个辅助函数
    p2->children.push_back(c2);
    c2->setParent(p2); // Child 通过 weak_ptr 引用 Parent

    c2->greetParent();

    // 当 p2 和 c2 离开作用域时,它们会被正确销毁
    std::cout << "--- Exiting main scope ---" << std::endl;
    return 0;
}

在上面的例子中,

Child
通过
std::weak_ptr parent
来引用其父对象。当
Parent
对象被销毁时,即使
Child
仍然存在,其
parent.lock()
操作也会返回
nullptr
,表示父对象已不存在,从而避免了循环引用导致的内存泄漏。

立即学习C++免费学习笔记(深入)”;

shared_ptr
循环引用到底是什么,它为什么会发生?

shared_ptr
的魅力在于它实现了自动的内存管理,通过引用计数来决定何时释放所管理的对象。每当一个
shared_ptr
被复制,引用计数就增加;每当一个
shared_ptr
离开作用域或被重置,引用计数就减少。当引用计数归零时,
shared_ptr
所指向的对象就会被析构。

循环引用,顾名思义,就是两个或多个

shared_ptr
对象相互持有对方的
shared_ptr
。想象一下A持有一个指向B的
shared_ptr
,同时B也持有一个指向A的
shared_ptr
。当A和B各自离开其初始作用域时,它们的引用计数会减一,但因为彼此的存在,引用计数永远不会降到零。结果就是,这两个对象会一直驻留在内存中,直到程序结束,造成内存泄漏。这就像两个人手拉手,谁也无法放开手去处理别的事情,最终都被困住了。这问题说起来简单,但实际项目中,尤其是在构建复杂对象图时,一不小心就可能埋下隐患。

讯飞星火
讯飞星火

科大讯飞推出的多功能AI智能助手

下载

如何在项目中识别潜在的
shared_ptr
循环引用?

识别

shared_ptr
循环引用,很多时候它不是那么显而易见,尤其是在大型、复杂的代码库中。通常,我们发现问题往往是从内存泄漏的现象开始的。

一个最直接的信号是:当你预期某个对象应该被销毁,但它的析构函数(如果你有打印日志)却迟迟没有被调用。这通常表明它的引用计数没有归零。

在开发阶段,可以采取几种策略来主动检测:

  • 代码审查 (Code Review):这是最原始但也是最有效的方法之一。在设计对象之间的关系时,特别关注那些相互引用的场景。问自己:这个对象真的需要“拥有”另一个对象吗?如果两个对象之间存在双向关联,通常只有一方应该拥有另一方,或者两者都只持有
    weak_ptr
    。例如,父子关系中,父拥有子是合理的,但子不应该拥有父。
  • 内存分析工具 (Memory Profilers):Valgrind (尤其是
    memcheck
    工具) 是Linux下非常强大的内存调试工具,可以检测到内存泄漏。Windows下有Visual Studio的诊断工具,或者一些商业内存分析器。它们能帮你找出哪些内存块在程序结束时仍然没有被释放。虽然它们不能直接告诉你哪个
    shared_ptr
    导致了循环,但它们能指明泄漏的内存区域,缩小排查范围。
  • 调试器与断点:在对象的析构函数中设置断点。如果程序运行结束,而这些断点没有被触发,那么这些对象很可能发生了内存泄漏。结合调试器,你可以检查
    shared_ptr
    的引用计数(如果你的IDE支持,或者你可以通过自定义
    shared_ptr
    的deleter来打印计数)。
  • 日志与跟踪:在
    shared_ptr
    管理的对象的构造函数和析构函数中加入日志输出,观察它们的生命周期。如果一个对象应该在某个时间点被销毁,但日志却没有出现其析构信息,那么就值得深入调查。

在我自己的经验里,很多时候是先通过内存分析工具发现有泄漏,然后回溯代码,在可疑的相互引用点进行仔细的代码审查,最终定位到循环引用的。这通常是一个迭代的过程,需要耐心。

除了
weak_ptr
,还有哪些设计模式或策略可以规避循环引用?

虽然

weak_ptr
是解决
shared_ptr
循环引用的标准答案,但从更宏观的设计层面看,我们也可以通过一些设计模式或架构策略来从根本上避免这种问题。这通常涉及到重新思考对象之间的“拥有”关系和生命周期管理。

  • 单向依赖原则:这是最基本的设计思想。在设计对象关系时,尽量保持依赖的单向性。如果A依赖B,那么B不应该反过来依赖A。例如,在一个GUI应用中,一个按钮可以持有对其所属窗口的引用(
    shared_ptr
    ),但窗口通常不应该持有对其子按钮的
    shared_ptr
    (而是通过
    std::vector>
    或直接原始指针管理)。如果窗口需要与按钮交互,可以通过事件回调、观察者模式或
    weak_ptr
    来建立非拥有性连接。
  • 父子关系中的非拥有性引用:在严格的父子层级结构中,父节点拥有子节点是自然的(
    shared_ptr
    unique_ptr
    )。子节点如果需要引用父节点,通常应该使用
    weak_ptr
    。如果子节点需要修改父节点,可以通过函数参数传递父节点的
    shared_ptr
    ,或者通过事件机制进行通知。
  • 观察者模式 (Observer Pattern):当一个对象(Subject)的状态改变时,需要通知其他对象(Observer)。Subject通常不“拥有”Observer,而是维护一个Observer列表(通常是原始指针或
    weak_ptr
    )。Observer持有Subject的
    weak_ptr
    。这样,Subject的生命周期与Observer无关,避免了相互拥有。
  • 事件驱动架构:在更复杂的系统中,对象之间不直接持有彼此的引用,而是通过发布/订阅事件来通信。一个对象发布事件,另一个对象订阅并处理事件。这样,对象之间的耦合度大大降低,自然也就规避了直接引用导致的循环问题。
  • 使用原始指针 (Raw Pointers) 或引用:在某些严格控制生命周期的场景下,如果能明确知道被引用对象的生命周期长于引用者,那么使用原始指针或引用作为非拥有性引用是完全可以的。但这需要非常小心的管理,因为原始指针没有自动的空悬指针检测机制,一旦被指向的对象被销毁,原始指针就变成了悬空指针,访问会导致未定义行为。这种方法适用于那些生命周期由外部明确控制的局部、临时引用,或者在
    shared_ptr
    的内部实现中(例如
    enable_shared_from_this
    )。
  • 重新审视所有权语义:有时候,循环引用的出现是因为我们对对象之间的所有权理解不清。一个对象真的需要“拥有”另一个对象吗?它只是需要“访问”它吗?明确所有权语义是解决循环引用的第一步。如果所有权是共享的,
    shared_ptr
    是合适的;如果所有权是唯一的,
    unique_ptr
    是更好的选择;如果只是观察或访问,
    weak_ptr
    或原始指针才是正确的工具。

这些策略并非相互排斥,而是可以结合使用,共同构建一个健壮、无内存泄漏的C++应用。关键在于,在设计阶段就深入思考对象间的关系和生命周期,而不是等到问题出现再去修补。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
空指针异常处理
空指针异常处理

本专题整合了空指针异常解决方法,阅读专题下面的文章了解更多详细内容。

22

2025.11.16

windows查看端口占用情况
windows查看端口占用情况

Windows端口可以认为是计算机与外界通讯交流的出入口。逻辑意义上的端口一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。怎么查看windows端口占用情况呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

839

2023.07.26

查看端口占用情况windows
查看端口占用情况windows

端口占用是指与端口关联的软件占用端口而使得其他应用程序无法使用这些端口,端口占用问题是计算机系统编程领域的一个常见问题,端口占用的根本原因可能是操作系统的一些错误,服务器也可能会出现端口占用问题。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1133

2023.07.27

windows照片无法显示
windows照片无法显示

当我们尝试打开一张图片时,可能会出现一个错误提示,提示说"Windows照片查看器无法显示此图片,因为计算机上的可用内存不足",本专题为大家提供windows照片无法显示相关的文章,帮助大家解决该问题。

804

2023.08.01

windows查看端口被占用的情况
windows查看端口被占用的情况

windows查看端口被占用的情况的方法:1、使用Windows自带的资源监视器;2、使用命令提示符查看端口信息;3、使用任务管理器查看占用端口的进程。本专题为大家提供windows查看端口被占用的情况的相关的文章、下载、课程内容,供大家免费下载体验。

455

2023.08.02

windows无法访问共享电脑
windows无法访问共享电脑

在现代社会中,共享电脑是办公室和家庭的重要组成部分。然而,有时我们可能会遇到Windows无法访问共享电脑的问题。这个问题可能会导致数据无法共享,影响工作和生活的正常进行。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

2355

2023.08.08

windows自动更新
windows自动更新

Windows操作系统的自动更新功能可以确保系统及时获取最新的补丁和安全更新,以提高系统的稳定性和安全性。然而,有时候我们可能希望暂时或永久地关闭Windows的自动更新功能。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

824

2023.08.10

windows boot manager
windows boot manager

windows boot manager无法开机的解决方法:1、系统文件损坏,使用Windows安装光盘或USB启动盘进入恢复环境,选择修复计算机,然后选择自动修复;2、引导顺序错误,进入恢复环境,选择命令提示符,输入命令"bootrec /fixboot"和"bootrec /fixmbr",然后重新启动计算机;3、硬件问题,使用硬盘检测工具进行扫描和修复;4、重装操作系统。本专题还提供其他解决

1688

2023.08.28

go语言 注释编码
go语言 注释编码

本专题整合了go语言注释、注释规范等等内容,阅读专题下面的文章了解更多详细内容。

30

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 8.2万人学习

Git 教程
Git 教程

共21课时 | 3.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号