从零开始 C++------ 十六.深度拆解 C++ 智能指针:unique_ptr/shared_ptr/weak_ptr 底层模拟、内存泄漏根治

系列文章目录

提示:这里是系列文章的专栏

并不喜欢吃鱼的C++专栏


提示:以下是文章目录哦!

文章目录

目录

​编辑

系列文章目录

文章目录

前言

一、为什么需要智能指针?裸指针的内存泄漏痛点

[1.1 裸指针异常场景下必然内存泄漏](#1.1 裸指针异常场景下必然内存泄漏)

问题代码

泄漏场景分析

[场景 1:int* array2 = new int10; 内存分配失败抛异常](#场景 1:int* array2 = new int[10]; 内存分配失败抛异常)

[场景 2:new 全部成功,进入 try 代码块,Divide 触发除零异常](#场景 2:new 全部成功,进入 try 代码块,Divide 触发除零异常)

[场景 3:代码全程无任何异常(new 成功、除法合法)](#场景 3:代码全程无任何异常(new 成功、除法合法))

细节补充:

[1.2 解决方案:RAII 思想 + 智能指针](#1.2 解决方案:RAII 思想 + 智能指针)

[二、RAII 设计思想 & 简易智能指针模拟实现](#二、RAII 设计思想 & 简易智能指针模拟实现)

[2.1 RAII 核心定义](#2.1 RAII 核心定义)

[2.2 简易 RAII 智能指针手写实现](#2.2 简易 RAII 智能指针手写实现)

[2.3 使用简易智能指针改造泄漏代码](#2.3 使用简易智能指针改造泄漏代码)

整体设计目标

[1. 模板 template​编辑](#1. 模板 template编辑)

[2. 私有成员 T* _ptr](#2. 私有成员 T* _ptr)

[3. 构造函数 SmartPtr (T* ptr) : _ptr (ptr)](#3. 构造函数 SmartPtr (T* ptr) : _ptr (ptr))

[4. 析构函数~SmartPtr ()(RAII 最核心)​编辑](#4. 析构函数~SmartPtr ()(RAII 最核心)编辑)

优化效果

[三、C++ 标准库四大智能指针完整使用](#三、C++ 标准库四大智能指针完整使用)

[3.1 auto_ptr(废弃,禁止使用)](#3.1 auto_ptr(废弃,禁止使用))

核心特性

缺陷演示代码

[3.2 unique_ptr 唯一智能指针(推荐优先使用)](#3.2 unique_ptr 唯一智能指针(推荐优先使用))

核心特性

示例代码

[3.3 shared_ptr 共享智能指针(引用计数)](#3.3 shared_ptr 共享智能指针(引用计数))

核心特性

[3.4 自定义删除器(适配数组、文件、锁等非 new 资源)](#3.4 自定义删除器(适配数组、文件、锁等非 new 资源))

完整示例

[3.5 weak_ptr 弱智能指针(辅助 shared_ptr,解决循环引用)](#3.5 weak_ptr 弱智能指针(辅助 shared_ptr,解决循环引用))

基础特性

[weak_ptr 基础示例](#weak_ptr 基础示例)

四、四大智能指针底层模拟实现

[4.1 auto_ptr 模拟实现(管理权转移)](#4.1 auto_ptr 模拟实现(管理权转移))

[4.2 unique_ptr 模拟实现(禁用拷贝,仅支持移动)](#4.2 unique_ptr 模拟实现(禁用拷贝,仅支持移动))

[4.3 shared_ptr 模拟实现(引用计数核心)](#4.3 shared_ptr 模拟实现(引用计数核心))

核心原理

[4.4 weak_ptr 极简模拟实现](#4.4 weak_ptr 极简模拟实现)

[五、shared_ptr 循环引用问题 & weak_ptr 解决方案](#五、shared_ptr 循环引用问题 & weak_ptr 解决方案)

[5.1 什么是循环引用?内存泄漏根源](#5.1 什么是循环引用?内存泄漏根源)

泄漏逻辑拆解

[六、shared_ptr 线程安全问题(PDF 第六节)](#六、shared_ptr 线程安全问题(PDF 第六节))

[6.1 两个层面的线程安全区分](#6.1 两个层面的线程安全区分)

[6.2 并发测试示例(存在数据竞争)](#6.2 并发测试示例(存在数据竞争))

[七、智能指针演进历史:Boost 库与 C++ 标准关系(PDF 第七节)](#七、智能指针演进历史:Boost 库与 C++ 标准关系(PDF 第七节))

[八、内存泄漏完整讲解(PDF 第八节)](#八、内存泄漏完整讲解(PDF 第八节))

[8.1 内存泄漏定义](#8.1 内存泄漏定义)

两种泄漏场景

[8.2 内存泄漏危害](#8.2 内存泄漏危害)

演示代码(短期程序无明显危害)

[8.3 内存泄漏检测工具](#8.3 内存泄漏检测工具)

[8.4 避免内存泄漏两套方案](#8.4 避免内存泄漏两套方案)

九、四大智能指针选型总结


前言

提示:这里可以添加本文要记录的大概内容:

传统 C++ 裸指针依靠手动 new/delete 管理堆内存,存在无法根治的缺陷:多分支逻辑、异常抛出场景极易遗漏释放代码,频繁诱发内存泄漏;资源生命周期管控分散,错误检测与资源回收代码高度耦合,底层函数需要兼顾报错与清理,业务逻辑臃肿杂乱。

智能指针依托 RAII 资源管理思想,将资源获取与释放绑定对象生命周期,完美分离资源申请逻辑错误处理逻辑:底层函数仅负责判断资源是否创建失败并抛出异常,无需关心上层如何处理回收;上层调用链依靠局部对象自动析构兜底释放资源,无论代码正常运行还是触发异常,堆内存、文件句柄、网络套接字等资源都能安全回收。

本文将从裸指针泄漏痛点切入,逐层拆解 RAII 核心原理、四大标准智能指针特性、手写底层模拟实现、自定义删除器、循环引用内存泄漏解决方案、多线程安全边界等全套知识点,结合完整可运行代码演示每一处语法与踩坑细节,帮你彻底搞懂智能指针完整体系。


提示:以下是本篇文章正文内容

一、为什么需要智能指针?裸指针的内存泄漏痛点

1.1 裸指针异常场景下必然内存泄漏

问题代码

cpp 复制代码
#include <iostream>
using namespace std;

double Divide(int a, int b)
{
    if (b == 0)
        throw "Divide by zero condition!";
    return (double)a / b;
}

void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10]; // 若new抛异常,array1直接泄漏
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 仅捕获除零异常,array2开辟失败时不会进这里
        delete[] array1;
        delete[] array2;
        throw; // 重新抛出异常
    }
    // 正常流程释放
    delete[] array1;
    delete[] array2;
}

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

泄漏场景分析

  1. array2 = new int[10]开辟内存失败抛异常:函数直接终止,array1无任何释放逻辑,永久泄漏
  2. Divide触发除 0 异常进入 catch,虽手动释放,但多层连续 new、多资源(文件、锁)场景下,try-catch 嵌套爆炸,代码臃肿难维护
  3. 手动管理资源依赖程序员记忆,分支越多越容易漏写delete
场景 1:int* array2 = new int[10]; 内存分配失败抛异常

C++ 中new分配堆内存失败时,默认会抛出std::bad_alloc类型异常。

  • 执行顺序:先执行array1 = new int[10],堆内存开辟成功
  • 再执行array2 = new int[10],内存不足,触发throw bad_alloc
  • 此时函数Func直接中断,不会进入下方的try-catch代码块
  • 后果:array1指向的堆数组没有任何delete[]释放,永久内存泄漏
场景 2:new 全部成功,进入 try 代码块,Divide 触发除零异常

流程:

  1. array1array2内存开辟成功
  2. 进入try内部,读取输入lentime,调用Divide(len, time)
  3. time == 0Divide抛出字符串异常
  4. 立刻跳出Divide,跳到当前匹配的catch(...)分支
  5. catch(...)万能捕获 ,能捕获任意类型异常:
    • 先执行delete[] array1; delete[] array2; 释放两块堆内存
    • throw; 不带参数,表示重新抛出当前捕获到的异常 ,继续向上传递给外层main里的try-catch
  6. catch执行完毕,不会走到函数末尾的正常释放代码
场景 3:代码全程无任何异常(new 成功、除法合法)
  1. try 内代码顺利执行完毕,无 throw
  2. 直接跳过catch分支
  3. 执行函数末尾 delete[] array1; delete[] array2;,正常释放内存,无泄漏
细节补充:
  1. catch(...):省略号表示捕获所有类型的异常,只能放在一组 catch 的最后;
  2. throw; 空抛:只允许写在catch内部,作用是把当前捕获到的异常再次向上抛出,交给外层处理。

1.2 解决方案:RAII 思想 + 智能指针

裸指针手动释放缺陷无法靠 try-catch 根治,RAII 是标准化资源管理方案,智能指针是 RAII 最典型落地实现


二、RAII 设计思想 & 简易智能指针模拟实现

2.1 RAII 核心定义

RAII(Resource Acquisition Is Initialization,资源获取即初始化)

  1. 资源:堆内存、文件句柄、网络 socket、互斥锁等
  2. 核心逻辑:资源交给局部对象托管,对象构造时获取资源,对象生命周期结束(出作用域、异常退出)自动调用析构释放资源
  3. 优势:无论代码正常执行还是抛异常,对象必然析构,资源 100% 释放,彻底杜绝泄漏

2.2 简易 RAII 智能指针手写实现

2.3 使用简易智能指针改造泄漏代码

整体设计目标

前面裸指针代码最大问题:不管正常退出还是抛异常退出,都没法保证一定执行 delete \[\] 。 这个SmartPtr是极简 RAII 智能指针原型,设计目标只有 3 个:

  1. 接管裸指针的堆资源,对象生命周期结束自动释放内存,解决异常泄漏
  2. 重载运算符,用起来和普通裸指针完全一致,降低使用成本
  3. 封装裸指针,把资源释放逻辑收拢到类内部,不用使用者手动管理 delete

下面分模块逐个讲设计原因

1. 模板 template

为什么要用模板?

智能指针要管理任意类型堆资源:intdouble、自定义Date、数组等

  • 如果不写模板,只能写死int*,只能管理 int 数组
  • 模板泛型,SmartPtr<int>管 int 数组、SmartPtr<Date>管自定义对象数组,一套代码适配所有类型,复用性拉满
2. 私有成员 T* _ptr
复制代码
    T* _ptr;

设计原因

  1. 存储托管的裸指针 :所有 new 出来的堆地址都存在_ptr里,类内部统一持有资源
  2. 封装隐藏 :私有变量,外部无法直接修改_ptr,防止外部手动delete _ptr造成二次释放、野指针
  3. 整个类所有成员函数都围绕这个指针做托管、访问、释放
3. 构造函数 SmartPtr (T* ptr) : _ptr (ptr)

核心设计思想:RAII「资源获取即初始化」

RAII 规则:创建对象时,立刻把资源交给对象托管

  1. 使用者SmartPtr<int> sp(new int[10]);
    • new开辟堆内存(获取资源)
    • 同步构造SmartPtr局部对象,构造函数把堆指针存入_ptr,资源交给对象管理
  2. 为什么参数是T* ptr? 接收new T[]返回的裸指针,建立对象和堆资源的绑定关系
  3. 这里简化实现没有加explicit,标准库 unique_ptr/shared_ptr 会加 explicit 阻止隐式转换,本示例是简易 demo 所以省略
4. 析构函数~SmartPtr ()(RAII 最核心)

为什么析构里写 delete \[\]?解决裸指针泄漏的关键

C++ 规则:局部对象离开作用域时,一定会自动调用析构函数,无论函数是正常 return,还是中途 throw 异常栈展开

优化效果

无论new失败、Divide抛异常,局部对象sp1/sp2都会销毁,自动执行delete[],无内存泄漏,代码大幅简化


三、C++ 标准库四大智能指针完整使用

前置说明

  1. 头文件:<memory>
  2. 分类:auto_ptr(C++98废弃)unique_ptr(C++11)shared_ptr(C++11)weak_ptr(C++11)
  3. 共性:除weak_ptr外均遵循 RAII,重载*/->模拟指针;
  4. 差异核心:拷贝 / 移动逻辑、资源管理方式。

3.1 auto_ptr(废弃,禁止使用)

核心特性

拷贝时转移资源管理权,原对象置空悬空,后续访问崩溃,设计缺陷严重,C++11 标准废弃,企业编码规范禁用。

缺陷演示代码

3.2 unique_ptr 唯一智能指针(推荐优先使用)

核心特性

  1. 禁止拷贝构造、禁止拷贝赋值,编译器直接报错;
  2. 支持移动构造、移动赋值std::move),移动后原智能指针置空;
  3. 支持数组特化 unique_ptr<T[]>,原生适配 new[],析构自动调用 delete[]
  4. 支持自定义删除器,删除器类型作为模板参数
  5. 构造函数被 explicit 修饰,禁止裸指针隐式转换;
  6. 重载 operator* / operator-> / operator[],像裸指针一样访问资源;
  7. 支持 operator bool 隐式转换,可直接 if(up) 判断是否持有资源。

示例代码

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

struct Date
{
    int _year, _month, _day;
    Date(int y=1,int m=1,int d=1):_year(y),_month(m),_day(d)
    {
        cout << "构造Date" << endl;
    }
    ~Date()
    {
        cout << "析构Date" << endl;
    }
};

int main()
{
    // 1. 裸指针构造,explicit 禁止隐式转换
    unique_ptr<Date> up1(new Date(2026,7,12));
    // unique_ptr<Date> upErr = new Date; // 编译报错,explicit阻止隐式转换

    // 2. 禁止拷贝,下面两行直接编译失败
    // unique_ptr<Date> up2(up1);
    // unique_ptr<Date> up3 = up1;

    // 3. 移动所有权 std::move
    unique_ptr<Date> up4(move(up1));
    // 移动后 up1 为空,不再管理资源
    if (!up1)
        cout << "up1 已悬空,无资源" << endl;

    // 4. 访问资源 * ->
    cout << up4->_year << endl;
    cout << (*up4)._month << endl;

    // 5. 判空
    if (up4)
        cout << "up4 持有有效资源" << endl;

    return 0;
}

运行逻辑:函数结束时 up4 析构,自动释放 Date 对象,无内存泄漏;即使函数中途抛异常,局部对象自动析构

3.3 shared_ptr 共享智能指针(引用计数)

核心特性

  1. 共享所有权 :支持拷贝、移动,底层堆上存储引用计数
  2. 每拷贝一次计数 + 1,每析构一次计数 - 1;计数为 0 时释放资源;
  3. 提供make_shared高效创建(一次堆内存分配,性能优于裸指针构造);
  4. 删除器通过构造函数参数传入;
  5. 重载operator bool,可直接 if 判断是否为空;
  6. 构造函数explicit修饰,禁止裸指针隐式转换。
cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

// 模拟图中的红色资源对象
struct Resource
{
    Resource()
    {
        cout << "创建一块堆资源" << endl;
    }
    ~Resource()
    {
        cout << "释放一块堆资源" << endl;
    }
};

int main()
{
    // ========== 左侧图示逻辑 sp1、sp2 共享同一资源 ==========
    shared_ptr<Resource> sp1(new Resource);
    cout << "sp1 创建后,引用计数:" << sp1.use_count() << endl;

    // sp2(sp1) 拷贝构造,和sp1共用资源、共用计数块,计数+1 → 2
    shared_ptr<Resource> sp2(sp1);
    cout << "sp2(sp1)拷贝后,引用计数:" << sp1.use_count() << endl;
    cout << "sp1与sp2是否指向同一资源:" << (sp1.get() == sp2.get()) << endl;
    cout << "sp1与sp2共用同一个引用计数块" << endl << endl;

    // ========== 右侧图示逻辑 sp3 独立资源 ==========
    shared_ptr<Resource> sp3(new Resource);
    cout << "sp3独立创建资源,引用计数:" << sp3.use_count() << endl;
    cout << "sp3和sp1资源地址不同,拥有独立的引用计数块" << endl;

    return 0;
}

代码对应图形说明

  1. sp1sp2:两个 shared_ptr 内部黑色指针指向同一个红色资源;绿色指针指向同一个绿色计数块,数值为 2
  2. sp3:单独黑色箭头指向另一块红色资源,绿色箭头指向单独的计数块,数值为 1
  3. 关键底层逻辑:
    • 每一组共享资源,都会在堆上分配一块独立的引用计数控制块(绿色方框)
    • 拷贝构造 shared_ptr<Resource> sp2(sp1) 只会增加同一块计数,不会新建资源与计数
    • 重新 new Resource 会开辟全新资源 + 全新计数块,和之前完全隔离

运行结果:

3.4 自定义删除器(适配数组、文件、锁等非 new 资源)

智能指针默认析构调用deletenew[]/ 文件句柄 / 网络句柄需要自定义释放逻辑,支持 4 种删除器:仿函数、函数指针、lambda、std::function

完整示例

cpp 复制代码
#include <cstdio>
// 1. 函数指针删除器
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}
// 2. 仿函数删除器
template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};
// 文件释放仿函数
class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "关闭文件:" << ptr << endl;
        fclose(ptr);
    }
};

int main()
{
    // 方案1:标准数组特化(最简单)
    unique_ptr<Date[]> up1(new Date[5]);
    shared_ptr<Date[]> sp1(new Date[5]);

    // 方案2:仿函数删除器
    unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

    // 方案3:函数指针删除器
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

    // 方案4:lambda删除器
    auto delArr = [](Date* ptr) { delete[] ptr; };
    unique_ptr<Date, decltype(delArr)> up4(new Date[5], delArr);
    shared_ptr<Date> sp4(new Date[5], delArr);

    // 管理文件句柄(非堆内存资源)
    shared_ptr<FILE> fp1(fopen("test.txt", "r"), Fclose());
    shared_ptr<FILE> fp2(fopen("test.txt", "r"), [](FILE* f) { fclose(f); });
    return 0;
}

关键区分知识点

1.unique_ptr删除器是模板参数,类型固化

  • unique_ptr 删除器 = 模板参数 :删除器类型是 unique_ptr 类型的一部分,编译期固定,栈上存储,零堆开销;不同删除器 = 完全不同的类型,不能互相赋值 / 转换

2.shared_ptr删除器是构造函数参数,存储在内部控制块,灵活多变

  • shared_ptr 删除器 = 构造函数入参 :删除器存放在堆上的控制块,运行期绑定,不影响 shared_ptr<T> 本身类型;同一个 shared_ptr<T> 可以持有不同删除器,互相赋值、拷贝无限制

3.5 weak_ptr 弱智能指针(辅助 shared_ptr,解决循环引用)

基础特性

  1. 不遵循 RAII,不能单独管理资源,无法通过裸指针构造;
  2. 仅能绑定shared_ptr不增加引用计数
  3. 无重载*/->,不能直接访问资源;
  4. 核心接口:
    • expired():判断绑定的资源是否已释放;
    • lock():返回shared_ptr,安全访问资源,资源销毁则返回空智能指针;
    • use_count():获取对应 shared_ptr 的引用计数。

weak_ptr 基础示例

四、四大智能指针底层模拟实现

4.1 auto_ptr 模拟实现(管理权转移)

cpp 复制代码
namespace bit
{
template<class T>
class auto_ptr
{
public:
    explicit auto_ptr(T* ptr = nullptr) : _ptr(ptr) {}
    // 拷贝构造:转移所有权,原对象置空
    auto_ptr(auto_ptr<T>& sp)
    {
        _ptr = sp._ptr;
        sp._ptr = nullptr;
    }
    // 赋值重载
    auto_ptr<T>& operator=(auto_ptr<T>& ap)
    {
        if (this != &ap)
        {
            delete _ptr;
            _ptr = ap._ptr;
            ap._ptr = nullptr;
        }
        return *this;
    }
    ~auto_ptr()
    {
        delete _ptr;
    }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
private:
    T* _ptr;
};
}

4.2 unique_ptr 模拟实现(禁用拷贝,仅支持移动)

cpp 复制代码
namespace bit
{
template<class T>
class unique_ptr
{
public:
    explicit unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}
    ~unique_ptr() { delete _ptr; }
    // 删除拷贝构造、拷贝赋值
    unique_ptr(const unique_ptr<T>&) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
    // 移动构造、移动赋值
    unique_ptr(unique_ptr<T>&& sp)
    {
        _ptr = sp._ptr;
        sp._ptr = nullptr;
    }
    unique_ptr<T>& operator=(unique_ptr<T>&& sp)
    {
        if (_ptr != sp._ptr)
        {
            delete _ptr;
            _ptr = sp._ptr;
            sp._ptr = nullptr;
        }
        return *this;
    }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
private:
    T* _ptr;
};
}

4.3 shared_ptr 模拟实现(引用计数核心)

核心原理

  1. 资源指针_ptr + 堆上引用计数_pcount(不能用静态成员,每个资源独立计数);
  2. 拷贝时计数 ++,析构 / 赋值时计数 --;计数为 0 时释放资源 + 释放计数;
  3. 内置通用删除器,支持自定义删除器
cpp 复制代码
namespace bit
{
template<class T>
class shared_ptr
{
public:
    explicit shared_ptr(T* ptr = nullptr)
        : _ptr(ptr), _pcount(new int(1))
    {}
    // 带自定义删除器构造
    template<class D>
    shared_ptr(T* ptr, D del)
        : _ptr(ptr), _pcount(new int(1)), _del(del)
    {}
    // 拷贝构造:共享计数
    shared_ptr(const shared_ptr<T>& sp)
    {
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        _del = sp._del;
        ++(*_pcount);
    }
    // 释放资源逻辑
    void release()
    {
        if (_pcount && --(*_pcount) == 0)
        {
            _del(_ptr);
            delete _pcount;
            _ptr = nullptr;
            _pcount = nullptr;
        }
    }
    // 赋值重载
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            release();
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            _del = sp._del;
            ++(*_pcount);
        }
        return *this;
    }
    ~shared_ptr() { release(); }
    T* get() const { return _ptr; }
    int use_count() const { return *_pcount; }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
private:
    T* _ptr;
    int* _pcount; // 标准库使用atomic<int>保证线程安全
    function<void(T*)> _del = [](T* p) { delete p; };
};
}

4.4 weak_ptr 极简模拟实现

仅保存资源指针,不管理计数,无释放逻辑:

cpp 复制代码
namespace bit
{
template<class T>
class weak_ptr
{
public:
    weak_ptr() : _ptr(nullptr) {}
    weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        _ptr = sp.get();
        return *this;
    }
private:
    T* _ptr = nullptr;
};
}

五、shared_ptr 循环引用问题 & weak_ptr 解决方案

5.1 什么是循环引用?内存泄漏根源

双向链表节点互相使用shared_ptr指向对方,对象析构后双方引用计数均为 1,资源永远不会释放

泄漏逻辑拆解

  1. n1、n2 出作用域析构,各自计数从 2 变为 1;
  2. n1 资源依赖n2->_prev持有,n2 资源依赖n1->_next持有;
  3. 两个智能指针成员依附节点本身,节点不销毁则成员不析构,计数永远无法归零。

六、shared_ptr 线程安全问题(PDF 第六节)

6.1 两个层面的线程安全区分

  1. 引用计数操作 :多线程拷贝 / 析构shared_ptr时,读写_pcount存在竞争,标准库使用atomic<int>原子变量保证计数线程安全;手写简易版int*计数会并发崩溃;
  2. 托管资源本身:智能指针不保护指向的对象,多线程修改对象成员需要手动加锁。

6.2 并发测试示例(存在数据竞争)

cpp 复制代码
#include <thread>
#include <mutex>
struct AA
{
    int _a1 = 0;
};
int main()
{
    bit::shared_ptr<AA> p(new AA);
    const size_t n = 100000;
    mutex mtx;
    auto func = [&]()
    {
        for (size_t i = 0; i < n; ++i)
        {
            bit::shared_ptr<AA> copy(p); // 计数并发修改,简易实现无原子会崩溃
            unique_lock<mutex> lk(mtx);
            copy->_a1++; // 对象成员修改必须手动加锁
        }
    };
    thread t1(func);
    thread t2(func);
    t1.join();
    t2.join();
    cout << p->_a1 << endl;
    return 0;
}

七、智能指针演进历史:Boost 库与 C++ 标准关系(PDF 第七节)

  1. C++98:仅auto_ptr,设计缺陷严重;
  2. Boost 库:提供scoped_ptr/scoped_array(对应 C++11 unique_ptr)、shared_ptr/shared_arrayweak_ptr,是标准智能指针原型;
  3. C++ TR1:引入shared_ptr,非正式标准;
  4. C++11:正式纳入unique_ptr/shared_ptr/weak_ptr,废弃 auto_ptr,实现参考 Boost 设计。

八、内存泄漏完整讲解(PDF 第八节)

8.1 内存泄漏定义

程序丢失堆内存控制权,无法释放已申请内存,并非物理内存消失,而是逻辑上永久占用。

两种泄漏场景

  1. 裸指针忘记 delete、异常分支漏释放;
  2. shared_ptr 循环引用导致资源无法释放。

8.2 内存泄漏危害

  1. 短期运行程序:进程退出后操作系统回收所有内存,危害极小;
  2. 长期后台服务、服务器、嵌入式程序:内存持续上涨,可用内存枯竭,程序卡顿、崩溃。

演示代码(短期程序无明显危害)

8.3 内存泄漏检测工具

  • Linux:valgrind;
  • Windows:VLD 内存泄漏检测工具。

8.4 避免内存泄漏两套方案

  1. 事前预防(推荐):全程使用智能指针管理所有堆资源;自定义资源遵循 RAII 封装;
  2. 事后排查:上线前使用泄漏检测工具扫描代码。

九、四大智能指针选型总结

| 指针类型 | 拷贝 | 移动 | 引用计数 | 适用场景 |
| auto_ptr | 转移所有权(悬空) | 支持 | 无 | 废弃,禁止使用 |
| unique_ptr | 不支持 | 支持 | 无 | 独占资源,性能最优,优先选用 |
| shared_ptr | 支持 | 支持 | 有 | 需要多对象共享同一份资源 |

weak_ptr 不支持 支持 不增加计数 配合 shared_ptr,解决循环引用
相关推荐
不相心 -w-1 小时前
Cmake的基础用法
linux·开发语言·c++
撑伞的鱼99371 小时前
C++开发用什么AI编程工具效果好?2026年实测横评(附选型指南)
开发语言·c++·ai编程·cursor
大数据张老师2 小时前
Typora 导出 Word 模版操作手册
开发语言·c#·word·typora
略略略咯咯2 小时前
stream流浅拷贝
开发语言·python
long3162 小时前
Java 团队入门到精通学习资料
java·开发语言
城管不管2 小时前
rabbitmq如何保证消息不丢失?解决方案又是什么?
开发语言·ai·面试·职场和发展·rabbitmq·php·agent
程序员良辰2 小时前
【TongWeb7】启动接近两分钟问题排查
java·开发语言·中间件
深色風信子2 小时前
Kotlin Bytedeco OpenCV 图像图像58 无缝克隆
开发语言·opencv·kotlin·bytedeco
朝阳392 小时前
react19【系列实用教程】实用组件封装
开发语言·前端·javascript