讲师中心 微信公众号
AI工具推荐 视频效率加速

C++内存回收策略 智能指针生命周期

冬静酱_3580

冬静酱_3580

发布时间:2025-08-24 10:39:01

|

962人浏览过

|

来源于php中文网

原创

C++无自动垃圾回收,依赖手动管理易致内存泄漏、悬挂指针和重复释放;智能指针通过RAII机制将资源管理绑定对象生命周期,unique_ptr实现独占所有权,离开作用域自动释放,避免泄漏;shared_ptr通过引用计数允许多方共享,计数归零时释放资源;weak_ptr打破循环引用,与shared_ptr协同,不增引用计数,用lock()安全访问对象,确保内存安全回收。

c++内存回收策略 智能指针生命周期

C++的内存回收,说白了,它自己可没有Java或者Python那种自带的“垃圾回收器”来帮你清理门户。它依赖的是一套更原始,也更强大的机制:程序员手动管理。但手动管理嘛,就容易出岔子。这就是为什么智能指针会出现,它们通过一种叫RAII(Resource Acquisition Is Initialization,资源获取即初始化)的哲学,把内存的分配和释放跟对象的生命周期牢牢绑定在一起。简单来说,一旦你用智能指针管理了一个资源,当这个智能指针自己寿终正寝(比如离开作用域)的时候,它就会自动帮你把关联的内存也清理掉。这大大提升了内存的安全性,也让我们的代码健壮性上了一个台阶,避免了那些恼人的内存泄漏和悬挂指针。

解决方案

要有效管理C++的内存,尤其是动态分配的内存,核心策略就是将资源管理与对象生命周期同步。传统上,我们使用

new
和
delete
来手动分配和释放内存,但这带来了巨大的心智负担和潜在的错误。任何一个疏忽,都可能导致内存泄漏、重复释放或访问已释放内存的未定义行为。

智能指针正是为了解决这些问题而生的。它们是C++标准库提供的一组模板类,封装了原始指针,并在自身生命周期结束时自动调用

delete
来释放所管理的内存。这背后的魔法就是RAII原则:资源在构造时获取,在析构时释放。通过智能指针,我们不再需要显式地调用
delete
,从而极大地减少了内存管理相关的错误。它们将内存管理这个“脏活累活”自动化了,让开发者可以更专注于业务逻辑本身。

C++传统内存管理有哪些常见陷阱?智能指针如何有效规避它们?

说实话,C++的内存管理,就像一把双刃剑,强大但容易伤到自己。最常见的几个坑,我个人感觉是:

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

  1. 内存泄漏(Memory Leak):这是最普遍的。你
    new
    了一块内存,结果忘了
    delete
    ,或者在
    delete
    之前,程序就因为异常跳出了当前作用域。比如,你在一个函数里分配了内存,但函数执行到一半抛了异常,或者提前
    return
    了,那块内存就永远“挂”在那里,没法被回收了。时间一长,系统资源就耗尽了。
  2. 悬挂指针(Dangling Pointer):这玩意儿更危险。你
    delete
    了一块内存,但你的某个指针变量还指着那个地址。之后你再通过这个指针去访问那块内存,就可能读到脏数据,甚至直接导致程序崩溃。
  3. 重复释放(Double Free):顾名思义,就是对同一块内存调用了两次
    delete
    。这通常会导致未定义行为,程序崩溃是常态,有时候甚至更隐蔽,让你抓耳挠腮找不到原因。

智能指针,特别是

std::unique_ptr
和
std::shared_ptr
,就是来堵这些漏洞的。

  • std::unique_ptr
    :它实现了独占所有权的概念。一块内存,只能被一个
    unique_ptr
    管理。当你把一个
    unique_ptr
    赋值给另一个时,原有的
    unique_ptr
    就会失去所有权(通过移动语义)。这就从根本上杜绝了重复释放的问题,因为永远只有一个指针在负责这块内存的生命周期。同时,当
    unique_ptr
    离开作用域时,它会自动调用析构函数,释放内存,有效避免了内存泄漏。
  • std::shared_ptr
    :它通过引用计数来管理共享所有权。多个
    shared_ptr
    可以共同拥有同一块内存,每多一个
    shared_ptr
    指向它,引用计数就加一;每少一个,引用计数就减一。只有当引用计数归零时(也就是没有
    shared_ptr
    再指向它了),内存才会被真正释放。这解决了多方需要访问同一块动态内存,但又不知道谁该负责释放的问题,也能有效防止内存泄漏。

它们把手动管理变成了自动管理,程序员的心智负担减轻了,代码的稳定性自然就上去了。

unique_ptr
与
shared_ptr
的生命周期和所有权模型有何本质区别?何时选择使用它们?

这两种智能指针是C++11引入的重头戏,它们在内存管理上扮演的角色截然不同,理解它们的区别是高效使用C++的关键。

std::unique_ptr
:独占所有权

  • 所有权模型:独占所有权。一个
    unique_ptr
    实例拥有它所指向的对象的唯一所有权。这意味着,不能有另一个
    unique_ptr
    同时指向同一个对象。
  • 生命周期:当
    unique_ptr
    本身被销毁时(例如,它所在的函数返回,或者它所在的局部作用域结束),它会自动删除所管理的内存。它不能被复制,但可以被移动。移动操作会将所有权从一个
    unique_ptr
    转移到另一个,原
    unique_ptr
    将不再拥有对象。
  • 特点:
    • 轻量级,几乎没有额外的运行时开销(除了存储原始指针本身)。
    • 非拷贝构造、非拷贝赋值。
    • 支持自定义删除器(deleter),可以管理非内存资源,比如文件句柄、网络连接等。
  • 何时使用:
    • 当你确定一个对象只有一个所有者,并且这个所有者负责其生命周期时。
    • 作为函数返回值,将所有权从函数内部转移到外部。
    • 在容器中存储动态分配的对象,比如
      std::vector<std::unique_ptr<MyObject>>
      。
    • 管理一些需要独占访问的资源,例如文件句柄。

举个例子:

#include <iostream>
#include <memory> // 包含 unique_ptr 和 make_unique

class MyResource {
public:
    MyResource() { std::cout << "MyResource created\n"; }
    ~MyResource() { std::cout << "MyResource destroyed\n"; }
    void doSomething() { std::cout << "Doing something with MyResource\n"; }
};

std::unique_ptr<MyResource> createResource() {
    return std::make_unique<MyResource>(); // 返回一个unique_ptr,所有权转移
} // MyResource在这里不会被销毁,因为所有权转移了

void processResource(std::unique_ptr<MyResource> res) {
    res->doSomething();
} // res离开作用域,MyResource被销毁

int main() {
    std::unique_ptr<MyResource> ptr1 = createResource();
    ptr1->doSomething();

    // std::unique_ptr<MyResource> ptr2 = ptr1; // 编译错误:unique_ptr不能拷贝
    std::unique_ptr<MyResource> ptr2 = std::move(ptr1); // 可以移动
    if (!ptr1) {
        std::cout << "ptr1 is now empty after move.\n";
    }
    ptr2->doSomething();

    // 也可以直接传递给函数,函数接收所有权
    processResource(std::move(ptr2)); // MyResource在这里被销毁
    // ptr2现在也空了

    return 0;
}

std::shared_ptr
:共享所有权

Yarn - Control and Access Threads.com via the CLI
Yarn - Control and Access Threads.com via the CLI

通过 yarn-threads-cli 与 Threads(Meta)交互。当用户想要阅读首页动态、点赞、收藏的帖子或特定帖子时使用;查看...

下载
  • 所有权模型:共享所有权。多个
    shared_ptr
    实例可以共同拥有同一个对象。
  • 生命周期:
    shared_ptr
    通过引用计数(reference count)来管理对象的生命周期。每当一个
    shared_ptr
    实例被创建或复制,指向同一个对象时,引用计数就加一;每当一个
    shared_ptr
    实例被销毁或重新赋值时,引用计数就减一。只有当引用计数归零时(即没有
    shared_ptr
    再指向该对象),所管理的内存才会被释放。
  • 特点:
    • 有额外的运行时开销,因为需要维护引用计数(通常是一个独立的控制块)。
    • 可拷贝、可赋值。
    • 支持自定义删除器。
    • 可能导致循环引用问题(需要
      weak_ptr
      解决)。
  • 何时使用:
    • 当多个部分或模块需要共享同一个动态分配的对象,并且它们都对该对象的生命周期有贡献时。
    • 在工厂模式中,返回一个对象的所有权可以被多方共享。
    • 构建复杂的图结构或树结构,其中节点可能被多个父节点引用。
#include <iostream>
#include <memory> // 包含 shared_ptr 和 make_shared

class MySharedResource {
public:
    MySharedResource() { std::cout << "MySharedResource created\n"; }
    ~MySharedResource() { std::cout << "MySharedResource destroyed\n"; }
    void report() { std::cout << "Shared resource is active.\n"; }
};

int main() {
    std::shared_ptr<MySharedResource> s_ptr1 = std::make_shared<MySharedResource>();
    std::cout << "s_ptr1 ref count: " << s_ptr1.use_count() << std::endl; // 1

    {
        std::shared_ptr<MySharedResource> s_ptr2 = s_ptr1; // 拷贝,引用计数增加
        std::cout << "s_ptr1 ref count: " << s_ptr1.use_count() << std::endl; // 2
        s_ptr2->report();
    } // s_ptr2离开作用域,引用计数减1

    std::cout << "s_ptr1 ref count: " << s_ptr1.use_count() << std::endl; // 1
    s_ptr1->report();

    // s_ptr1离开作用域,引用计数减到0,MySharedResource被销毁
    return 0;
}

选择哪个,取决于你对内存所有权的设计。如果你能明确谁是唯一的拥有者,

unique_ptr
是首选,因为它更高效。如果存在多方共享同一个资源的需求,且它们的生命周期相互独立,那么
shared_ptr
就是你的朋友。

weak_ptr
在解决循环引用问题中扮演了什么角色?它如何与
shared_ptr
协同工作?

std::weak_ptr
是一个很有意思的智能指针,它不像
unique_ptr
或
shared_ptr
那样直接管理对象的生命周期。它的主要作用是“观察”一个被
shared_ptr
管理的对象,但它本身不拥有该对象,也不会增加对象的引用计数。

循环引用问题

shared_ptr
虽然方便,但它有一个著名的陷阱:循环引用(circular reference)。当两个或多个对象通过
shared_ptr
相互引用时,它们的引用计数永远不会降到零,即使它们已经不再被外部代码访问。这导致这些对象及其关联的内存永远不会被释放,从而造成内存泄漏。

考虑一个简单的父子关系:一个

Parent
对象有一个
shared_ptr
指向它的
Child
,而
Child
对象也有一个
shared_ptr
指向它的
Parent
。

#include <iostream>
#include <memory>

class Child; // 前向声明

class Parent {
public:
    std::shared_ptr<Child> child_ptr;
    Parent() { std::cout << "Parent created\n"; }
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

class Child {
public:
    std::shared_ptr<Parent> parent_ptr; // 这里是问题所在
    Child() { std::cout << "Child created\n"; }
    ~Child() { std::cout << "Child destroyed\n"; }
};

int main() {
    {
        std::shared_ptr<Parent> p = std::make_shared<Parent>();
        std::shared_ptr<Child> c = std::make_shared<Child>();

        p->child_ptr = c;
        c->parent_ptr = p;

        std::cout << "Parent ref count: " << p.use_count() << std::endl; // 2 (p自身 + c->parent_ptr)
        std::cout << "Child ref count: " << c.use_count() << std::endl;  // 2 (c自身 + p->child_ptr)
    } // p和c离开作用域,但Parent和Child的析构函数都没有被调用!
    std::cout << "Exiting scope. Objects should have been destroyed, but might not be due to circular reference.\n";
    return 0;
}

在这个例子中,当

p
和
c
离开作用域时,
p
的引用计数从2降到1(因为
c->parent_ptr
还在引用它),
c
的引用计数也从2降到1(因为
p->child_ptr
还在引用它)。结果是,
Parent
和
Child
对象的引用计数都停留在1,永远不会降到0,它们永远不会被销毁。

weak_ptr
如何解决

weak_ptr
就是为了打破这种循环而设计的。它提供了一种“非拥有”的引用方式。当一个
weak_ptr
指向一个对象时,它不会增加该对象的引用计数。这意味着,
weak_ptr
的存在不会阻止
shared_ptr
在引用计数归零时销毁对象。

要访问

weak_ptr
指向的对象,你需要先调用它的
lock()
方法,它会返回一个
shared_ptr
。如果对象仍然存在(即有至少一个
shared_ptr
还在引用它),
lock()
会返回一个有效的
shared_ptr
;否则,它会返回一个空的
shared_ptr
。

修正上述循环引用问题的方法是,让其中一个指针变成

weak_ptr
。通常,我们会让“子”指向“父”的指针成为
weak_ptr
,因为父对象的生命周期往往独立于子对象。

#include <iostream>
#include <memory>

class ChildFixed; // 前向声明

class ParentFixed {
public:
    std::shared_ptr<ChildFixed> child_ptr;
    ParentFixed() { std::cout << "ParentFixed created\n"; }
    ~ParentFixed() { std::cout << "ParentFixed destroyed\n"; }
};

class ChildFixed {
public:
    std::weak_ptr<ParentFixed> parent_ptr; // 使用 weak_ptr
    ChildFixed() { std::cout << "ChildFixed created\n"; }
    ~ChildFixed() { std::cout << "ChildFixed destroyed\n"; }

    void accessParent() {
        if (auto p = parent_ptr.lock()) { // 尝试获取 shared_ptr
            std::cout << "ChildFixed can access ParentFixed.\n";
        } else {
            std::cout << "ParentFixed no longer exists.\n";
        }
    }
};

int main() {
    {
        std::shared_ptr<ParentFixed> p = std::make_shared<ParentFixed>();
        std::shared_ptr<ChildFixed> c = std::make_shared<ChildFixed>();

        p->child_ptr = c;
        c->parent_ptr = p; // weak_ptr 不增加引用计数

        std::cout << "ParentFixed ref count: " << p.use_count() << std::endl; // 2 (p自身 + p->child_ptr)
        std::cout << "ChildFixed ref count: " << c.use_count() << std::endl;  // 2 (c自身 + c->parent_ptr指向的Parent的引用计数是1)
        // 注意:c->parent_ptr是weak_ptr,它不影响p的引用计数。p的引用计数是2是因为p自身和p->child_ptr指向的c的parent_ptr(如果它也是shared_ptr)
        // 这里需要更正,p的引用计数是2是因为p自身和c->parent_ptr指向的p。
        // 而c的引用计数是2是因为c自身和p->child_ptr指向的c。
        // 在ChildFixed中,parent_ptr是weak_ptr,所以它不会增加ParentFixed的引用计数。
        // 实际上,p的引用计数是1(只有p自身),c的引用计数是2(c自身 + p->child_ptr)。
        // 让我们重新检查这个逻辑。

        // p的引用计数:
        // 1. p = make_shared<ParentFixed>() -> p的引用计数为1
        // 2. p->child_ptr = c -> c的引用计数为2 (c自身 + p->child_ptr)
        // 3. c->parent_ptr = p -> p的引用计数仍为1,因为c->parent_ptr是weak_ptr

        // 实际输出应该是:
        // ParentFixed ref count: 1
        // ChildFixed ref count: 2
        std::cout << "ParentFixed ref count: " << p.use_count() << std::endl; // 1
        std::cout << "ChildFixed ref count: " << c.use_count() << std::endl;  // 2

        c->accessParent(); // 可以访问
    } // p和c离开作用域。
    // 首先,c的引用计数降为1(p->child_ptr还指向它)。
    // 然后,p的引用计数降为0,ParentFixed被销毁。
    // ParentFixed销毁后,p->child_ptr不再指向c,c的引用计数降为0,ChildFixed被销毁。
    std::cout << "Exiting scope. Objects should now be destroyed.\n";
    return 0;
}

通过使用

weak_ptr
,我们打破了循环引用,确保了对象能在不再被需要时被正确地销毁。
weak_ptr
就像一个观察者,它知道对象是否存在,但从不干预对象的生命周期。这是在设计复杂对象关系时,避免内存泄漏的一个关键技巧。

热门AI工具

更多
切问学术

切问学术是一款AI论文写作工具,复旦大学NLP团队推出的AI学术智能体。

LibLibAI
LibLibAI Hot

一款AI视频创作工具,主要用于国内领先的AI创意平台,以海量模型、低门槛操作与“创作-分享-商业化”生态,让小白与专业创作者都能高效实现图文乃至视频创意表达,适合需要提升相关任务效率的用户。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

相关专题

更多
resource是什么文件
resource是什么文件

Resource文件是一种特殊类型的文件,它通常用于存储应用程序或操作系统中的各种资源信息。它们在应用程序开发中起着关键作用,并在跨平台开发和国际化方面提供支持。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

320

2023.12.20

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2408

2023.11.20

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

3248

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

584

2025.10.23

C++ 智能指针与现代内存管理
C++ 智能指针与现代内存管理

深入讲解 C++ 现代内存管理的核心工具——智能指针,涵盖 unique_ptr 独占所有权语义、shared_ptr 引用计数机制与循环引用问题、weak_ptr 弱引用的应用场景、make_unique/make_shared 工厂函数的性能优势、自定义删除器的编写、RAII 资源管理思想的实践,以及从裸指针迁移到智能指针的重构策略,帮助开发者编写安全无泄漏的现代 C++ 代码。

299

2026.04.23

数据库Delete用法
数据库Delete用法

数据库Delete用法:1、删除单条记录;2、删除多条记录;3、删除所有记录;4、删除特定条件的记录。更多关于数据库Delete的内容,大家可以访问下面的文章。

2431

2023.11.13

drop和delete的区别
drop和delete的区别

drop和delete的区别:1、功能与用途;2、操作对象;3、可逆性;4、空间释放;5、执行速度与效率;6、与其他命令的交互;7、影响的持久性;8、语法和执行;9、触发器与约束;10、事务处理。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

765

2023.12.29

PHP 命令行脚本与自动化任务开发
PHP 命令行脚本与自动化任务开发

本专题系统讲解 PHP 在命令行环境(CLI)下的开发与应用,内容涵盖 PHP CLI 基础、参数解析、文件与目录操作、日志输出、异常处理,以及与 Linux 定时任务(Cron)的结合使用。通过实战示例,帮助开发者掌握使用 PHP 构建 自动化脚本、批处理工具与后台任务程序 的能力。

446

2025.12.13

Buffalo框架数据库开发全教程
Buffalo框架数据库开发全教程

本专题围绕Buffalo框架数据库开发,讲解database.yml多环境配置、soda与fizz迁移生成回滚、模型结构体标签、增删改查与条件查询、一对多与多对多关联、数据校验、回调钩子、事务处理及原生SQL执行能力。

20

2026.09.23

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
尚学堂ios初级视频教程
尚学堂ios初级视频教程

共77课时 | 20.9万人学习

尚学堂Swift入门视频教程
尚学堂Swift入门视频教程

共44课时 | 16.5万人学习

Conan 2 安装指南
Conan 2 安装指南

共0课时 | 0人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn