智能指针的使用

github地址

有梦想的电信狗

前言

在现代 C++ 开发中,资源管理 始终是绕不开的话题。随着项目规模不断扩大、内存模型愈发复杂,传统的 new/delete 手动管理方式极易导致内存泄漏、重复释放、异常路径未释放等问题,成为 C++ 程序最常见、也最难排查的 Bug 之一。

为了解决这一痛点,C++11 引入了更安全也更现代的资源管理机制——智能指针(Smart Pointer)。它们基于 RAII(资源获取即初始化) 思想,通过对象生命周期管理动态内存,使得 C++ 能够像带垃圾回收机制的语言一样安全地使用动态资源,同时仍保留手动控制的灵活性与高性能。

本篇博客将系统性介绍 C++11 提供的三种智能指针:

  • unique_ptr —— 独占所有权,零开销的 RAII 指针
  • shared_ptr —— 共享所有权,引用计数逻辑透明可靠
  • weak_ptr —— 弱引用,用于解决循环引用问题

同时还会通过大量代码示例,深入讲解:

  • 智能指针的使用方式与底层原理
  • 动态数组、非内存资源的管理
  • 删除器(deleter)的实现与差异
  • 智能指针常见误区与最佳实践

希望通过这篇文章,你能真正掌握智能指针的使用模式,从而写出更安全、更现代、更健壮的 C++ 代码。


一、什么是智能指针

智能指针:是 C++ 标准库提供的一种封装了原始指针的类模板核心作用是自动管理动态内存,避免手动 new/delete 导致的内存泄漏(如:异常抛出时忘记释放内存)或重复释放等问题。

  • 它的本质是利用 RAII(资源获取即初始化) 机制将动态内存的生命周期与智能指针对象的生命周期绑定 —— 当智能指针对象离开作用域时,其析构函数会自动调用 delete 释放所管理的内存

二、 常用智能指针的类型有哪些?

1. std::unique_ptr

std::unique_ptr

  • 基本特性独占所有权,同一时间只能有一个 unique_ptr 管理某块内存,不允许拷贝,只能移动
  • 适用场景:管理单个对象或数组,明确内存归属唯一的场景

使用示例

#include <memory>
using namespace std;

int main()
{
    unique_ptr<int> ptr1(new int(10));	//1.管理单个int对象

    auto ptr2 = make_unique<int>(20);	//2.或用更安全的make_unique(C++14)

    // unique_ptr<int> ptr3 = ptr1; 	 //3.不允许拷贝(编译报错)

    unique_ptr<int> ptr4 = move(ptr1);	  //4.允许移动(所有权转移)
}
// 离开作用域时,ptr2、ptr4自动释放内存

2. std::shared_ptr

std::shared_ptr

  • 基本特性共享所有权,通过引用计数记录有多少个 shared_ptr 管理同一块内存,当计数为 0 时自动释放
  • 适用场景:需要多个指针共享同一资源,常用于需要拷贝指针的场景(如:容器中存储的对象被多个地方引用)

使用示例

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

int main()
{
    // 多个智能指针共享同一个引用计数
    auto ptr1 = make_shared<int>(30);
    cout << ptr1.use_count() << endl;

    shared_ptr<int> ptr2 = ptr1; // 引用计数变为2
    cout << ptr1.use_count() << endl;

    //使用大括号定义了一个 “临时代码块”  ---> 定义了一个局部作用域
    { 
        shared_ptr<int> ptr3 = ptr1;  // 引用计数变为3
        cout << ptr1.use_count() << endl;
    }

    // ptr3销毁,引用计数变回2
    cout << ptr1.use_count() << endl;

} // ptr1、ptr2销毁,引用计数变为0,内存释放

在这里插入图片描述


3. std::weak_ptr

std::weak_ptr

  • 基本特性弱引用,不增加引用计数,用于解决 shared_ptr 的循环引用问题(两个 shared_ptr 互相引用导致计数无法归零)
  • 适用场景:作为观察者关联共享资源,不参与所有权管理

使用示例

#include <memory>
using namespace std;
        
struct Node
{
    weak_ptr<Node> next; // 用weak_ptr避免循环引用
};
        
int main()
{
    auto node1 = make_shared<Node>();
    auto node2 = make_shared<Node>();
        
    node1->next = node2; // weak_ptr不增加计数
    node2->next = node1;
} // 计数正常归零,内存释放

总结:

  • 独占资源unique_ptr —> 适合 “一对一” 的场景
  • 共享资源shared_ptr —> 适合 “多对一” 的场景
  • 观察资源weak_ptr —> 解决循环引用

三、智能指针的具体使用

使用 C++ 智能指针的核心是利用其自动管理内存的特性,避免手动 new/delete 导致的问题。

以下是三种常用智能指针的具体使用方法和场景:

1. std::unique_ptr(独占所有权)

#include <iostream>
#include <memory>  // 需包含智能指针头文件
using namespace std;

int main() 
{
    /*--------------- 创建 unique_ptr(管理单个对象)---------------*/
    //方式1:直接绑定 new 分配的内存(不推荐,存在异常安全风险)
    unique_ptr<int> ptr1(new int(10));

    //方式2:用 make_unique 创建(C++14 推荐,更安全)
    auto ptr2 = make_unique<int>(20);  // 自动推导类型

    /*--------------- 访问所管理的对象(同普通指针)---------------*/
    *ptr2 = 30;             // 修改值
    cout << *ptr2 << endl;  // 输出:30

    /*--------------- 转移所有权(只能用 move,原指针会变为空)---------------*/
    unique_ptr<int> ptr3 = move(ptr2);  //注意:ptr2 不再拥有内存
    if (ptr2 == nullptr) 
    {
        cout << "ptr2 已为空" << endl;
    }

    /*--------------- 管理动态数组(需指定数组类型)---------------*/
    unique_ptr<int[]> arr_ptr = make_unique<int[]>(5);  // 5个int的数组
    arr_ptr[0] = 1;                                     // 数组访问
}
// 离开作用域时,所有 unique_ptr 自动释放内存(无需手动 delete)

在这里插入图片描述


2. std::shared_ptr(共享所有权)

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

int main()
{
    //1.创建 shared_ptr(推荐用 make_shared)
    auto ptr1 = make_shared<int>(100);  // 引用计数 = 1

    //2.共享所有权(拷贝指针时,引用计数增加)
    shared_ptr<int> ptr2 = ptr1;  // 引用计数 = 2
    shared_ptr<int> ptr3 = ptr2;  // 引用计数 = 3

    //3.访问对象(同普通指针)
    *ptr3 = 200;
    cout << *ptr1 << endl;  // 输出:200(所有指针指向同一内存)

    //4.查看引用计数(use_count() 仅用于调试)
    cout << "计数:" << ptr1.use_count() << endl;  // 输出:3

    //5.局部作用域演示计数变化
    {
        shared_ptr<int> ptr4 = ptr1;  // 计数 = 4
        cout << "局部域中的计数:" << ptr1.use_count() << endl;  // 输出:4
    }  // ptr4 销毁,计数 = 3

    //6.手动释放(重置指针,计数减少)
    ptr2.reset();  // ptr2 不再指向内存,计数 = 2
}
// ptr1、ptr3 销毁,计数 = 0 → 内存自动释放

在这里插入图片描述


3. std::weak_ptr(弱引用,解决循环引用)

#include <iostream>
#include <memory>  // 包含智能指针所需的头文件
using namespace std;

// 定义链表节点结构
// 场景:链表节点之间可能互相引用,容易引发shared_ptr的循环引用问题
struct Node
{
    int value;               // 节点存储的值
    weak_ptr<Node> next;     // 指向链表下一个节点的弱指针
    //关键:使用weak_ptr而非shared_ptr,避免循环引用
};

int main()
{
    //1.创建两个共享指针,分别管理两个Node对象
    auto node1 = make_shared<Node>();  // node1的引用计数为1
    auto node2 = make_shared<Node>();  // node2的引用计数为1
    //注意: make_shared是创建shared_ptr的推荐方式,安全且高效


    //2.构建节点间的互相引用关系
    node1->next = node2;  // weak_ptr接收shared_ptr时,不增加node2的引用计数(仍为1)
    node2->next = node1;  // 同理,node1的引用计数仍为1
    //注意:若此处用shared_ptr,会导致引用计数循环增加,无法归零

    //3.1:temp是有效的shared_ptr,说明node2仍存在
    //注意:访问weak_ptr指向的对象:必须通过lock()方法转换为shared_ptr
    if (auto temp = node1->next.lock()) 
    {
        cout << "node2 有效" << endl;
    }
    //3.2:若node2已被释放,进入此分支
    else
    {
        cout << "node2 已释放" << endl;
    }
    /* 说明:lock()的作用:检查被引用的对象是否还存在
    *     1. 若存在:返回一个指向该对象的shared_ptr(此时引用计数临时+1)
    *     2. 若已释放:返回空的shared_ptr
    */

}  // main函数结束,局部变量node1和node2开始销毁
   // 1. node2的引用计数从1减为0 → 其管理的Node对象被释放
   // 2. node1的引用计数从1减为0 → 其管理的Node对象被释放
   // 3. 由于使用weak_ptr,没有循环引用,所有内存正常释放(无内存泄漏)

在这里插入图片描述


四、为什么需要智能指针

在 C++ 中,智能指针的出现主要是为了:解决手动管理动态内存时容易出现的问题,其核心价值在于自动管理内存生命周期,避免内存相关的 bug。


具体来说,需要智能指针的原因可以从以下几个方面理解:

1. 避免内存泄漏

  • 手动管理动态内存(使用new分配、delete释放)时,若因逻辑疏漏导致delete未执行,会造成内存泄漏(已分配的内存无法回收,直到程序结束)
void func() 
{
    int* ptr1 = new int(10);
    
    int* ptr2 = ptr; // 两个指针指向同一块内存
    
    delete ptr1;
    delete ptr2; // 重复释放,行为未定义
}
  • 智能指针会在自身生命周期结束时(如:超出作用域、被销毁)自动调用delete,无论程序执行路径如何(即使有提前返回、异常抛出等),都能保证内存被释放

2. 防止重复释放

  • 手动释放内存时,若对同一块内存多次调用delete,会导致未定义行为(程序崩溃、数据损坏等)
void func() 
{
    int* ptr1 = new int(10);
    
    int* ptr2 = ptr; // 两个指针指向同一块内存
    
    delete ptr1;
    delete ptr2; // 重复释放,行为未定义
}

  • 智能指针通过引用计数等机制追踪内存的引用情况,只有当最后一个引用它的智能指针被销毁时,才会真正释放内存,避免重复释放

3. 应对异常安全

  • 当程序抛出异常时,手动管理的内存可能因delete语句被跳过而泄漏
void func() 
{
    int* ptr = new int(10);

    try 
    {
        someOperation(); // 若此函数抛出异常
    }
    catch (...) 
    {
        // 如果发生了异常:且未在catch中手动释放ptr,内存泄漏
        throw;
    }

    delete ptr;  // 如果未发生了异常:ptr指向的资源将在这里释放 
}
  • 智能指针的析构函数会在异常发生时被自动调用(C++ 的栈展开机制),确保内存释放,无需手动在异常处理中额外处理

4. 简化内存管理逻辑

  • 复杂程序中,动态内存的所有权可能在多个函数、对象之间传递,手动追踪所有权并确定delete的时机非常困难
  • 智能指针通过明确的所有权语义(如:unique_ptr的独占所有权、shared_ptr的共享所有权),简化了内存管理的逻辑,降低了人为出错的概率

5. 总结

总结

C++ 没有垃圾回收机制,动态内存的分配与释放完全依赖程序员手动控制,这使得内存管理成为 C++ 开发中的常见痛点(内存泄漏、重复释放、异常安全等

智能指针通过封装原始指针,在编译期和运行期自动处理内存的释放时机,本质上是 用对象管理资源“资源获取即初始化”(RAII:Resource Acquisition Is Initialization)思想的体现,大幅提升了代码的安全性和可维护性


五、智能指针的本质与优势

1. 本质

智能指针(unique_ptr/shared_ptr)是 RAII思想 的典型实现,RAII 设计思想:用对象生命周期管理资源

  • RAII 是 Resource Acquisition Is Initialization 的缩写,中文译为 “资源获取即初始化”

它是一种管理动态资源的设计思想,核心逻辑是:

  • 资源获取:在对象构造时获取资源(如:内存、文件句柄、网络连接)
  • 资源持有:资源在对象的生命周期内始终有效
  • 资源释放:对象析构时自动释放资源(无需手动调用 delete/close 等)

通过绑定 “资源的生命周期”“对象的生命周期”,避免因手动管理失误(如:忘记释放、异常导致跳过释放)引发的资源泄漏。


2. 优势

智能指针的核心优势:

  1. 自动释放:无需手动调用 delete,即使程序因异常跳转,也能保证内存被释放
  2. 避免泄漏:解决了 “忘记释放”、“重复释放”、“释放后继续使用” 等常见问题
  3. 清晰语义:通过 unique_ptr(独占)和 shared_ptr(共享)清晰表达资源的管理方式

六、使用智能指针的注意事项

1. RAII 与资源释放

  • 智能指针的析构函数默认调用 delete 释放资源
  • 若管理的资源不是new分配的(如:文件句柄、网络连接),需自定义删除器
    • 注意:避免用智能指针管理非动态内存,否则会导致 delete 栈内存的未定义行为
/*------------- 自定义删除器:释放文件句柄 -------------*/
void deleteFile(FILE* fp)
{
    fclose(fp);
}

// 析构时调用 deleteFile 释放资源
std::shared_ptr<FILE> p( fopen("test.txt", "w"), deleteFile );

2. 数组特化支持

  • unique_ptrshared_ptr 可**通过模板特化支持动态数组**
unique_ptr<int[]> arr1 = make_unique<int[]>(10); // 管理 10 个 int 的数组
shared_ptr<int[]> arr2 = make_shared<int[]>(10); // 同理

arr1[0] = 100; // 支持数组下标访问

3. 推荐的创建方式 make_shared/make_unique

  • 相比直接new(如:shared_ptr<int>(new int(10))),工厂函数更安全(避免内存泄漏风险)且高效
auto p1 = make_unique<int>(10); // 推荐

auto p2 = unique_ptr<int>(new int(10)); // 不推荐(异常安全风险)

4. 空指针判断与类型安全

  • 支持operator bool:直接判断智能指针是否为空(管理资源)
if (p1)  // 等价于 p1 != nullptr
{
    cout << "p1 管理资源" << endl;
}
  • 构造函数用explicit修饰:禁止普通指针隐式转换为智能指针,避免误操作
// 编译报错(禁止隐式转换)
shared_ptr<int> p = new int(10); 

// 正确写法
shared_ptr<int> p(new int(10)); 

七、智能指针的删除器

1. 什么是删除器?

删除器:是智能指针(如:unique_ptrshared_ptr)用于释放所管理资源的一个重要机制。

  • 它的核心作用是定义资源的释放方式,确保智能指针在生命周期结束时,能正确、安全地回收所管理的资源
  • 它允许自定义资源释放的方式,而不仅限于 delete 操作,这使得智能指针可以管理各种类型的资源,如:文件句柄、网络连接、锁等

2. 为什么要引入删除器?

智能指针默认使用 delete 运算符释放资源,但实际场景中存在很多特殊情况:

  • 动态数组:需要用 delete[] 释放(而非 delete
  • 非内存资源(如:文件句柄 FILE*、网络套接字 socket):需要调用特定函数释放(如:fcloseclosesocket
  • 自定义的资源释放逻辑(如:日志记录、状态清理等)

此时,默认的 delete 无法满足需求,必须通过自定义删除器来指定资源的释放方式。

总结使用删除器可以管理非 new 分配的资源(如:malloc、fopen 分配的资源)


3. 删除器的本质是什么?

删除器本质是一个= “可调用对象”=,它可以是:

  • 普通函数
  • 仿函数(重载 operator() 的类对象)
  • lambda 表达式
  • 函数指针
  • std::function包装的函数
  • ......

当智能指针需要释放其管理的资源时,会调用这个删除器而不是简单的 delete


八、删除器怎么使用?

1. 与 unique_ptr 配合使用

unique_ptr 是独占所有权的智能指针,其模板参数必须显式指定删除器的类型(删除器类型是 unique_ptr 类型的一部分)


1. 函数指针作为删除器

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

/*------------------------ 定义一个资源类 ------------------------*/
struct Resource
{
    int id;  // 资源的唯一标识,用于区分不同的Resource对象

    Resource(int i) : id(i) { cout << "Resource " << id << " 创建\n"; }
    ~Resource() { cout << "Resource " << id << " 被默认释放\n"; }
};

/*------------------------ 定义“函数指针”删除器 ------------------------*/
template<class T>
void ArrayDeleter(T* ptr) //注意:数组中元素的类型(此处为Resource)
{
    if (ptr) //安全检查:避免对空指针执行释放操作
    {
        //1.显示使用自定义删除器释放数组的提示
        cout << "用 ArrayDeleter 释放数组\n";

        //2.用delete[]释放动态数组(与new[]配对)
        delete[] ptr;
    }
}

int main()
{
    // 创建unique_ptr智能指针,管理一个包含3个Resource对象的动态数组
    unique_ptr<Resource, void(*)(Resource*)> up
    (
        new Resource[3]{ 1, 2, 3 },  // 动态数组:用new[]创建3个Resource对象,初始化ID为1、2、3
        ArrayDeleter<Resource>       // 绑定删除器:指定用ArrayDeleter<Resource>函数释放资源
    );
    //注意:使用自定义删除器时,
    // unique_ptr的模板参数必须显式包含删除器类型,因为删除器类型是unique_ptr类型的一部分

    /* 模板参数说明:<资源类型, 删除器类型(函数指针)>
    *        1. 第一个参数 Resource:表示智能指针管理的资源类型(数组元素类型)
    *        2. 第二个参数 void(*)(Resource*):表示删除器的类型(函数指针类型,指向接收Resource*参数、返回void的函数)
    *
    */
}
/*
*   main函数结束,up的生命周期结束
*   此时unique_ptr会自动调用绑定的删除器(ArrayDeleter<Resource>)释放管理的动态数组
*/

在这里插入图片描述


2. 仿函数作为删除器

  • 仿函数是重载了 operator() 的类,适合需要复用或带状态的删除逻辑

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

/*------------------------ 定义“仿函数”删除器 ------------------------*/
struct FileDeleter
{
    void operator()(FILE* fp) const //参数fp:指向FILE类型的指针(文件句柄),即需要释放的资源
    {
        if (fp)  // 安全检查:避免对空指针执行操作(fclose(nullptr)可能导致崩溃)
        {
            //1.显示当前关闭的文件句柄(便于追踪资源释放)
            cout << "用 FileDeleter 关闭文件\n";

            //2.调用标准库函数fclose关闭文件,释放系统资源
            fclose(fp); //替代手动调用fclose,由智能指针自动触发
        }
    }
};


int main()
{
    // 创建unique_ptr智能指针,管理文件句柄(FILE*类型资源)
    unique_ptr<FILE, FileDeleter> up_file
    (
        fopen("test.txt", "r"),  // 资源初始化:调用fopen打开文件,返回FILE*句柄
        FileDeleter()            // 绑定删除器:传入FileDeleter的实例(可省略,因unique_ptr会默认构造)
    ); //注:"r"表示只读模式,若文件不存在则fopen返回nullptr
    // 注意:若fopen失败(返回nullptr),删除器也会安全处理(因operator()内有if(fp)判断)

    /* 模板参数说明:<资源类型, 仿函数类型>
    *        1. 第一个参数 FILE:表示智能指针管理的资源类型(文件句柄类型)
    *        2. 第二个参数 FileDeleter:表示删除器的类型(仿函数类)
    */
    

    // 当up_file生命周期结束时(main函数退出),发生以下过程:
    // 1. unique_ptr自动调用绑定的删除器(FileDeleter的operator())
    // 2. 删除器调用fclose关闭文件,确保资源不泄漏
    // 无需手动调用fclose,避免了“忘记关闭文件”导致的资源泄漏

    return 0;
}

在这里插入图片描述

3. labmda表达式作为删除器

  • lambda 表达式简洁灵活,适合临时的简单释放逻辑
#include <iostream>   
#include <memory>    
using namespace std;

/*------------------------ 定义一个资源类 ------------------------*/
struct Resource
{
    int id;   // 资源的唯一标识,用于区分不同的Resource对象

    Resource(int i) : id(i) { cout << "Resource " << id << " 创建\n"; }
    ~Resource() { cout << "Resource " << id << " 被默认释放\n"; }
};

int main()
{
    /*------------------------ 定义“lambda表达式”删除器 ------------------------*/
    auto lambda_deleter = [](Resource* ptr) 
        { 
            //lambda捕获列表为空([]),参数为Resource*(指向数组的指针)
            if (ptr)  // 安全检查:避免对空指针执行释放操作
            {
                //1.标识当前使用lambda删除器释放数组
                cout << "用 lambda 释放数组\n";

                //2.用delete[]释放动态数组(与new[]配对)
                delete[] ptr;
            }
        };

    // 创建unique_ptr智能指针,管理包含2个Resource对象的动态数组
    unique_ptr<Resource, decltype(lambda_deleter)> up
    (
        new Resource[2]{ 4, 5 },  // 动态数组:用new[]创建2个Resource对象,ID为4和5
        lambda_deleter            // 绑定删除器:指定用上面定义的lambda表达式释放资源
    );
    //注意:因lambda类型是编译器自动生成的匿名类型,必须用decltype推导

    /* 模板参数说明:<资源类型, decltype(lambda)>(通过 decltype 获取 lambda 类型)
    *        1. 第一个参数 Resource:表示智能指针管理的资源类型(数组元素类型)
    *        2. 第二个参数 decltype(lambda_deleter):通过decltype获取lambda的类型
    */



    /* 当up的生命周期结束时(main函数退出):
    *          1. unique_ptr自动调用绑定的lambda_deleter删除器
    *          2. lambda删除器先打印"用 lambda 释放数组"
    *          3. 再调用delete[]释放数组(触发元素析构,打印"被默认释放")
    *  整个过程无需手动释放,避免内存泄漏
    */
    return 0;
}

在这里插入图片描述


2. 与 shared_ptr 配合使用

shared_ptr 是共享所有权的智能指针,其模板参数不需要包含删除器类型(删除器类型不影响 shared_ptr 本身的类型),只需在构造时传入删除器即可,这使得 shared_ptr 对删除器的使用更灵活


1. 函数指针作为删除器

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

/*------------------------ 定义一个资源类 ------------------------*/
struct Resource
{
    int id;   // 资源的唯一标识,用于区分不同的Resource对象

    Resource(int i) : id(i) { cout << "Resource " << id << " 创建\n"; }
    ~Resource() { cout << "Resource " << id << " 被默认释放\n"; }
};

/*------------------------ 定义“函数指针”删除器 ------------------------*/
template<class T>
void ArrayDeleter(T* ptr) //注意:数组中元素的类型(此处为Resource)
{
    if (ptr) //安全检查:避免对空指针执行释放操作
    {
        //1.显示使用自定义删除器释放数组的提示
        cout << "用 ArrayDeleter 释放数组\n";

        //2.用delete[]释放动态数组(与new[]配对)
        delete[] ptr;
    }
}

int main()
{
    // 创建shared_ptr智能指针,管理包含3个Resource对象的动态数组
    shared_ptr<Resource> sp
    (
        new Resource[3]{ 1, 2, 3 },  // 动态数组:用new[]创建3个Resource对象,ID为1、2、3
        ArrayDeleter<Resource>       // 绑定删除器:传入ArrayDeleter<Resource>函数作为释放逻辑
    );
    /* 注意:
    *    1. shared_ptr模板参数仅需指定资源类型(Resource),无需包含删除器类型
    *    2. shared_ptr的删除器在构造时传入,不影响智能指针类型
    */


    // shared_ptr的核心特性:支持共享所有权,可以创建多个shared_ptr共享同一资源(引用计数会自动增加)
    // 例如:
    // shared_ptr<Resource> sp2 = sp;  // 此时引用计数为2

    /* 当最后一个持有该资源的shared_ptr(如sp、sp2等)生命周期结束时:
    *          1. 引用计数减为0,触发资源释放
    *          2. 自动调用绑定的ArrayDeleter<Resource>删除器
    *          3. 删除器执行:先打印释放提示,再用delete[]释放数组(调用元素析构函数)
    *  整个过程无需手动管理内存,彻底避免内存泄漏
    */


    return 0;
}

在这里插入图片描述

2. 仿函数作为删除器

/*------------------------ 定义一个资源类 ------------------------*/
struct Resource
{
    int id;  // 资源的唯一标识,用于区分不同的Resource对象

    Resource(int i) : id(i) { cout << "Resource " << id << " 创建\n"; }
    ~Resource() { cout << "Resource " << id << " 被默认释放\n"; }
};

/*------------------------ 定义“函数指针”删除器 ------------------------*/


int main()
{
    // 创建shared_ptr智能指针,管理文件句柄(FILE*类型的资源)
    shared_ptr<FILE> sp_file
    (
        fopen("test.txt", "r"),  // 资源初始化:调用fopen打开文件
        FileDeleter()            // 绑定删除器:传入FileDeleter仿函数的实例
    ); 
    /* 注意事项:
    *     1. "r"表示只读模式,成功则返回FILE*指针,失败返回nullptr
    *     2. shared_ptr模板参数仅需指定资源类型(FILE),无需包含删除器类型
    *     2. shared_ptr构造时直接传入删除器,不影响智能指针类型
    *     3. 若fopen失败(返回nullptr),删除器会通过if(fp)判断跳过操作,避免错误
    */

    // shared_ptr支持共享所有权:多个shared_ptr可共享同一文件句柄
    // 例如:
    // shared_ptr<FILE> sp_file2 = sp_file;  // 引用计数+1


    /* 当最后一个持有该文件句柄的shared_ptr(如:sp_file、sp_file2)生命周期结束时:
    *          1. 引用计数减为0,触发资源释放
    *          2. 自动调用FileDeleter仿函数的operator()方法
    *          3. 仿函数内部调用fclose关闭文件,确保资源不泄漏
    *  无需手动调用fclose,避免“忘记关闭文件”导致的系统资源泄漏
    */

    return 0;
}

在这里插入图片描述


3. lambda 表达式作为删除器

int main()
{
    // 创建shared_ptr智能指针,管理包含2个Resource对象的动态数组
    shared_ptr<Resource> sp
    (
        new Resource[2]{ 6, 7 },  // 动态数组:用new[]创建2个Resource对象,ID为6和7
        [](Resource* ptr)         // 直接传入lambda表达式作为删除器
        {
            if (ptr) // 安全检查:避免对空指针执行释放操作
            {
                //1.标识当前使用lambda删除器释放数组
                cout << "用 lambda 释放数组\n";

                //2.用delete[]释放动态数组(与new[]配对)
                delete[] ptr;
            }
        }
    );
    //注意:shared_ptr模板参数仅需指定资源类型(Resource),无需包含删除器类型

    // shared_ptr的特性:支持共享所有权:可以创建多个shared_ptr共享同一资源,引用计数会自动维护
    // 例如:
    // shared_ptr<Resource> sp2 = sp;  // 此时引用计数增加到2

    /* 当最后一个持有该资源的shared_ptr(如:sp、sp2等)生命周期结束时:
    *      1. 引用计数减少到0,触发资源释放机制
    *      2. 自动调用绑定的lambda表达式(删除器)
    *      3. lambda删除器执行流程:
    *               - 先打印"用 lambda 释放数组"
    *               - 再调用delete[]释放数组,此时会依次调用数组中每个元素的析构函数
    *               - 最终完成数组内存的释放,避免内存泄漏
    */

    // 优势:lambda表达式作为删除器无需提前定义,适合简单的释放逻辑,代码更紧凑
    return 0;
}

在这里插入图片描述


3. 删除器使用的总结

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>  // 智能指针头文件
#include <cstdio>  // 文件操作相关函数
using namespace std;

/*---------------------------定义:“日期类”---------------------------*/
struct Date
{
    int _year;  // 年
    int _month; // 月
    int _day;   // 日

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

    //2.实现:“析构函数”
    ~Date()
    {
        //当对象被销毁时输出提示信息,便于追踪智能指针的释放行为
        cout << "~Date()" << endl;
    }
};

/*---------------------------定义“函数指针”删除器:删除动态数组---------------------------*/
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;  // 使用delete[]释放动态数组,与new[]配对
}


/*---------------------------定义“仿函数”删除器:删除动态数组---------------------------*/
template<class T>
class DeleteArray
{
public:
    // 重载()运算符,使类的对象可以像函数一样被调用
    void operator()(T* ptr)
    {
        delete[] ptr;  // 使用delete[]释放动态数组
    }
};


/*---------------------------定义“仿函数”删除器:关闭文件句柄---------------------------*/
class Fclose
{
public:
    // 重载()运算符,接收FILE*类型的指针
    void operator()(FILE* ptr)
    {
        //1.显示关闭的文件指针
        cout << "fclose:" << ptr << endl;

        //2.调用标准库函数,关闭文件
        fclose(ptr);
    }
};


int main()
{
    /*----------------------------------- 案例一:智能指针管理“动态数组” -----------------------------------*/
    // unique_ptr<Date> up1(new Date[10]); 
    // shared_ptr<Date> sp1(new Date[10]); 
    /*  错误示例:直接用智能指针管理数组会导致内存泄漏
    *        1. 原因:智能指针默认使用delete释放资源,而动态数组需要用delete[]
    *        2. 所以:以上两行代码会编译通过,但运行时会产生未定义行为(内存泄漏或崩溃)
    */

    /*--------------- 解决方案1:利用智能指针的数组特化版本 --------------*/

    //注:unique_ptr和shared_ptr都提供了数组版本的特化,会自动使用delete[]
    unique_ptr<Date[]> up1(new Date[5]);  // 管理包含5个Date对象的动态数组
    shared_ptr<Date[]> sp1(new Date[5]);  // 数组特化版本,自动调用delete[]



    /*--------------- 解决方案2.1:自定义删除器(函数指针版本)--------------*/

    //1. unique_ptr需指定函数指针类型作为模板参数
    unique_ptr<Date, void(*)(Date*)> up2(new Date[5], DeleteArrayFunc<Date>);

    //2. shared_ptr对函数指针的支持更简洁,直接传入函数名即可
    shared_ptr<Date> sp2(new Date[5], DeleteArrayFunc<Date>);


    /*--------------- 解决方案2.2:自定义删除器(仿函数版本)--------------*/

    //1. unique_ptr需要在模板参数中显式指定删除器类型
    unique_ptr<Date, DeleteArray<Date>> up3(new Date[5], DeleteArray<Date>());

    //2. shared_ptr不需要在模板参数中指定删除器类型,直接在构造函数中传入即可
    shared_ptr<Date> sp3(new Date[5], DeleteArray<Date>());


    /*--------------- 解决方案2.3:自定义删除器(lambda表达式版本)--------------*/

    // 定义一个lambda表达式,用于释放动态数组
    auto delArrOBJ = [](Date* ptr) { delete[] ptr; };

    //1. unique_ptr需要用decltype获取lambda的类型作为模板参数
    unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);

    //2. shared_ptr可以直接使用lambda作为删除器,无需指定类型
    shared_ptr<Date> sp4(new Date[5], delArrOBJ);


    /*----------------------------------- 案例二:智能指针管理“非内存资源” -----------------------------------*/

    //1.使用Fclose仿函数作为删除器
    shared_ptr<FILE> sp5(fopen("test.txt", "r"), Fclose());


    //2.使用lambda表达式作为删除器(功能与Fclose仿函数相同)
    shared_ptr<FILE> sp6(
        fopen("test.txt", "r")
        , [](FILE* ptr)
        {
            cout << "fclose:" << ptr << endl;  // 调试输出
            fclose(ptr);                       // 关闭文件
        }
    );

    /* 程序结束时,所有智能指针会自动调用对应的删除器释放资源:
    *          1. 数组特化版本:调用delete[]
    *          2. 自定义删除器版本调用:对应的函数/仿函数/lambda
    *          3. 文件指针会被正确关闭,避免资源泄漏
    */
    return 0;
}


4. unique_ptr与shared_ptr使用删除器的差异是什么?

总结unique_ptrshared_ptr 使用删除器的差异


在这里插入图片描述


5. 使用删除器的需要注意什么?

使用删除器的注意事项:

  1. 删除器的参数类型必须匹配:删除器接收的参数类型必须与智能指针管理的资源类型一致(如:管理 FILE* 资源,删除器必须接收 FILE*
  2. 避免删除空指针:删除器内部最好判断 ptr != nullptr,避免对空指针执行释放操作(虽然 delete nullptr 是安全的,但 fclose(nullptr) 可能崩溃)
  3. shared_ptr的删除器存储在控制块shared_ptr 的删除器与引用计数一起存在控制块中,因此即使删除器很大,也不会增加 shared_ptr 本身的大小
  4. 默认删除器的局限性:智能指针默认使用 delete 释放资源,因此管理动态数组非内存资源时,必须显式指定删除器,否则会导致未定义行为

总结:

删除器的使用核心根据资源类型,定义对应的释放逻辑,然后将其绑定到智能指针

具体选择哪种形式的删除器(函数指针/仿函数 /lambda),取决于场景的复杂度和复用需求。


结语

智能指针是 C++11 引入的最具价值的语言特性之一,它用 RAII 的思想彻底改变了传统的资源管理方式,让 C++ 在保持高性能的同时,也拥有了更强的安全性。

在本文中,我们从基础概念到实际使用,从所有权模型到删除器机制,全面分析了 unique_ptrshared_ptrweak_ptr 的应用场景及注意事项。希望你在阅读之后,不仅能正确使用智能指针,更能理解其设计背后的思想:用对象的生命周期管理资源,让资源管理自动化、安全化、工程化。

智能指针并不是银弹,但它们确实是现代 C++ 编程中不可或缺的可靠工具。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐