C ++ 智能指针

在 C++ 程序开发中,内存泄漏与异常安全问题始终是开发者的"隐形陷阱"。手动通过 new/delete 管理动态内存,不仅易因遗漏释放、异常跳转导致资源泄漏,更会让代码逻辑冗余且难以维护。而智能指针的出现,正是以 RAII(资源获取即初始化)思想为核心,通过封装原始指针、利用对象生命周期自动管理资源,从根源上解决内存管理痛点,成为现代 C++ 开发中不可或缺的工具。


1. 智能指针的使用场景分析

问题场景:手动管理内存导致的泄漏风险

手动 new/delete 时,若发生异常,delete 可能无法执行,造成内存泄漏;且多层 new 抛异常时,异常捕获逻辑会非常复杂。

cpp 复制代码
// 除0时抛出异常
double Divide(int a, int b)
{
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    else
    {
        return (double)a / (double)b;
    }
}

void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10]; // 若此处抛异常,array1 无法释放

    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 捕获异常后手动释放内存
        cout << "delete []" << array1 << endl;
        delete[] array1;
        cout << "delete []" << array2 << endl;
        delete[] array2;
        throw; // 重新抛出异常
    }

    // 正常流程也要手动释放
    cout << "delete []" << array1 << endl;
    delete[] array1;
    cout << "delete []" << array2 << endl;
    delete[] array2;
}

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

具体风险点

  1. Func 函数内的风险:

int* array1 = new int[10]; int* array2 = new int[10]; 连续分配了两块数组内存。

如果在 new int[10] 过程中抛异常(例如内存不足),或者 Divide(len, time) 抛出除0异常,程序会跳转到 catch (...) 块。

在 catch 块中,虽然释放了 array1 和 array2,但如果是第一次 new 就失败,array2 根本没有被赋值,此时执行 delete[] array2 会导致未定义行为(程序崩溃)。

  1. 当 array2 = new int[10] 抛出异常(比如内存不足)时:

array1 已经成功分配内存

程序会直接跳转到最近的异常处理块,不会执行 try 内部的代码,也不会执行 Func 末尾的 delete[]

此时 array1 没有被释放,会造成内存泄漏; array2 还没成功创建,不需要释放

关键问题:new 操作在 try 块之前,异常发生时不会进入 catch 块,array1 无法被释放。

解决方案 1:把 new 放进 try 块(手动管理)

cpp 复制代码
void Func()
{
    int* array1 = nullptr;
    int* array2 = nullptr;

    try
    {
        array1 = new int[10];
        array2 = new int[10]; // 若失败,array1 已分配,会在 catch 中释放

        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        cout << "delete []" << array1 << endl;
        delete[] array1;   // 安全释放已分配的 array1
        cout << "delete []" << array2 << endl;
        delete[] array2;   // array2 可能为 nullptr,delete nullptr 是安全的
        throw;
    }

    // 正常流程释放
    cout << "delete []" << array1 << endl;
    delete[] array1;
    cout << "delete []" << array2 << endl;
    delete[] array2;
}

✅ 优点:能处理第二个 new 失败的情况 ❌ 缺点:代码依然繁琐,容易漏写 delete

解决方案 2:使用智能指针(推荐,RAII 自动管理)

cpp 复制代码
#include <memory> // 必须包含

void Func()
{
    // 用 unique_ptr 管理数组,构造即接管内存,析构自动释放
    unique_ptr<int[]> array1(new int[10]);
    unique_ptr<int[]> array2;

    try
    {
        array2.reset(new int[10]); // 若失败,array1 会自动析构释放

        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 无需手动 delete,array1 和 array2 会自动释放
        throw;
    }
    // 正常流程:无需任何释放代码
}

✅ 优点:无论 new 成功/失败、正常/异常流程,unique_ptr 出作用域时都会自动释放内存;完全避免内存泄漏,代码更简洁;delete[] nullptr 的问题也被智能指针内部处理了

2. RAII 和智能指针的设计思路

RAII 思想

RAII(Resource Acquisition Is Initialization):资源获取即初始化,利用对象生命周期管理动态资源(内存、文件、锁等)。

资源在对象构造时获取,对象生命周期内有效;资源在对象析构时自动释放,保证异常安全;避免手动 delete,从根源解决内存泄漏

智能指针的核心设计

智能指针是封装了原始指针的类,满足:

  1. RAII 机制:析构函数自动释放资源

  2. 指针行为模拟:重载 operator*/operator->/operator[] 等运算符,像原生指针一样访问资源

cpp 复制代码
// 模板类:简易智能指针,基于RAII思想实现
template<class T>
class SmartPtr
{
public:
    // 构造函数:RAII------资源获取即初始化
    // 接收一个动态分配的指针,接管该内存的管理权
    SmartPtr(T* ptr)
        :_ptr(ptr)  // 用传入的指针初始化成员指针
    {}

    // 析构函数:对象生命周期结束时自动释放资源
    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;  
        delete[] _ptr;                         
    }

    // 重载解引用运算符 *,模拟原生指针行为
    T& operator*()
    {
        return *_ptr;
    }

    // 重载箭头运算符 ->,用于访问对象成员
    T* operator->()
    {
        return _ptr;
    }

    // 重载下标运算符 [],支持数组形式访问
    T& operator[](size_t i)
    {
        return _ptr[i];
    }

private:
    T* _ptr;  // 封装的原生指针,管理动态内存
};

void Func()
{
    // 构造时接管new[]的内存,无需手动释放
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];

    for (size_t i = 0; i < 10; i++)
    {
        sp1[i] = sp2[i] = i;
    }

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

    // 函数结束,sp1、sp2局部对象析构
    // 自动调用析构函数释放内存,无需手动delete[]
}

第二个 new 失败时发生了什么?

sp1 已构造完成,是合法栈对象; sp2 构造到一半抛异常,构造失败; C++ 保证:已成功构造的局部对象会被正常析构; sp1 析构 → 执行 ~SmartPtr() → delete[] _ptr; 内存安全释放,无泄漏

3. C++ 标准库智能指针的使用

头文件与分类

所有智能指针定义在 <memory> 头文件中,核心类型:

|------------|-------|----------------------|----------------------|
| 智能指针 | 版本 | 核心特点 | 适用场景 |
| auto_ptr | C++98 | 拷贝时转移资源管理权,被拷贝对象悬空 | 已废弃,C++11 后禁止使用 |
| unique_ptr | C++11 | 独占所有权,不支持拷贝,仅支持移动 | 不需要共享、不需要拷贝的场景 |
| shared_ptr | C++11 | 共享所有权,引用计数实现,支持拷贝/移动 | 需要多个指针共享同一份资源 |
| weak_ptr | C++11 | 弱引用,不增加引用计数,不管理资源 | 解决 shared_ptr 循环引用问题 |

基础示例代码

cpp 复制代码
struct Date
{
    int _year;
    int _month;
    int _day;

    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}

    ~Date()
    {
        cout << "~Date()" << endl;
    }
};

int main()
{
    // auto_ptr:拷贝后原对象悬空(危险,已弃用)
    auto_ptr<Date> ap1(new Date);
    auto_ptr<Date> ap2(ap1); // ap1 变为 nullptr
    // ap1->_year++; // 空指针访问,未定义行为

    // unique_ptr:独占所有权,不支持拷贝,支持移动
    unique_ptr<Date> up1(new Date);
    // unique_ptr<Date> up2(up1); // 编译报错:拷贝构造被删除
    unique_ptr<Date> up3(move(up1)); // 移动后 up1 悬空

    // shared_ptr:共享所有权,引用计数
    shared_ptr<Date> sp1(new Date);
    shared_ptr<Date> sp2(sp1); // 引用计数+1
    shared_ptr<Date> sp3(sp2); // 引用计数+1
    cout << sp1.use_count() << endl; // 输出 3

    sp1->_year++;
    cout << sp1->_year << endl; // 所有指针指向同一块内存
    cout << sp2->_year << endl;
    cout << sp3->_year << endl;

    shared_ptr<Date> sp4(move(sp1)); // 移动后 sp1 悬空,引用计数不变

    return 0;
}

核心知识点

  1. auto_ptr(已废弃)

机制:拷贝时转移资源所有权,原指针置空

缺陷:易造成悬空指针、容器中使用会导致程序崩溃

结论:C++11 后彻底弃用,一律用 unique_ptr 替代

  1. unique_ptr(独占型)

核心特性:同一时间只能有一个指针拥有资源

语法限制:禁用拷贝构造、拷贝赋值,仅支持移动语义

适用场景:不需要共享、追求极致性能的场景

内存管理:析构时自动释放资源,异常安全

  1. shared_ptr(共享型)

核心机制:引用计数,多个指针共享同一份资源

计数规则:拷贝构造 / 赋值 → 计数 +1 析构 / 重置 → 计数 -1 计数为 0 → 释放资源

常用接口:use_count() 获取引用计数

注意:移动语义不会改变引用计数,仅转移所有权

  1. 通用指针行为

智能指针均重载 * / ->,使用方式与原生指针一致

栈上的智能指针对象,出作用域自动析构,无需手动释放

配合自定义类的析构函数,可直观观察资源生命周期

数组资源与删除器

智能指针默认用 delete 释放资源,若管理 new[] 分配的数组,需指定删除器:

方案1:特化版本(推荐)

cpp 复制代码
#include <iostream>
#include <memory>  // 智能指针头文件
using namespace std;

struct Date
{
    int _year;
    int _month;
    int _day;

    Date(int year = 1, int month = 1, int day = 1)
        :_year(year), _month(month), _day(day)
    {}

    ~Date()
    {
        cout << "~Date()" << endl;
    }
};

int main()
{
    // ==================== unique_ptr 数组特化 ====================
    // 1. 数组特化版本:<Date[]> 表示管理数组,析构自动用 delete[]
    unique_ptr<Date[]> up1(new Date[5]);

    // 数组特化后,支持 operator[] 下标访问,不支持 * / ->
    for (int i = 0; i < 5; ++i) {
        up1[i] = Date(2025, 3, 19);  // 赋值
    }

    // ==================== shared_ptr 数组特化 ====================
    // 2. C++17 起支持数组特化 <Date[]>,析构自动用 delete[]
    shared_ptr<Date[]> sp1(new Date[5]);

    // 同样支持 operator[] 下标访问
    sp1[0] = Date(2024, 12, 31);

    return 0;
}

1、unique_ptr<Date[]> 详解

  1. 核心机制

模板参数写 Date[],代表管理数组,是标准库提供的偏特化版本。

析构函数默认调用 delete[],完美匹配 new[]。

  1. 接口限制(特化后)

✅ 支持:operator[](数组下标访问)

❌ 不支持:operator*、operator->(因为是数组,不是单个对象)

  1. 为什么要用它?

如果写成 unique_ptr<Date> 管理数组:unique_ptr<Date> up(new Date[5]); // 错误!

析构会用 delete 而非 delete[] → 未定义行为(内存错乱/泄漏)。

2、shared_ptr<Date[]> 详解

  1. 版本要求

C++17 及以上 才支持 shared_ptr<Date[]> 数组特化。 C++11/14 只能用自定义删除器实现。

  1. 核心特性

引用计数机制不变,共享数组资源。析构自动调用 delete[]。支持 operator[] 下标访问。

3、C++11/14 下 shared_ptr 管理数组(兼容方案)

C++11/14 没有 shared_ptr<Date[]> 特化,必须指定删除器:

cpp 复制代码
// 方式1:lambda 删除器
shared_ptr<Date> sp1(new Date[5], [](Date* p){ delete[] p; });

// 方式2:函数对象删除器
shared_ptr<Date> sp2(new Date[5], default_delete<Date[]>());

注意:C++11/14 的 shared_ptr<Date> 不支持 [],只能用 get()[i] 访问:

cpp 复制代码
sp1.get()[0] = Date(2025, 3, 19);

4、关键知识点总结

  1. 匹配原则

new 单个对象 → unique_ptr<Date> / shared_ptr<Date> → delete

new[] 数组 → unique_ptr<Date[]> / shared_ptr<Date[]> → delete[]

  1. unique_ptr<Date[]>

C++11 就支持数组特化。 天然支持 [],禁止 */->,安全简洁。

  1. shared_ptr<Date[]>

C++17 正式支持。 支持 [],引用计数管理数组,自动 delete[]。

  1. 常见坑

不用数组特化版本管理数组 → 错用 delete → 未定义行为。

C++11/14 用 shared_ptr 管理数组必须手写删除器。

方案2:自定义删除器

cpp 复制代码
#include <iostream>
#include <memory>   // 智能指针
#include <cstdio>   // fopen, fclose, FILE
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) {}
    ~Date() { cout << "~Date()" << endl; }
};

// ==================== 1. 仿函数删除器 ====================
// 重载 operator(),作为删除器:调用 delete[]
template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

// ==================== 2. 函数指针删除器 ====================
// 普通模板函数,用于释放数组
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}

int main()
{
    // -------------------- 仿函数删除器 --------------------
    // unique_ptr:第二个模板参数是删除器类型
    unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());
    // shared_ptr:构造时传入删除器对象,自动推导类型
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

    // -------------------- 函数指针删除器 --------------------
    // unique_ptr:删除器类型为 函数指针 void(*)(Date*)
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

    // -------------------- lambda 删除器 --------------------
    auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
    // unique_ptr:需要用 decltype 获取 lambda 类型
    unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
    shared_ptr<Date> sp4(new Date[5], delArrOBJ);

    // ==================== 扩展:管理文件资源 ====================
    // 自定义仿函数,用于关闭文件
    class Fclose
    {
    public:
        void operator()(FILE* ptr)
        {
            cout << "fclose:" << ptr << endl;
            fclose(ptr);
        }
    };

    // 仿函数方式管理 FILE
    shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
    // lambda 方式管理 FILE
    shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    });

    return 0;
}

1、为什么需要自定义删除器?

智能指针默认用 delete 释放资源

管理 new[] 数组、文件、套接字、锁等资源时,需要自定义释放逻辑

本质:让智能指针支持非内存资源的 RAII 管理

2、三种删除器对比

  1. 仿函数删除器(推荐)

优点:无额外开销、类型安全、可复用 unique_ptr 需指定删除器类型:DeleteArray<Date>

适合工程化、频繁复用的场景

  1. 函数指针删除器

优点:简单直观 缺点:有函数指针开销,unique_ptr 写法繁琐 适合简单场景

  1. Lambda 删除器(最常用)

优点:就地编写、简洁灵活 unique_ptr 需用 decltype( lambda名 ) 作为模板参数

现代 C++ 首选方案

3、unique_ptr & shared_ptr 删除器区别

unique_ptr:删除器是类型的一部分,必须写在模板参数里;无运行时开销,效率更高;格式:unique_ptr<T, 删除器类型> ptr(资源, 删除器对象)

shared_ptr:删除器是构造参数,不影响类型;内部存储删除器,有轻微开销;格式:shared_ptr<T> ptr(资源, 删除器)

4、核心应用场景

1) 管理动态数组:new[] 必须搭配 delete[] 2) 管理文件:fopen → fclose

3) 管理网络套接字:socket → close 4) 管理互斥锁:lock → unlock

5) 第三方库资源:如 malloc 分配内存 → free

5、关键结论

数组优先用:unique_ptr<Date[]>、shared_ptr<Date[]>(C++17+)

兼容旧标准 / 自定义资源:使用Lambda 删除器最简洁

自定义删除器是 RAII 思想的延伸,让智能指针不只管理内存,可管理所有资源

其他实用特性

make_shared:更安全高效地创建 shared_ptr,一次分配内存(对象+引用计数)

cpp 复制代码
// 直接构造并返回 shared_ptr<Date>,参数会完美转发给 Date 构造函数
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
// auto 自动推导类型,写法更简洁,效果和上面完全一致
auto sp3 = make_shared<Date>(2024, 9, 11);

std::make_shared 是 C++11 提供的标准工厂函数,用于安全、高效地创建 shared_ptr。

底层优势

  1. 内存分配更高效:直接一次分配:对象内存 + 引用计数内存(连续块);对比 shared_ptr<Date>(new Date):要分两次分配(对象 + 计数)

  2. 异常更安全:杜绝new成功但计数初始化失败导致的内存泄漏;资源创建与智能指针封装是原子操作

  3. 代码更简洁:不用手写 new,避免裸指针暴露;配合 auto,代码极度精简

空指针判断:shared_ptr/unique_ptr 支持 operator bool,可直接判断是否为空

cpp 复制代码
// 智能指针重载了 operator bool,可以直接放在 if 里做判空
if (sp1) { 
    cout << "sp1 is not nullptr" << endl; 
}

// 取反判断:指针为空时成立
if (!sp4) { 
    cout << "sp4 is nullptr" << endl; 
}
  1. 隐式类型转换(operator bool):unique_ptr / shared_ptr 都重载了布尔转换运算符

作用:直接在 if / while 等条件中,判断智能指针是否为空(是否托管有效资源)

  1. 判断规则

if (sp):托管了有效指针 → 为 true;为空 → 为 false if (!sp):为空 → 为 true;不为空 → 为 false

  1. 等价写法(更显式)
cpp 复制代码
if (sp1.get() != nullptr)  // 效果一样
if (sp4.get() == nullptr)
  1. 适用范围:所有标准智能指针:unique_ptr/shared_ptr/weak_ptr(weak_ptr用lock()后再判空)

  2. 使用场景:在访问指针成员、解引用前,做空指针安全检查,避免未定义行为。

explicit 构造:防止普通指针隐式转换为智能指针,避免意外接管资源

cpp 复制代码
template<class T>
class shared_ptr {
    // 加 explicit:禁止隐式转换
    explicit shared_ptr(T* ptr = nullptr) 
        : _ptr(ptr), _pcount(new int(1)) 
    {}
    ...
};

explicit 只能修饰单参数构造函数,作用:禁止 普通指针 → 智能指针的隐式类型转换;只能显式构造智能指针,避免意外接管内存、造成重复释放

不加 explicit 会出什么问题(危险场景)

cpp 复制代码
void func(shared_ptr<Date> sp) {
    // ...
}

int main() {
    Date* p = new Date;

    // 危险:隐式把 p 转成 shared_ptr
    func(p); 

    // 函数结束,sp 析构 → delete p
    // 但外面还持有 p,变成野指针!
    p->_year = 2025;  // 未定义行为
}

调用 func(p) 时,编译器隐式构造临时 shared_ptr 接管 p;函数结束临时对象析构 → delete p;外层 p 变成野指针,继续访问直接崩溃

加 explicit 后(安全)

cpp 复制代码
explicit shared_ptr(T* ptr = nullptr);
// 再写
func(p);  // 编译报错!不允许隐式转换
// 必须显示构造
func(shared_ptr<Date>(p));  // 合法,显式构造
// p 已经交给 shared_ptr 托管,后面不能再手动管理。

正确写法(模拟标准库)

cpp 复制代码
template<class T>
class SmartPtr {
public:
    // 单参数构造 + explicit
    explicit SmartPtr(T* ptr = nullptr) 
        : _ptr(ptr) 
    {}

    ~SmartPtr() {
        delete _ptr;
    }

private:
    T* _ptr;
};

// 错误(有 explicit 时):
// SmartPtr<int> p = new int[10];  // 拷贝初始化 = 隐式转换,报错

// 正确(显式构造):
SmartPtr<int> p(new int[10]);     // 直接初始化,允许

总结:explicit 构造 = 禁止普通指针隐式变智能指针;目的:防止意外接管资源、重复释放、野指针

标准库 shared_ptr / unique_ptr 构造函数全都带 explicit,是工程必备规范。

4. 智能指针的原理

auto_ptr 模拟实现(了解,已弃用)

核心:拷贝/赋值时转移资源管理权,被拷贝对象变为 nullptr

cpp 复制代码
// 自定义命名空间,避免与 std::auto_ptr 冲突
namespace gxy
{
    // 模拟 C++98 废弃的 auto_ptr 智能指针
    template<class T>
    class auto_ptr
    {
    public:
        // 1. 构造函数:接管裸指针资源
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 2. 拷贝构造函数:核心设计------管理权转移
        auto_ptr(auto_ptr<T>& sp)
            :_ptr(sp._ptr)   // 接管原指针指向的资源
        {
            sp._ptr = nullptr; // 原指针置空,实现资源独占
        }

        // 3. 赋值运算符重载:同样是管理权转移
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)    // 防止自赋值
            {
                if (_ptr) 
                    delete _ptr; // 先释放当前对象管理的资源
                
                _ptr = ap._ptr;  // 接管新资源
                ap._ptr = nullptr; // 原对象置空
            }
            return *this;
        }

        // 4. 析构函数:自动释放资源
        ~auto_ptr()
        {
            if (_ptr)   // 指针非空时才释放
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        // 5. 重载指针运算符,模拟原生指针行为
        T& operator*()  { return *_ptr; }  // 解引用
        T* operator->() { return _ptr; }   // 成员访问

    private:
        T* _ptr;  // 托管的原生指针
    };
}

核心知识点总结

  1. 核心设计:管理权转移

拷贝/赋值时把资源所有权转移给新对象,原对象强制置空;同一时刻,只有一个auto_ptr持有资源

  1. 致命缺陷(为何被 C++11 废弃):拷贝后原对象悬空,极易踩坑
cpp 复制代码
gxy::auto_ptr<int> ap1(new int(10));
gxy::auto_ptr<int> ap2 = ap1;
// ap1 已为 nullptr,继续使用会崩溃
  1. 与 unique_ptr 本质区别

auto_ptr:靠运行时管理权转移实现独占,语法不安全;unique_ptr:编译期禁用拷贝,只允许移动,安全可靠。

unique_ptr 模拟实现

核心:独占所有权,禁止拷贝,仅允许移动

cpp 复制代码
// 自定义命名空间,避免与标准库冲突
namespace gxy
{
    // 模拟 C++11 独占式智能指针 unique_ptr
    template<class T>
    class unique_ptr
    {
    public:
        // 1. 显式构造函数,接管裸指针
        // explicit 禁止隐式转换:如 unique_ptr<int> p = new int; 编译报错
        explicit unique_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 2. 析构函数:自动释放资源
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        // 3. 核心:禁止拷贝构造、拷贝赋值
        // 从语法层面杜绝拷贝,保证独占性,编译期就报错
        unique_ptr(const unique_ptr<T>& sp) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

        // 4. 支持移动语义(转移所有权)
        // 右值引用:只能接收临时对象或 std::move 后的对象
        unique_ptr(unique_ptr<T>&& sp)
            :_ptr(sp._ptr)
        {
            sp._ptr = nullptr;   // 原指针置空,避免重复释放
        }

        // 移动赋值重载
        unique_ptr<T>& operator=(unique_ptr<T>&& sp)
        {
            if (this != &sp)    // 防止自移动
            {
                delete _ptr;    // 先释放当前资源
                _ptr = sp._ptr; // 接管新资源
                sp._ptr = nullptr; // 原对象置空
            }
            return *this;
        }

        // 5. 重载运算符,模拟原生指针行为
        T& operator*()  { return *_ptr; }  // 解引用
        T* operator->() { return _ptr; }   // 访问成员

    private:
        T* _ptr;  // 托管的原生指针
    };
}

核心知识点

  1. 核心设计:编译期独占

同一时刻只能有一个 unique_ptr 管理资源

靠 =delete 直接禁用拷贝,不是运行时置空,而是编译报错,比 auto_ptr 安全得多

  1. 关键语法:= delete

显式声明函数为删除函数; 任何拷贝行为直接编译失败,从根源避免悬空指针、重复释放

  1. 移动语义(&&)

允许转移所有权,但必须显式使用 std::move;转移后原对象置空,不影响程序安全;兼顾独占性与灵活性(可作为函数返回值、容器转移)

  1. explicit 构造

禁止 unique_ptr<int> p = new int; 这种隐式构造;必须显式写:unique_ptr<int> p(new int);

防止意外接管裸指针,提高安全性

  1. 与 auto_ptr 的本质区别

auto_ptr:拷贝时运行时置空,隐患极大 unique_ptr:拷贝直接编译报错,安全可控

都属于独占指针,但 unique_ptr 是现代 C++ 标准方案

  1. 适用场景:不需要共享、不需要拷贝的资源管理;性能敏感场景(无引用计数开销,效率接近裸指针);作为类成员指针,防止异常泄漏
cpp 复制代码
int main()
{
    gxy::unique_ptr<int> up1(new int(10));
    // gxy::unique_ptr<int> up2 = up1;  // 报错:拷贝被删除
    gxy::unique_ptr<int> up2 = std::move(up1); // 允许:移动转移

    *up2 = 20;
    return 0;
}

shared_ptr 模拟实现(核心)

核心:引用计数,多个指针共享资源,计数为 0 时释放资源

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

// 测试用的日期类
struct Date
{
    int _year;
    int _month;
    int _day;

    // 构造函数
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {}

    // 析构函数:用于观察对象是否被正确释放
    ~Date()
    {
        cout << "~Date()" << endl;
    }
};

// 自定义删除器:关闭文件(仿函数)
class Fclose
{
public:
    // 重载(),让类对象可以像函数一样调用
    void operator()(FILE* ptr)
    {
        cout << "fclose: " << ptr << endl;
        fclose(ptr); // 关闭文件,避免文件句柄泄漏
    }
};

// 自定义删除器:释放数组(函数模板)
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr; // 数组必须用 delete[],匹配 new[]
}

namespace gxy
{
    // 模拟实现 shared_ptr 共享型智能指针
    template<class T>
    class shared_ptr
    {
    public:
        // 1. 普通构造函数
        // 管理原生指针,引用计数初始化为 1
        shared_ptr(T* ptr)
            : _ptr(ptr)
            , _pcount(new atomic<int>(1))
        {}

        // 2. 带自定义删除器的构造函数
        // 可传入 lambda/函数/仿函数,自定义资源释放方式
        template<class D>
        shared_ptr(T* ptr, D del)
            : _ptr(ptr)
            , _pcount(new atomic<int>(1))
            , _del(del)
        {}

        // 3. 析构函数
        // 引用计数 --,减为 0 时真正释放资源
        ~shared_ptr()
        {
            if (--(*_pcount) == 0)
            {
                _del(_ptr);  // 调用删除器释放管理的资源
                delete _pcount; // 释放引用计数的内存
            }
        }

        // 4. 拷贝构造函数
        // 共享同一块资源,引用计数 ++
        shared_ptr(const shared_ptr<T>& sp)
            : _ptr(sp._ptr)
            , _pcount(sp._pcount)
        {
            (*_pcount)++;
        }

        // 5. 赋值运算符重载
        // 先释放旧资源,再共享新资源,维护引用计数
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            // 避免管理同一块资源时重复操作
            if (_ptr != sp._ptr)
            {
                // 先处理当前管理的旧资源
                if (--(*_pcount) == 0)
                {
                    _del(_ptr);  // 用删除器释放旧对象
                    delete _pcount; // 释放旧计数
                }

                // 共享新对象的指针和引用计数
                _pcount = sp._pcount;
                _ptr = sp._ptr;
                // 新资源计数 ++
                ++(*_pcount);
            }

            return *this;
        }

        // 6. 解引用重载,模拟指针解引用行为
        T& operator*()
        {
            return *_ptr;
        }

        // 7. -> 重载,访问指针指向的成员
        T* operator->()
        {
            return _ptr;
        }

        // 8. 获取当前引用计数
        int use_count()
        {
            return *_pcount;
        }

    private:
        T* _ptr;               // 指向管理的资源
        atomic<int>* _pcount;  // 原子引用计数,保证多线程安全

        // 删除器:默认使用 delete 释放单个对象,支持自定义
        function<void(T*)> _del = [](T* ptr) {delete ptr; };
    };
};


// 测试代码
int main()
{
    // 测试1:管理单个对象
    gxy::shared_ptr<Date> sp1(new Date);

    // 测试2:管理数组对象,使用 DeleteArrayFunc 删除器
    gxy::shared_ptr<Date> sp2(new Date[5], DeleteArrayFunc<Date>);

    // 管理数组,传自定义删除器(lambda),用 delete[]
    gxy::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });

    // 测试3:管理文件指针,使用 Fclose 删除器
    FILE* f = fopen("test.txt", "w");
    gxy::shared_ptr<FILE> sp4(f, Fclose());

    return 0;
}

1、核心原理

共享式智能指针,多个指针管理同一块资源。 通过引用计数实现资源自动释放。计数为 0 时,才真正释放资源。 引用计数必须在堆上开辟,保证共享。使用 atomic 原子类型,保证多线程安全。

2、自定义删除器(三种写法)

1)lambda:[](T* p){ delete[] p; }

2)函数指针:DeleteArrayFunc<T>

3)仿函数:class Fclose { void operator()(FILE* p) { fclose(p); } };

3、重点结论

new ↔ delete new[] ↔ delete[](必须传删除器) FILE* ↔ fclose(必须传删除器)

默认删除器不能用于数组。 引用计数是共享、堆上、原子的。

4、atomic

atomic 就是 原子类型,专门用来解决 多线程同时修改同一个变量时的数据错乱问题。

普通的 int count 在多线程里 ++、-- 不是一步完成的,会被打断,导致计数算错。

atomic<int> 能让 ++ / -- / 读写 变成一个不可分割的原子操作,不会算错。

为什么要用 atomic?(重点):引用计数 _pcount 会被 多个线程同时共享、同时++/--。
++(*_pcount);

如果是普通 int*:线程A读到 2,线程B也读到 2,两个线程都写成 3 → 计数少加1,彻底错乱

如果是 atomic<int>*:同一时间只有一个线程能修改要么全做完,要么完全不做 → 计数绝对正确
atomic<int>* _pcount; 保证 引用计数的 ++ 和 -- 线程安全;保证 多线程拷贝、赋值、析构时计数不会算错;这是 shared_ptr 线程安全的基础

普通 int:多线程不安全,计数会算错;atomic:多线程安全,计数一定正确

标准库 shared_ptr / unique_ptr 的用法

cpp 复制代码
int main()
{
    // 单个对象
    std::shared_ptr<Date> sp1(new Date);
    
    // 支持数组特化版本,自动用 delete[]
    std::shared_ptr<Date[]> sp2(new Date[10]);

    // lambda 删除器(最推荐)
    std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });
    
    // 函数指针删除器
    std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);

    // 管理文件,仿函数删除器
    std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
    
    // 管理文件,lambda 删除器
    shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
        fclose(ptr);
    });

    // unique_ptr 单个对象
    std::unique_ptr<Date> up1(new Date);
    
    // unique_ptr 数组特化,默认 delete[]
    std::unique_ptr<Date[]> up2(new Date[10]);
    
    // unique_ptr + 仿函数删除器(类型要传)
    std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));

    // unique_ptr + lambda 删除器(需要传decltype)
    auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
    std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);

    return 0;
}
  1. shared_ptr 数组:shared_ptr<Date[]> 自动用 delete[],或自己传删除器

  2. unique_ptr 数组:unique_ptr<Date[]> 默认就是 delete[]

  3. 删除器写法:shared_ptr:lambda 最方便,unique_ptr:仿函数推荐,lambda 麻烦

隐式类型转换、空判断、make_shared

cpp 复制代码
int main()
{
    // 裸指针构造
    std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
    
    // make_shared 写法(更安全、更高效)
    shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
    
    shared_ptr<Date> sp4;

    // 智能指针可以直接当 bool 用(重载了 operator bool)
    if (sp1)
        cout << "sp1 is not nullptr" << endl;

    // 判断空指针
    if (!sp4)
        cout << "sp4 is nullptr" << endl;

    // 错误!explicit 构造,不允许隐式类型转换
    // shared_ptr<Date> sp5 = new Date(2024, 9, 11);
    // unique_ptr<Date> sp6 = new Date(2024, 9, 11);

    return 0;
}
  1. make_shared:比 new 构造更高效、异常安全

  2. operator bool():if(sp1) 判断是否为空指针

  3. explicit 构造:不允许 shared_ptr<Date> p = new Date;只能 shared_ptr<Date> p(new Date);

weak_ptr 模拟实现

核心:弱引用,不增加引用计数,不管理资源生命周期

cpp 复制代码
namespace gxy
{
    // 弱智能指针:只观察,不管理,不影响引用计数
    template<class T>
    class weak_ptr
    {
    public:
        // 默认构造:空指针
        weak_ptr() = default;

        // 用 shared_ptr 构造 weak_ptr
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get())  // 只拿原生指针,不增加计数
        {}

        // 用 shared_ptr 赋值 weak_ptr
        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get();  // 同样只赋值指针,不增减计数
            return *this;
        }

    private:
        T* _ptr = nullptr;  // 仅仅保存观察用的原生指针
    };
}
  1. weak_ptr 是什么?观察者指针;只观察 shared_ptr 管理的对象;不参与管理权,不增加引用计数

不会调用析构,不会释放资源

  1. 它做了什么?只存一个原生指针 _ptr,从 shared_ptr 拿地址,完全不碰引用计数

  2. 重点特性:拷贝、赋值、析构 都不影响引用计数;不能直接 *、-> 访问对象(你这个简易版没做限制);真正的 weak_ptr 必须通过 lock() 转为 shared_ptr 才能使用

  3. 为什么需要 weak_ptr?解决 shared_ptr 循环引用 导致的内存泄漏。

比如:A 里面有 shared_ptr,B 里面有 shared_ptr,互相持有,计数永远不为 0 → 永远不释放。

用 weak_ptr 打破循环就行。

weak_ptr = 只看不管的智能指针;不增加计数,不释放资源,专门解决循环引用。

5. shared_ptr 和 weak_ptr

5.1 shared_ptr 循环引用问题

循环引用:两个 shared_ptr 互相指向对方,导致引用计数永远无法为 0,资源泄漏。

cpp 复制代码
struct ListNode
{
    int _data;
    shared_ptr<ListNode> _next;
    shared_ptr<ListNode> _prev;

    ~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
    std::shared_ptr<ListNode> n1(new ListNode);
    std::shared_ptr<ListNode> n2(new ListNode);

    n1->_next = n2; // n1 引用 n2
    n2->_prev = n1; // n2 引用 n1

    cout << n1.use_count() << endl; // 2
    cout << n2.use_count() << endl; // 2

    // n1、n2 析构后,引用计数减为 1,资源永远无法释放
    return 0;
}

构造两个节点:n1->_next = n2; n2->_prev = n1; 形成:n1 ↔ n2,互相持有对方的 shared_ptr。

引用计数情况:n1 的 use_count:2,n2 的 use_count:2

为什么?n1 自己 + n2->_prev,n2 自己 + n1->_next

cpp 复制代码
n1 的计数:2
   ↳ 外部 n1 智能指针
   ↳ n2->_prev 持有

n2 的计数:2
   ↳ 外部 n2 智能指针
   ↳ n1->_next 持有

出作用域会发生什么?1) n1 销毁 → 计数 2 → 1 2) n2 销毁 → 计数 2 → 1

结果:两个对象的引用计数都变成 1,永远不会降到 0→ 永远不调用析构函数 → 内存泄漏!

为什么会这样?

要释放 n1:必须先释放 n2->_prev,要释放 n2:必须先释放 n1->_next,互相等待,死锁释放

解决方案:将 _next/_prev 改为 weak_ptr,弱引用不增加引用计数

cpp 复制代码
struct ListNode
{
    int _data;
    weak_ptr<ListNode> _next; // 弱引用
    weak_ptr<ListNode> _prev; // 弱引用

    ~ListNode() { cout << "~ListNode()" << endl; }
};

weak_ptr 不增加引用计数,n1 的 use_count 最终能减到 0,n2 的 use_count 最终也能减到 0

→ 正常释放,没有泄漏

5.2 weak_ptr 核心特性

不支持 RAII:不管理资源生命周期,不释放资源

不增加引用计数:绑定 shared_ptr 时不影响引用计数

不重载指针运算符:operator*/operator-> 未重载,无法直接访问资源

安全访问:需调用 lock() 获取 shared_ptr,若资源已释放则返回空 shared_ptr

状态检查:expired() 检查资源是否过期,use_count() 获取当前引用计数

cpp 复制代码
int main()
{
    // sp1 管理 "111111",引用计数 = 1
    std::shared_ptr<string> sp1(new string("111111"));

    // sp2 拷贝 sp1,引用计数 = 2
    std::shared_ptr<string> sp2(sp1);

    // 用 shared_ptr 构造 weak_ptr
    // wp 只观察,不增加引用计数
    std::weak_ptr<string> wp = sp1;

    // expired():检查资源是否已释放
    // 资源还在 → 返回 false(0)
    cout << wp.expired() << endl;

    // use_count():查看对应 shared_ptr 的引用计数 → 2
    cout << wp.use_count() << endl;

    // sp1 指向新资源 "222222"
    // 旧资源 "111111" 计数 2 → 1
    sp1 = make_shared<string>("222222");

    // 资源还在 → false(0)
    cout << wp.expired() << endl;

    // 计数变为 1
    cout << wp.use_count() << endl;

    // sp2 也指向新资源 "333333"
    // 旧资源 "111111" 计数 1 → 0,被释放
    sp2 = make_shared<string>("333333");

    // 资源已销毁 → true(1)
    cout << wp.expired() << endl;

    // 计数变为 0
    cout << wp.use_count() << endl;

    // wp 现在观察 sp1("222222")
    wp = sp1;

    // lock():
    // 如果资源还在 → 返回一个有效的 shared_ptr,计数+1
    // 如果资源没了 → 返回空的 shared_ptr
    auto sp3 = wp.lock();

    // 资源还在 → false(0)
    cout << wp.expired() << endl;

    // sp1+sp3 共同管理 → 计数 2
    cout << wp.use_count() << endl;

    // sp1 指向新资源 "4444444"
    // 旧资源 "222222" 计数 2 → 1(还被 sp3 指着)
    sp1 = make_shared<string>("4444444");

    // 资源还在(sp3 还拿着)→ false(0)
    cout << wp.expired() << endl;

    // 计数 1
    cout << wp.use_count() << endl;

    return 0;
}

核心接口知识点

  1. wp.expired():判断观察的对象是否已经被释放,资源还在 → false,资源没了 → true

  2. wp.use_count():返回对应 shared_ptr 的引用计数;不增加计数,只是查看

  3. wp.lock():返回一个 shared_ptr;如果资源还活着:得到有效指针,计数+1;如果资源已销毁:得到空指针;作用:安全地访问对象,避免悬空指针

6. shared_ptr 的线程安全问题

线程安全范围

  1. 引用计数:多线程拷贝/析构 shared_ptr 时,引用计数修改需线程安全(标准库实现用原子操作或锁)

  2. 资源本身:shared_ptr 不保证指向资源的线程安全,需外层控制(如互斥锁)

线程安全示例

cpp 复制代码
struct AA
{
    int _a1 = 0;
    int _a2 = 0;
    ~AA() { cout << "~AA()" << endl; }
};

int main()
{
    gxy::shared_ptr<AA> p(new AA);
    const size_t n = 100000;
    mutex mtx;

    auto func = [&]()
    {
        // 智能指针拷贝 ++计数
        for (size_t i = 0; i < n; ++i)
        {
            gxy::shared_ptr<AA> copy(p); // 拷贝时引用计数+1(线程安全)
            unique_lock<mutex> lk(mtx);
            copy->_a1++;
            copy->_a2++;
        }
    };

    thread t1(func);
    thread t2(func);

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

    cout << p->_a1 << endl; // 200000
    cout << p->_a2 << endl; // 200000
    cout << p.use_count() << endl; // 1

    return 0;
}
  1. shared_ptr 的线程安全:引用计数的 ++ / -- 是线程安全的(因为用了 atomic),多个线程同时拷贝 shared_ptr 不会错乱

  2. 但指向的资源不是线程安全的:_a1++、_a2++ 不是原子操作,必须加 mutex / lock 保护,否则结果会少算

  3. 为什么要写 gxy::shared_ptr<AA> copy(p);:每循环一次都拷贝一次智能指针,保证在线程工作期间,对象不会被释放,更安全、更规范

7. C++11 和 Boost 中智能指针的关系

Boost 库:C++ 标准库的先驱,提供了 scoped_ptr/shared_ptr/weak_ptr 等实现

版本演进:

C++98:auto_ptr(设计缺陷)

Boost:scoped_ptr/scoped_array/shared_ptr/shared_array/weak_ptr

C++ TR1:引入 shared_ptr(非标准)

C++11:引入 unique_ptr/shared_ptr/weak_ptr,其中 unique_ptr 对应 Boost scoped_ptr,实现参考 Boost

8. 内存泄漏

8.1 什么是内存泄漏

定义:程序未释放已不再使用的内存,失去对内存的控制,造成资源浪费。

原因:忘记 delete、异常导致 delete 未执行、设计错误等

危害:

短期程序:影响小,进程结束后内存回收

长期程序(操作系统、后台服务):内存不断减少,响应变慢,最终卡死

代码示例:new 大块内存不释放

cpp 复制代码
int main()
{
    // 申请 1GB 空间,不释放
    char* ptr = new char[1024 * 1024 * 1024];
    cout << (void*)ptr << endl;

    return 0;
}
  1. 进程结束,操作系统会自动回收所有内存:即使你不 delete[],程序退出后系统也会回收,不会造成"系统内存泄漏"

  2. 危害:只有在长期运行不退出的程序(服务器、后台程序)才会产生内存泄漏,短期小程序跑一次就退,完全没问题

  3. 为什么可以这么写?现代操作系统:进程资源独立,进程销毁 → 全部虚拟内存回收

8.2 如何检测内存泄漏

Linux:valgrind、AddressSanitizer 等工具

Windows:VLD(Visual Leak Detector)等第三方工具

8.3 如何避免内存泄漏

  1. 事前预防:

良好编码规范:new 与 delete 匹配;优先使用智能指针(RAII)管理资源; 特殊场景自定义 RAII 类

  1. 事后查错:定期用内存泄漏检测工具排查;项目上线前重点检测

9. 总结

RAII 是智能指针的核心思想,利用对象生命周期自动管理资源

unique_ptr:独占所有权,高效无额外开销,适合不需要共享的场景

shared_ptr:共享所有权,引用计数实现,适合多对象共享资源

weak_ptr:弱引用,解决 shared_ptr 循环引用问题

智能指针从根源解决异常安全和内存泄漏问题,是现代 C++ 资源管理的首选


智能指针不止是语法糖,更是C++资源管理思想的体现。把内存交给对象生命周期去托管,代码更简洁,程序更安全,这正是现代C++的魅力所在。

相关推荐
布谷歌1 小时前
Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么?
开发语言·python
虚幻如影1 小时前
python识别验证码
开发语言·python
今儿敲了吗1 小时前
python基础学习笔记第七章——文件操作
笔记·python·学习
不染尘.1 小时前
最小生成树算法
开发语言·数据结构·c++·算法·图论
ChineHe1 小时前
基础篇003_Python基础语法
开发语言·人工智能·python
Klong.k2 小时前
判断是不是素数题目
数据结构·算法
QQsuccess2 小时前
AI全体系保姆级详讲——第一部分:了解AI基本定义
人工智能·算法
NX-二次开发2 小时前
UG CAM API 获取、设置切削层中的切削方式类型方法,如设置仅底面、恒定、临界深度的类型
c++
_日拱一卒2 小时前
LeetCode:移动零
算法·leetcode·职场和发展