一、开篇:C++ 程序员的 “噩梦”—— 异常与内存泄漏

如果你是一名 C++ 开发者,一定对 “内存管理” 这个词又爱又恨。C++ 赋予我们直接操作内存的权力,但这份权力也带来了巨大的责任 —— 忘记释放内存会导致泄漏,重复释放会导致崩溃,而异常的出现,更是让内存管理的难度直线上升。

想象这样一个场景:你用new分配了一块内存,计划在使用后用delete释放。可偏偏在newdelete之间,程序抛出了一个未捕获的异常,正常的代码流程被打断,delete语句永远没有机会执行。久而久之,这些 “被遗忘” 的内存会不断占用系统资源,对于服务器、操作系统这类长期运行的程序来说,最终会导致性能下降甚至卡死。

这不是危言耸听,我们先看一段真实的 “问题代码”,感受下异常是如何导致内存泄漏的:

#include <iostream>
using namespace std;

// 除法函数:当除数为0时抛出异常
double Divide(int a, int b) {
    if (b == 0) {
        // 抛出异常:中断当前流程,跳转到最近的catch块
        throw "Divide by zero condition!";
    }
    return static_cast<double>(a) / static_cast<double>(b);
}

void Func() {
    // 动态分配两个int数组(各10个元素)
    int* array1 = new int[10];
    int* array2 = new int[10];

    int len, time;
    cout << "请输入len和time:";
    cin >> len >> time;

    // 调用Divide:若time=0,会抛出异常
    cout << Divide(len, time) << endl;

    // 正常情况下的释放逻辑
    cout << "释放array1:" << array1 << endl;
    delete[] array1;
    cout << "释放array2:" << array2 << endl;
    delete[] array2;
}

int main() {
    try {
        Func();
    }
    // 捕获const char*类型的异常
    catch (const char* errmsg) {
        cout << "捕获异常:" << errmsg << endl;
    }
    // 捕获其他标准异常
    catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    // 捕获所有未匹配的异常
    catch (...) {
        cout << "捕获未知异常" << endl;
    }

    return 0;
}

1.1 异常的 “破坏力”:打断代码流程

我们运行这段代码,若输入10 0(除数为 0),会发生什么?

  1. Func()中,array1array2成功用new分配内存;
  2. 调用Divide(10, 0),触发b==0,抛出异常"Divide by zero condition!"
  3. 异常会立即中断当前执行流Func()Divide之后的delete[]语句永远不会执行;
  4. 异常被main()中的catch (const char* errmsg)捕获,打印错误信息;
  5. 程序结束,但array1array2指向的内存从未被释放 —— 内存泄漏发生了!

1.2 手动解决?代码会变得 “丑陋且脆弱”

为了避免泄漏,你可能会想到 “在try-catch中手动释放内存”,比如这样修改Func()

void Func() {
    int* array1 = nullptr;
    int* array2 = nullptr;

    try {
        array1 = new int[10];
        array2 = new int[10]; // 若这里抛异常,array1已分配但未释放

        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;

        // 正常释放
        delete[] array1;
        delete[] array2;
    }
    catch (...) {
        // 异常时释放已分配的内存
        if (array1) {
            delete[] array1;
            cout << "异常时释放array1:" << array1 << endl;
        }
        if (array2) {
            delete[] array2;
            cout << "异常时释放array2:" << array2 << endl;
        }
        throw; // 重新抛出异常,让上层处理
    }
}

但这种方案有两个致命问题:

  • 代码冗余:每分配一个资源,都要在catch中手动判断并释放,资源越多,代码越臃肿;
  • 漏洞隐患:若new int[10]本身抛异常(比如内存不足),array1已分配但array2未分配,此时catch中需要精准判断哪些资源已分配,极易出错;
  • 扩展性差:若后续增加更多资源(如文件指针、网络连接),需要同步修改catch中的释放逻辑,维护成本极高。

显然,手动处理异常场景下的资源释放,既不优雅也不可靠。我们需要一种 “一劳永逸” 的方案 —— 这就是智能指针诞生的背景。

二、智能指针的 “基石”:RAII 思想

在介绍智能指针之前,我们必须先理解它的核心设计理念 ——RAII(Resource Acquisition Is Initialization,资源获取即初始化)。RAII 的本质是:用对象的生命周期管理资源

2.1 RAII 的核心逻辑

RAII 的核心思想可以拆解为 3 步:

  1. 资源获取即初始化:在对象构造时,获取需要管理的资源(如new内存、fopen文件、lock互斥锁);
  2. 资源随对象生存:只要对象存在,资源就有效,我们可以通过对象访问资源;
  3. 资源随对象销毁:当对象生命周期结束(如离开作用域、被 delete),调用对象的析构函数,自动释放资源。

由于 C++ 的析构函数一定会被执行(即使发生异常,栈上的对象也会被自动销毁,即 “栈展开”),RAII 能确保资源无论在正常流程还是异常流程下,都能被安全释放。

2.2 一个简单的 RAII 示例:管理动态数组

我们来实现一个管理int数组的 RAII 类ArrayManager,感受下 RAII 的魅力:

#include <iostream>
using namespace std;

class ArrayManager {
public:
    // 1. 构造时获取资源(new数组)
    explicit ArrayManager(size_t size) 
        : _ptr(new int[size]) 
        , _size(size) {
        cout << "ArrayManager:分配" << _size << "个int的数组,地址:" << _ptr << endl;
    }

    // 3. 析构时释放资源(delete[]数组)
    ~ArrayManager() {
        if (_ptr) {
            delete[] _ptr;
            cout << "ArrayManager:释放数组,地址:" << _ptr << endl;
        }
    }

    // 2. 提供访问资源的接口(像普通数组一样使用)
    int& operator[](size_t index) {
        if (index >= _size) {
            throw out_of_range("Array index out of range");
        }
        return _ptr[index];
    }

    // 禁止拷贝和赋值(避免资源重复释放,后续会讲)
    ArrayManager(const ArrayManager&) = delete;
    ArrayManager& operator=(const ArrayManager&) = delete;

private:
    int* _ptr;   // 管理的资源(动态数组)
    size_t _size;// 数组大小
};

现在,我们用ArrayManager改造之前的Func(),看看异常场景下是否还会泄漏:

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

void Func() {
    // 用RAII对象管理资源,构造时自动new
    ArrayManager array1(10);
    ArrayManager array2(10);

    // 像普通数组一样赋值
    for (size_t i = 0; i < 10; ++i) {
        array1[i] = i;
        array2[i] = 10 - i;
    }

    int len, time;
    cout << "请输入len和time:";
    cin >> len >> time;
    cout << Divide(len, time) << endl;

    // 无需手动delete!对象离开作用域时析构会自动释放
}

int main() {
    try {
        Func();
    }
    catch (const char* errmsg) {
        cout << "捕获异常:" << errmsg << endl;
    }
    catch (const out_of_range& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    catch (...) {
        cout << "捕获未知异常" << endl;
    }

    return 0;
}

2.3 RAII 的优势:异常下的 “自动保护”

运行代码,输入10 0(触发异常),输出如下:

可以看到:

  • 即使Divide抛出异常,array1array2作为栈上对象,会在Func()退出时被自动销毁;
  • 析构函数被调用,数组内存被成功释放,没有内存泄漏
  • 我们无需手动写任何释放逻辑,代码简洁且安全。

2.4 从 RAII 到智能指针:让 “资源管理者” 像指针一样

ArrayManager虽然解决了资源释放问题,但它只能管理int数组,不够通用。如果我们需要管理double数组、自定义类对象(如Date),难道要重复写多个 RAII 类吗?

当然不用!我们可以将 RAII 思想模板化,并让它模拟普通指针的行为(重载*->等运算符)—— 这就是智能指针的本质。

简单来说:智能指针 = RAII 模板 + 指针行为模拟

三、自定义智能指针:理解底层实现

在学习 C++ 标准库的智能指针之前,我们先手动实现一个简单的智能指针SmartPtr,掌握其核心逻辑。

3.1 目标:实现一个通用的智能指针

我们的SmartPtr需要满足以下需求:

  1. 模板化:支持管理任意类型的资源(如intDate、数组);
  2. RAII 特性:构造时接收new的指针,析构时delete资源;
  3. 指针行为:重载*(解引用)、->(访问成员)、[](数组访问),支持if判断是否为空;
  4. 避免资源重复释放:禁止拷贝和赋值(后续会优化为支持共享)。

3.2 实现SmartPtr

#include <iostream>
using namespace std;

// 模板参数T:智能指针管理的资源类型
template <typename T>
class SmartPtr {
public:
    // 1. 构造:获取资源(接收new的指针)
    explicit SmartPtr(T* ptr = nullptr) 
        : _ptr(ptr) {
        if (_ptr) {
            cout << "SmartPtr:管理资源,地址:" << _ptr << endl;
        } else {
            cout << "SmartPtr:空指针" << endl;
        }
    }

    // 2. 析构:释放资源(delete或delete[])
    ~SmartPtr() {
        if (_ptr) {
            // 注意:这里默认用delete,若管理数组需要特殊处理(后续讲删除器)
            delete _ptr;
            cout << "SmartPtr:释放资源,地址:" << _ptr << endl;
        }
    }

    // 3. 模拟指针行为:解引用(*)
    T& operator*() const {
        if (!_ptr) {
            throw runtime_error("SmartPtr:解引用空指针");
        }
        return *_ptr;
    }

    // 4. 模拟指针行为:访问成员(->)
    T* operator->() const {
        if (!_ptr) {
            throw runtime_error("SmartPtr:访问空指针成员");
        }
        return _ptr;
    }

    // 5. 模拟指针行为:数组访问([])
    T& operator[](size_t index) const {
        if (!_ptr) {
            throw runtime_error("SmartPtr:数组访问空指针");
        }
        // 注意:这里假设T是数组类型,实际需要特化(后续讲)
        return _ptr[index];
    }

    // 6. 支持if判断:是否管理资源(重载bool类型转换)
    explicit operator bool() const {
        return _ptr != nullptr;
    }

    // 7. 禁止拷贝和赋值(避免资源重复释放)
    SmartPtr(const SmartPtr<T>&) = delete;
    SmartPtr<T>& operator=(const SmartPtr<T>&) = delete;

    // (可选)获取原始指针(谨慎使用,避免绕过智能指针管理)
    T* get() const {
        return _ptr;
    }

private:
    T* _ptr; // 管理的原始资源指针
};

// 特化SmartPtr<T[]>:支持管理new[]分配的数组(析构用delete[])
template <typename T>
class SmartPtr<T[]> {
public:
    explicit SmartPtr(T* ptr = nullptr) 
        : _ptr(ptr) {
        if (_ptr) {
            cout << "SmartPtr<T[]>:管理数组资源,地址:" << _ptr << endl;
        } else {
            cout << "SmartPtr<T[]>:空指针" << endl;
        }
    }

    ~SmartPtr() {
        if (_ptr) {
            delete[] _ptr; // 数组用delete[]释放
            cout << "SmartPtr<T[]>:释放数组资源,地址:" << _ptr << endl;
        }
    }

    T& operator[](size_t index) const {
        if (!_ptr) {
            throw runtime_error("SmartPtr<T[]>:数组访问空指针");
        }
        return _ptr[index];
    }

    explicit operator bool() const {
        return _ptr != nullptr;
    }

    SmartPtr(const SmartPtr<T[]>&) = delete;
    SmartPtr<T[]>& operator=(const SmartPtr<T[]>&) = delete;

    T* get() const {
        return _ptr;
    }

private:
    T* _ptr;
};

3.3 测试SmartPtr:解决异常泄漏问题

我们用SmartPtr改造最初的异常示例,看看效果:

3.3.1 测试 1:管理普通对象(如Date

先定义一个Date类,方便观察构造和析构:

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
    }

    ~Date() {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
    }

    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

然后测试异常场景:

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

void Func() {
    // 用SmartPtr管理Date对象(new Date)
    SmartPtr<Date> sp1(new Date(2024, 5, 20));
    // 用SmartPtr<T[]>管理int数组(new int[10])
    SmartPtr<int[]> sp2(new int[10]);

    // 像普通指针一样使用
    sp1->Print(); // 访问成员:等价于 (*sp1).Print()
    (*sp1).Print(); // 解引用

    // 数组赋值
    for (size_t i = 0; i < 10; ++i) {
        sp2[i] = i;
    }

    // 触发异常
    int len = 10, time = 0;
    cout << Divide(len, time) << endl;

    // 无需手动delete!
}

int main() {
    try {
        Func();
    }
    catch (const char* errmsg) {
        cout << "捕获异常:" << errmsg << endl;
    }
    catch (const runtime_error& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    catch (...) {
        cout << "捕获未知异常" << endl;
    }

    return 0;
}
3.3.2 运行结果:资源被自动释放

结果分析:

  • 即使Divide抛出异常,sp1sp2Func()退出时被析构;
  • sp1的析构调用delete,触发Date的析构;
  • sp2SmartPtr<int[]>特化版)的析构调用delete[],释放数组;
  • 没有内存泄漏,完全解决了异常场景下的资源释放问题!

3.4 SmartPtr的不足:不支持共享

我们的SmartPtr禁止拷贝和赋值,这在 “多个指针需要共享同一个资源” 的场景下就不适用了。比如:

void Func() {
    SmartPtr<Date> sp1(new Date(2024, 5, 20));
    // SmartPtr<Date> sp2 = sp1; // 编译错误:拷贝构造被delete
}

如果我们需要sp1sp2都指向同一个Date对象,并且只有当最后一个智能指针析构时才释放Date,该怎么办?

这就需要引入引用计数机制 —— 这也是shared_ptr的核心原理。

四、C++ 标准库智能指针:从auto_ptrweak_ptr

C++ 标准库(<memory>头文件)提供了 4 种智能指针:auto_ptr(C++98,已弃用)、unique_ptr(C++11)、shared_ptr(C++11)、weak_ptr(C++11)。它们的核心都是 RAII,但在 “资源共享” 和 “使用场景” 上有本质区别。

4.1 被时代抛弃的auto_ptr:C++98 的妥协产物

auto_ptr是 C++98 标准中唯一的智能指针,它试图解决 “异常下的内存泄漏”,但设计上存在严重缺陷,C++11 后已被明确弃用(不推荐使用)。

4.1.1 auto_ptr的核心逻辑:拷贝时 “转移资源管理权”

auto_ptr的设计思路是:同一时间,只有一个auto_ptr能管理资源。当你拷贝一个auto_ptr时,原auto_ptr会失去资源管理权(变为空指针),新auto_ptr接管资源。

4.1.2 代码示例:auto_ptr的 “坑”
#include <iostream>
#include <memory> // 标准库智能指针头文件
using namespace std;

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
    }

    ~Date() {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
    }

    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // 用auto_ptr管理Date对象
    auto_ptr<Date> ap1(new Date(2024, 5, 20));
    ap1->Print(); // 正常访问

    // 拷贝ap1到ap2:ap1失去管理权,变为空指针
    auto_ptr<Date> ap2 = ap1;
    ap2->Print(); // 正常访问(ap2接管资源)

    // 危险!ap1已为空,解引用会崩溃
    // ap1->Print(); // 运行时错误:访问空指针

    return 0;
}
4.1.3 auto_ptr的致命问题
  1. 拷贝后原对象悬空:拷贝后原auto_ptr变为空,若后续再访问原对象,会导致未定义行为(崩溃);
  2. 不支持容器存储:若将auto_ptr存入vector,容器在扩容时会拷贝元素,导致原auto_ptr悬空,后续访问容器元素会崩溃;
  3. 逻辑反直觉:拷贝操作本应是 “复制一份资源” 或 “共享资源”,但auto_ptr的拷贝却是 “转移资源”,不符合程序员的直觉。

因此,C++11 及以后,永远不要使用auto_ptr,改用unique_ptrshared_ptr

4.2 独占资源的unique_ptrauto_ptr的 “正确替代品”

unique_ptr是 C++11 为 “独占资源” 场景设计的智能指针,它的核心特点是:禁止拷贝,支持移动,确保同一时间只有一个unique_ptr管理资源。

4.2.1 unique_ptr的核心特性
  • 独占性:通过删除拷贝构造和拷贝赋值运算符,禁止拷贝unique_ptr
  • 支持移动:允许通过std::move转移资源管理权(转移后原unique_ptr变为空);
  • 高效轻量:无额外开销(不维护引用计数),性能接近普通指针;
  • 支持数组:提供unique_ptr<T[]>特化版本,析构时自动用delete[]释放数组;
  • 支持删除器:可自定义资源释放逻辑(如管理文件指针、互斥锁)。
4.2.2 代码示例 1:unique_ptr的基本使用
#include <iostream>
#include <memory>
using namespace std;

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
    }

    ~Date() {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
    }

    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // 1. 管理普通对象
    unique_ptr<Date> up1(new Date(2024, 5, 20));
    up1->Print(); // 正常访问

    // 2. 禁止拷贝:编译错误(拷贝构造被delete)
    // unique_ptr<Date> up2 = up1; 
    // unique_ptr<Date> up3(up1);

    // 3. 支持移动:用std::move转移管理权
    unique_ptr<Date> up4 = move(up1);
    up4->Print(); // up4接管资源
    if (!up1) { // 移动后up1为空
        cout << "up1已为空" << endl;
    }

    // 4. 管理数组:使用unique_ptr<T[]>特化版
    unique_ptr<int[]> up5(new int[10]);
    for (size_t i = 0; i < 10; ++i) {
        up5[i] = i; // 数组访问
    }

    // 5. 支持if判断:是否为空
    if (up4) {
        cout << "up4管理资源" << endl;
    }

    return 0;
}

运行结果:

4.2.3 代码示例 2:unique_ptr的删除器

默认情况下,unique_ptrdelete释放资源(数组用delete[])。若资源不是用new分配的(如fopen的文件指针、malloc的内存),需要自定义删除器(一个可调用对象,如函数、仿函数、lambda)。

案例 1:管理文件指针(fopen/fclose
#include <iostream>
#include <memory>
#include <cstdio> // fopen, fclose
using namespace std;

// 自定义删除器:仿函数(释放文件指针)
struct FileDeleter {
    void operator()(FILE* fp) const {
        if (fp) {
            fclose(fp);
            cout << "FileDeleter:关闭文件,指针:" << fp << endl;
        }
    }
};

int main() {
    // 用unique_ptr管理文件指针,指定删除器为FileDeleter
    unique_ptr<FILE, FileDeleter> up_file(fopen("test.txt", "w"));
    if (up_file) {
        fprintf(up_file.get(), "Hello, unique_ptr!"); // get()获取原始指针
        cout << "文件写入成功" << endl;
    }

    // 程序结束时,up_file析构,自动调用FileDeleter释放文件
    return 0;
}
案例 2:用 lambda 作为删除器
#include <iostream>
#include <memory>
#include <cstdlib> // malloc, free
using namespace std;

int main() {
    // 用malloc分配内存,自定义lambda删除器(用free释放)
    unique_ptr<int, void(*)(int*)> up_malloc(
        static_cast<int*>(malloc(sizeof(int) * 10)),
        [](int* ptr) {
            if (ptr) {
                free(ptr);
                cout << "lambda删除器:free内存,地址:" << ptr << endl;
            }
        }
    );

    if (up_malloc) {
        up_malloc.get()[0] = 100; // 访问内存
        cout << "up_malloc[0] = " << up_malloc.get()[0] << endl;
    }

    return 0;
}
4.2.4 unique_ptr的适用场景
  • 独占资源:资源只需一个所有者,无需共享(如函数返回动态分配的对象、管理局部动态数组);
  • 高效场景:对性能要求高,不希望引用计数的额外开销;
  • 容器元素:可存入vector等容器(需用move转移所有权)。

4.3 支持共享的shared_ptr:引用计数的艺术

shared_ptr是 C++11 为 “共享资源” 场景设计的智能指针,它的核心是引用计数机制,允许多个shared_ptr共享同一个资源,只有当最后一个shared_ptr析构时,才释放资源。

4.3.1 shared_ptr的核心原理:引用计数

引用计数的逻辑可以拆解为 5 步:

  1. 初始化:当创建一个shared_ptr时,会同时在堆上分配一个 “引用计数器”,初始值为 1(表示当前有 1 个shared_ptr管理资源);
  2. 拷贝:当拷贝一个shared_ptr(如sp2 = sp1)时,两个shared_ptr指向同一个资源,同时引用计数器加 1(count += 1);
  3. 赋值:当一个shared_ptr赋值给另一个(如sp3 = sp2)时,先将原sp3的引用计数器减 1(若减到 0 则释放原资源),再将sp2的引用计数器加 1;
  4. 析构:当一个shared_ptr析构时,引用计数器减 1(count -= 1);
  5. 释放资源:若引用计数器减到 0,说明当前shared_ptr是最后一个管理资源的对象,此时释放资源和引用计数器。
4.3.2 引用计数的示意图
// 初始:sp1管理资源,引用计数=1
shared_ptr<Date> sp1(new Date(2024,5,20));
资源(Date)<-- sp1
                |
引用计数器(1)<--

// 拷贝:sp2 = sp1,引用计数=2
shared_ptr<Date> sp2 = sp1;
资源(Date)<-- sp1
                |
                sp2
                |
引用计数器(2)<--

// 析构sp1:引用计数=1,不释放资源
sp1.~shared_ptr();
资源(Date)<-- sp2
                |
引用计数器(1)<--

// 析构sp2:引用计数=0,释放资源和计数器
sp2.~shared_ptr();
资源(Date)被释放
引用计数器被释放
4.3.3 代码示例 1:shared_ptr的基本使用
#include <iostream>
#include <memory>
using namespace std;

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
    }

    ~Date() {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
    }

    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // 1. 初始化:sp1管理资源,引用计数=1
    shared_ptr<Date> sp1(new Date(2024, 5, 20));
    cout << "sp1引用计数:" << sp1.use_count() << endl; // 输出1
    sp1->Print();

    // 2. 拷贝:sp2 = sp1,引用计数=2
    shared_ptr<Date> sp2 = sp1;
    cout << "sp1引用计数:" << sp1.use_count() << endl; // 输出2
    cout << "sp2引用计数:" << sp2.use_count() << endl; // 输出2
    sp2->Print();

    // 3. 赋值:sp3 = sp2,引用计数=3
    shared_ptr<Date> sp3;
    sp3 = sp2;
    cout << "sp1引用计数:" << sp1.use_count() << endl; // 输出3
    cout << "sp3引用计数:" << sp3.use_count() << endl; // 输出3

    // 4. 重置sp1:sp1不再管理资源,引用计数=2
    sp1.reset(); // 等价于 sp1 = nullptr;
    cout << "sp1是否为空:" << (sp1 ? "否" : "是") << endl; // 输出“是”
    cout << "sp2引用计数:" << sp2.use_count() << endl; // 输出2

    // 5. 离开作用域:sp2、sp3析构,引用计数依次减为1、0,释放资源
    cout << "main函数结束,开始析构shared_ptr" << endl;

    return 0;
}

运行结果:

4.3.4 代码示例 2:make_shared—— 更安全的shared_ptr创建方式

除了用new构造shared_ptr,C++11 还提供了std::make_shared函数,它能直接用对象的构造参数创建shared_ptr,优势如下:

  1. 减少内存分配次数new构造需要分配 “资源内存” 和 “引用计数器内存” 两次;make_shared一次性分配一块内存,同时存储资源和计数器,更高效;
  2. 避免内存泄漏:若new成功但shared_ptr构造失败(如内存不足),new的资源会泄漏;make_shared在分配内存时原子操作,不会泄漏;
  3. 代码更简洁:无需手动写new
#include <iostream>
#include <memory>
using namespace std;

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
    }

    ~Date() {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // 用make_shared创建shared_ptr:直接传入Date的构造参数
    shared_ptr<Date> sp1 = make_shared<Date>(2024, 5, 20);
    cout << "sp1引用计数:" << sp1.use_count() << endl; // 1

    // 用auto简化类型
    auto sp2 = make_shared<Date>(2024, 5, 21);
    cout << "sp2引用计数:" << sp2.use_count() << endl; // 1

    return 0;
}

注意make_shared的唯一缺点是,若资源的析构需要很长时间,引用计数器会和资源一起占用内存,直到最后一个weak_ptr(后续讲)也析构。但在大多数场景下,make_shared仍是首选。

4.3.5 代码示例 3:shared_ptr的删除器

unique_ptr类似,shared_ptr也支持自定义删除器,且语法更简洁(删除器作为构造函数的第二个参数,无需指定模板参数)。

案例 1:管理new[]数组
#include <iostream>
#include <memory>
using namespace std;

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
    }

    ~Date() {
        cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
    }
};

int main() {
    // 方式1:用lambda作为删除器,释放new[]数组
    shared_ptr<Date> sp1(new Date[3]{Date(2024,5,20), Date(2024,5,21), Date(2024,5,22)},
        [](Date* ptr) {
            delete[] ptr;
            cout << "lambda删除器:释放Date数组" << endl;
        }
    );

    // 方式2:C++17后,shared_ptr<T[]>特化版支持直接管理数组(无需删除器)
    shared_ptr<Date[]> sp2(new Date[2]{Date(2024,5,23), Date(2024,5,24)});
    sp2[0]; // 支持数组访问

    return 0;
}

运行结果:

案例 2:管理互斥锁
#include <iostream>
#include <memory>
#include <mutex>
using namespace std;

int main() {
    mutex mtx;
    // 用shared_ptr管理互斥锁的lock/unlock
    shared_ptr<mutex> sp_mtx(&mtx,
        [](mutex* p) {
            if (p->try_lock()) {
                p->unlock();
                cout << "删除器:解锁互斥锁" << endl;
            }
        }
    );

    // 锁定互斥锁
    sp_mtx->lock();
    cout << "互斥锁已锁定" << endl;

    // 离开作用域时,sp_mtx析构,自动解锁
    return 0;
}
4.3.6 shared_ptr的陷阱:循环引用

shared_ptr虽然强大,但存在一个致命问题 ——循环引用,会导致内存泄漏。

什么是循环引用?

当两个或多个shared_ptr互相引用,形成 “闭环”,每个shared_ptr的引用计数都无法减到 0,最终资源无法释放,导致内存泄漏。

代码示例:链表节点的循环引用
#include <iostream>
#include <memory>
using namespace std;

// 链表节点:next和prev都用shared_ptr管理
struct ListNode {
    int _data;
    shared_ptr<ListNode> _next; // 指向后一个节点
    shared_ptr<ListNode> _prev; // 指向前一个节点

    ListNode(int data) : _data(data) {
        cout << "ListNode(" << _data << ") 构造" << endl;
    }

    ~ListNode() {
        cout << "ListNode(" << _data << ") 析构" << endl;
    }
};

int main() {
    // 创建两个节点
    shared_ptr<ListNode> n1 = make_shared<ListNode>(10);
    shared_ptr<ListNode> n2 = make_shared<ListNode>(20);

    cout << "n1引用计数:" << n1.use_count() << endl; // 1
    cout << "n2引用计数:" << n2.use_count() << endl; // 1

    // 循环引用:n1->_next指向n2,n2->_prev指向n1
    n1->_next = n2;
    n2->_prev = n1;

    cout << "n1引用计数:" << n1.use_count() << endl; // 2(n1本身 + n2->_prev)
    cout << "n2引用计数:" << n2.use_count() << endl; // 2(n2本身 + n1->_next)

    // main函数结束,n1和n2析构
    cout << "main函数结束" << endl;

    return 0;
}
循环引用导致的泄漏

问题分析

  1. main结束时,n1n2析构,它们的引用计数各减 1,变为 1;
  2. n1的引用计数由n2->_prev维持(1),n2的引用计数由n1->_next维持(1);
  3. 由于n1n2互相引用,引用计数永远无法减到 0,ListNode(10)ListNode(20)永远不会析构 —— 内存泄漏!
解决方案:weak_ptr

为了解决shared_ptr的循环引用问题,C++11 引入了weak_ptr—— 一种 “弱引用” 智能指针,它不管理资源,也不增加shared_ptr的引用计数。

4.4 解决循环引用的weak_ptr:弱引用的 “救星”

weak_ptrshared_ptr的 “辅助工具”,它本身不具备 RAII 特性,不能直接管理资源,只能绑定到shared_ptr上,观察shared_ptr管理的资源。

4.4.1 weak_ptr的核心特性
  • 不增加引用计数:绑定shared_ptr时,不会增加shared_ptr的引用计数;
  • 不管理资源weak_ptr析构时,不会影响资源的释放;
  • 无法直接访问资源weak_ptr没有重载*->,必须通过lock()函数获取shared_ptr后,才能访问资源;
  • 支持过期检查:通过expired()函数检查绑定的shared_ptr是否已释放资源(若已释放,返回true)。
4.4.2 用weak_ptr解决循环引用

修改链表节点的代码,将_next_prev改为weak_ptr

#include <iostream>
#include <memory>
using namespace std;

// 链表节点:next和prev用weak_ptr管理
struct ListNode {
    int _data;
    weak_ptr<ListNode> _next; // 弱引用:不增加引用计数
    weak_ptr<ListNode> _prev; // 弱引用:不增加引用计数

    ListNode(int data) : _data(data) {
        cout << "ListNode(" << _data << ") 构造" << endl;
    }

    ~ListNode() {
        cout << "ListNode(" << _data << ") 析构" << endl;
    }
};

int main() {
    shared_ptr<ListNode> n1 = make_shared<ListNode>(10);
    shared_ptr<ListNode> n2 = make_shared<ListNode>(20);

    cout << "n1引用计数:" << n1.use_count() << endl; // 1
    cout << "n2引用计数:" << n2.use_count() << endl; // 1

    // 绑定weak_ptr:不增加引用计数
    n1->_next = n2; // weak_ptr = shared_ptr:合法,不增计数
    n2->_prev = n1; // weak_ptr = shared_ptr:合法,不增计数

    cout << "n1引用计数:" << n1.use_count() << endl; // 1(weak_ptr不增计数)
    cout << "n2引用计数:" << n2.use_count() << endl; // 1(weak_ptr不增计数)

    // 通过weak_ptr访问资源:需先lock()获取shared_ptr
    if (shared_ptr<ListNode> sp = n1->_next.lock()) {
        cout << "n1->_next的data:" << sp->_data << endl; // 输出20
    }

    cout << "main函数结束" << endl;

    return 0;
}
4.4.3 运行结果:资源正常释放

问题解决分析

  1. n1->_nextn2->_prevweak_ptr,绑定n2n1时不增加引用计数,因此n1n2的引用计数始终为 1;
  2. main结束时,n1n2析构,引用计数减到 0,ListNode(10)ListNode(20)被正常释放;
  3. 循环引用被打破,内存泄漏问题解决!
4.4.4 weak_ptr的其他用法
用法 1:检查资源是否过期
#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> sp = make_shared<int>(100);
    weak_ptr<int> wp = sp;

    cout << "wp是否过期:" << (wp.expired() ? "是" : "否") << endl; // 否
    cout << "wp绑定的引用计数:" << wp.use_count() << endl; // 1

    // 释放sp的资源
    sp.reset();

    cout << "wp是否过期:" << (wp.expired() ? "是" : "否") << endl; // 是
    cout << "wp绑定的引用计数:" << wp.use_count() << endl; // 0

    // 过期后lock()返回空shared_ptr
    if (shared_ptr<int> sp2 = wp.lock()) {
        cout << "sp2管理的资源:" << *sp2 << endl;
    } else {
        cout << "wp已过期,无法获取资源" << endl; // 输出此句
    }

    return 0;
}
用法 2:在回调函数中避免循环引用

在面向对象编程中,若对象 A 的回调函数持有对象 B 的shared_ptr,对象 B 又持有对象 A 的shared_ptr,会形成循环引用。此时可将对象 B 中的shared_ptr改为weak_ptr

#include <iostream>
#include <memory>
#include <functional>
using namespace std;

class B; // 前向声明

class A {
public:
    A() { cout << "A 构造" << endl; }
    ~A() { cout << "A 析构" << endl; }

    // 存储B的弱引用
    weak_ptr<B> _wp_b;

    // 回调函数:通过weak_ptr访问B
    void Callback() {
        if (shared_ptr<B> sp_b = _wp_b.lock()) {
            cout << "A::Callback:访问B的资源" << endl;
        } else {
            cout << "A::Callback:B已释放" << endl;
        }
    }
};

class B {
public:
    B() { cout << "B 构造" << endl; }
    ~B() { cout << "B 析构" << endl; }

    // 存储A的强引用(shared_ptr)
    shared_ptr<A> _sp_a;

    // 调用A的回调
    void CallA() {
        _sp_a->Callback();
    }
};

int main() {
    shared_ptr<A> sp_a = make_shared<A>();
    shared_ptr<B> sp_b = make_shared<B>();

    // 绑定:A持有B的weak_ptr,B持有A的shared_ptr(无循环引用)
    sp_a->_wp_b = sp_b;
    sp_b->_sp_a = sp_a;

    // 调用B的方法,间接调用A的回调
    sp_b->CallA();

    return 0;
}

运行结果:

可以看到,AB都能正常析构,没有循环引用。

五、shared_ptr 的线程安全问题:你必须知道的细节

在实际开发中,多线程场景极为常见,而shared_ptr作为 C++ 中最常用的智能指针,其线程安全特性很容易被误解 —— 很多开发者认为 “shared_ptr是线程安全的,用它管理资源就无需担心多线程问题”,但事实并非如此。shared_ptr的线程安全需要拆解为两个独立层面分析,二者的责任边界完全不同,必须清晰区分才能避免踩坑。

5.1 先明确:shared_ptr 线程安全的 “责任边界”

C++ 标准对shared_ptr的线程安全有明确规定,核心可概括为 **“管自己,不管资源”**:

  1. 自身引用计数的线程安全shared_ptr内部的引用计数存储在堆上,多线程对同一个shared_ptr进行拷贝、赋值、析构(本质是修改引用计数)时,引用计数的增减操作是原子的,不会出现竞争条件(标准库已内部实现保护);
  2. 指向资源的线程安全shared_ptr仅负责管理资源的生命周期,不保证对 “资源本身” 的访问安全。若多线程同时通过shared_ptr读取或修改资源(比如修改资源的成员变量),必须由用户手动保证线程安全,否则会出现数据竞争。

简单来说:shared_ptr只保证 “自己的引用计数不出错”,至于它指向的对象是否安全,完全是用户的责任。

5.2 代码示例 1:引用计数的线程安全(标准库已保证)

首先通过一个示例验证 “引用计数的线程安全”—— 多线程频繁拷贝shared_ptr,观察最终引用计数是否正确。

5.2.1 测试代码

#include <iostream>
#include <memory>
#include <thread>
#include <vector>
using namespace std;

// 测试用资源类:仅用于观察构造和析构
class TestResource {
public:
    TestResource() {
        cout << "TestResource 构造(资源地址:" << this << ")" << endl;
    }
    ~TestResource() {
        cout << "TestResource 析构(资源地址:" << this << ")" << endl;
    }
};

int main() {
    // 创建一个shared_ptr,管理TestResource资源
    shared_ptr<TestResource> sp = make_shared<TestResource>();
    const size_t thread_count = 8;  // 启动8个线程
    const size_t loop_count = 100000; // 每个线程拷贝10万次

    // 线程函数:循环拷贝shared_ptr,修改引用计数
    auto copy_task = [&]() {
        for (size_t i = 0; i < loop_count; ++i) {
            shared_ptr<TestResource> temp_sp = sp; // 拷贝:引用计数+1
            // 临时变量temp_sp析构时,引用计数自动-1
        }
    };

    // 创建并启动所有线程
    vector<thread> threads;
    for (size_t i = 0; i < thread_count; ++i) {
        threads.emplace_back(copy_task);
    }

    // 等待所有线程执行完毕
    for (auto& t : threads) {
        t.join();
    }

    // 最终引用计数:仅sp本身持有资源,应为1
    cout << "最终引用计数:" << sp.use_count() << endl;

    return 0;
}

5.2.2 运行结果

5.2.3 结果分析

  • 8 个线程各拷贝 10 万次,意味着引用计数被频繁执行 “+1”(拷贝时)和 “-1”(临时变量析构时),总计 160 万次修改;
  • 最终引用计数仍为 1,说明引用计数的增减是原子操作,没有出现 “计数错乱”(比如少加 1、少减 1);
  • 若引用计数不是线程安全的,最终计数可能小于 1(导致资源提前释放)或大于 1(导致资源泄漏),但标准库的实现避免了这些问题。

5.3 代码示例 2:资源的线程不安全(用户需手动处理)

既然引用计数是安全的,那为什么还会有 “shared_ptr 线程安全问题”?核心在于资源本身的访问安全。下面通过一个 “多线程修改共享计数器” 的示例,展示资源的线程不安全问题。

5.3.1 测试代码(无锁保护,线程不安全)

#include <iostream>
#include <memory>
#include <thread>
#include <vector>
using namespace std;

// 共享资源:计数器(用于多线程修改)
struct SharedCounter {
    int count = 0; // 待修改的共享变量

    // 自增函数:无锁保护
    void increment() {
        count++; // 非原子操作:拆解为「读取count→加1→写回count」三步
    }
};

int main() {
    // shared_ptr管理SharedCounter资源
    shared_ptr<SharedCounter> sp_counter = make_shared<SharedCounter>();
    const size_t thread_count = 2;    // 2个线程
    const size_t loop_count = 100000; // 每个线程自增10万次

    // 线程函数:循环调用increment修改共享变量
    auto increment_task = [&]() {
        for (size_t i = 0; i < loop_count; ++i) {
            sp_counter->increment(); // 多线程同时修改count
        }
    };

    // 启动线程
    thread t1(increment_task);
    thread t2(increment_task);

    // 等待线程结束
    t1.join();
    t2.join();

    // 预期结果:2*100000=200000;实际结果会小于200000
    cout << "最终计数结果:" << sp_counter->count << endl;

    return 0;
}

5.3.2 运行结果(示例)

5.3.3 问题分析:为什么计数会 “丢失”?

核心原因是count++不是原子操作,多线程执行时会出现 “数据竞争”:

  1. 线程 1 读取count=100后,还没来得及加 1,线程 2 也读取到count=100
  2. 线程 1 将100+1=101写回 count,此时 count=101;
  3. 线程 2 也将100+1=101写回 count,覆盖了线程 1 的结果;
  4. 原本应两次自增(count 从 100→102),最终只实现了一次(100→101),导致 “计数丢失”。

这里shared_ptr本身没有问题(引用计数始终正确),但它指向的SharedCounter资源因为没有锁保护,出现了线程安全问题 —— 这就是 “shared_ptr不管资源安全” 的具体体现。

5.4 解决方案:为共享资源添加锁保护

要解决资源的线程不安全问题,最直接的方式是为资源的 “临界区操作”(如increment)加锁,确保同一时间只有一个线程能修改共享变量。C++ 标准库提供的std::mutex(互斥锁)是常用工具。

5.4.1 改进代码(加锁保护,线程安全)

#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <mutex> // 引入互斥锁头文件
using namespace std;

struct SharedCounter {
    int count = 0;
    mutex mtx; // 互斥锁:保护count的修改

    void increment() {
        // unique_lock:构造时加锁,析构时自动解锁(避免忘记解锁导致死锁)
        unique_lock<mutex> lock(mtx); 
        count++; // 临界区:此时只有当前线程能执行
    }
};

int main() {
    shared_ptr<SharedCounter> sp_counter = make_shared<SharedCounter>();
    const size_t thread_count = 2;
    const size_t loop_count = 100000;

    auto increment_task = [&]() {
        for (size_t i = 0; i < loop_count; ++i) {
            sp_counter->increment();
        }
    };

    thread t1(increment_task);
    thread t2(increment_task);

    t1.join();
    t2.join();

    // 预期结果:200000(稳定)
    cout << "最终计数结果:" << sp_counter->count << endl;

    return 0;
}

5.4.2 运行结果

5.4.3 原理分析:互斥锁如何保证安全?

  • 互斥性:当线程 1 调用increment()时,unique_lock<mutex>会锁定mtx,此时线程 2 再调用increment()会阻塞(等待锁释放);
  • 自动解锁unique_lock是 RAII 风格的锁管理工具,当它离开作用域(即increment()函数结束)时,会自动调用mtx.unlock(),避免 “忘记解锁” 导致的死锁;
  • 临界区保护count++被包裹在 “加锁→解锁” 之间,成为 “原子操作”,彻底解决了数据竞争。

5.5 深入:自定义 shared_ptr 时的引用计数安全

虽然标准库的shared_ptr已保证引用计数安全,但了解其底层实现逻辑(尤其是 “如何保证引用计数原子性”),能帮助我们更深刻理解线程安全的本质。下面通过 “自定义简易 shared_ptr” 的示例,展示引用计数安全的实现方式。

5.5.1 问题背景:朴素引用计数的线程不安全

若自定义shared_ptr时,用普通int*存储引用计数,多线程修改会出现问题:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

template <typename T>
class UnsafeSharedPtr {
public:
    // 构造:初始化资源和引用计数(普通int*)
    explicit UnsafeSharedPtr(T* ptr = nullptr) 
        : _ptr(ptr)
        , _pcount(new int(1)) {} // 引用计数初始为1

    // 拷贝构造:引用计数+1(非原子操作)
    UnsafeSharedPtr(const UnsafeSharedPtr<T>& other) 
        : _ptr(other._ptr)
        , _pcount(other._pcount) {
        (*_pcount)++; // 普通int自增:线程不安全
    }

    // 析构:引用计数-1,为0时释放资源
    ~UnsafeSharedPtr() {
        if (--(*_pcount) == 0) { // 普通int自减:线程不安全
            delete _ptr;
            delete _pcount;
            cout << "资源和引用计数释放" << endl;
        }
    }

private:
    T* _ptr;     // 指向资源的指针
    int* _pcount;// 指向引用计数的指针(普通int)
};

// 测试用资源
struct TestObj {};

int main() {
    UnsafeSharedPtr<TestObj> sp(new TestObj());
    const size_t thread_count = 5;
    const size_t loop_count = 10000;

    auto copy_task = [&]() {
        for (size_t i = 0; i < loop_count; ++i) {
            UnsafeSharedPtr<TestObj> temp = sp; // 多线程拷贝
        }
    };

    vector<thread> threads;
    for (size_t i = 0; i < thread_count; ++i) {
        threads.emplace_back(copy_task);
    }

    for (auto& t : threads) {
        t.join();
    }

    return 0;
}
问题表现

程序可能崩溃或出现 “资源提前释放”:

  • 多线程同时修改*_pcount时,可能出现 “引用计数提前减为 0”(比如线程 A 和 B 同时读取*_pcount=1,A 先减为 0 并释放资源,B 再减为 - 1,后续操作访问已释放内存);
  • 最终可能导致delete已释放的_ptr_pcount,触发程序崩溃。

5.5.2 解决方案:用原子类型保证引用计数安全

C++11 提供的std::atomic是原子类型,其++--等操作是原子的,不会被线程打断。将引用计数从int*改为atomic<int>*,即可解决线程安全问题:

#include <iostream>
#include <thread>
#include <vector>
#include <atomic> // 引入原子类型头文件
using namespace std;

template <typename T>
class SafeSharedPtr {
public:
    explicit SafeSharedPtr(T* ptr = nullptr) 
        : _ptr(ptr)
        , _pcount(new atomic<int>(1)) {} // 引用计数改为atomic<int>*

    SafeSharedPtr(const SafeSharedPtr<T>& other) 
        : _ptr(other._ptr)
        , _pcount(other._pcount) {
        (*_pcount)++; // 原子自增:线程安全
    }

    ~SafeSharedPtr() {
        // 原子自减:先减1,再判断是否为0
        if (--(*_pcount) == 0) {
            delete _ptr;
            delete _pcount;
            cout << "资源和引用计数释放" << endl;
        }
    }

private:
    T* _ptr;              // 指向资源的指针
    atomic<int>* _pcount; // 指向原子引用计数的指针
};

// 测试用资源
struct TestObj {};

int main() {
    SafeSharedPtr<TestObj> sp(new TestObj());
    const size_t thread_count = 5;
    const size_t loop_count = 10000;

    auto copy_task = [&]() {
        for (size_t i = 0; i < loop_count; ++i) {
            SafeSharedPtr<TestObj> temp = sp;
        }
    };

    vector<thread> threads;
    for (size_t i = 0; i < thread_count; ++i) {
        threads.emplace_back(copy_task);
    }

    for (auto& t : threads) {
        t.join();
    }

    cout << "程序正常结束" << endl;
    return 0;
}
运行结果
原理分析
  • atomic<int>的操作由编译器和硬件保证 “不可分割”,多线程同时执行(*_pcount)++--(*_pcount)时,不会出现竞争;
  • 即使 5 个线程各拷贝 1 万次,引用计数的修改也能准确执行,最终析构时--(*_pcount)会正确减为 0,安全释放资源。

5.6 shared_ptr 线程安全的核心要点

通过以上分析,我们可以将shared_ptr的线程安全总结为 **“两个层面,两种责任”**:

1. 引用计数层面(标准库负责安全)

  • 标准库shared_ptr的引用计数基于原子类型实现,多线程对同一shared_ptr的拷贝、赋值、析构不会导致计数错误;
  • 自定义shared_ptr时,需将引用计数改为atomic<int>*或加锁,避免线程安全问题。

2. 资源访问层面(用户负责安全)

  • shared_ptr不保证其指向资源的访问安全,多线程操作资源时,必须通过std::mutex(互斥锁)、std::atomic(原子变量)等工具手动保护;
  • 忽略资源的线程安全会导致数据竞争,表现为结果错误、程序崩溃等不可预期行为。

简言之:使用shared_ptr时,无需担心 “引用计数错乱”,但必须关注 “资源是否被多线程安全访问”—— 前者是标准库的责任,后者是开发者的责任。

六、总结:智能指针的使用建议与价值

智能指针是 C++ 解决内存管理问题的 “核心工具”,其本质是 RAII 思想的工程化实现。通过本文的梳理,我们可总结出以下使用建议:

  1. 优先选择合适的智能指针

    • 若资源无需共享,用unique_ptr(高效、无额外开销);
    • 若资源需要共享,用shared_ptr(灵活、支持拷贝);
    • 若存在shared_ptr循环引用,用weak_ptr(弱引用、不增计数);
    • 永远不要使用auto_ptr(已弃用,存在悬空风险)。
  2. 注意 shared_ptr 的细节

    • make_shared创建shared_ptr(更高效、避免泄漏);
    • 明确shared_ptr的线程安全边界(引用计数安全,资源访问需手动加锁);
    • 避免用shared_ptr管理非new分配的资源(需自定义删除器,确保释放逻辑正确)。
  3. 理解 RAII 的本质

    • 智能指针的核心是 “对象生命周期管理资源”,不仅限于内存,还可用于文件、锁、网络连接等;
    • 自定义资源管理类时,遵循 RAII 思想,确保 “获取即初始化,销毁即释放”。

智能指针的出现,让 C++ 开发者从 “手动管理内存的繁琐与风险” 中解放出来,显著提升了代码的安全性和可维护性。掌握智能指针的原理与用法,是每个 C++ 开发者的必备技能,也是写出高质量 C++ 代码的基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐