欢迎光临
我们一直在努力

【C++】 智能指针


   C++ 智能指针完整详解   

前言

        在 C++ 裸指针开发中,内存泄漏、野指针、重复释放、异常安全等问题长期困扰开发者。C++11 引入智能指针(Smart Pointer),基于 RAII(资源获取即初始化)思想自动管理堆内存生命周期,程序退出作用域时自动释放内存,彻底规避手动 new/delete 带来的各类内存隐患。

        标准库智能指针全部定义于头文件 <memory>,核心三类:std::unique_ptr、std::shared_ptr、std::weak_ptr。

        本文从原理、API、场景、坑点、性能对比完整拆解。


一、核心基础:RAII 机制

智能指针本质是栈上封装裸指针的模板类,核心逻辑 RAII:

  • 构造函数:接管堆内存(new 出来的对象);
  • 析构函数:自动调用 delete 释放内存;
  • 离开作用域(函数返回、代码块结束、异常抛出)时,栈对象自动销毁,析构触发内存释放。
  • 对比裸指针痛点:

    // 裸指针问题:异常会导致内存泄漏
    void bad_func() {
    int* p = new int(10);
    throw runtime_error("error"); // 抛出异常,delete 永远不会执行,内存泄漏
    delete p;
    }

    // unique_ptr 自动兜底,异常也会释放内存
    void good_func() {
    unique_ptr<int> p(new int(10));
    throw runtime_error("error"); // 栈对象销毁,自动释放内存
    }


    二、std::unique_ptr 独占智能指针(推荐优先使用)

    1. 核心特性

    独占所有权:同一时刻只能有一个 unique_ptr 持有对象,禁止拷贝,仅支持移动语义。

    • 开销极低:底层仅封装一个裸指针,无额外引用计数;
    • 性能几乎等同于裸指针,日常开发首选;
    • 不支持拷贝构造 / 赋值,仅支持 std::move 转移所有权。

    2. 基础用法

    (1)创建方式

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

    // 方式1:new 初始化(不推荐)
    unique_ptr<int> p1(new int(100));

    // 方式2:make_unique(C++14 推荐,异常安全、简洁)
    auto p2 = make_unique<string>("hello unique");

    // 管理数组
    auto arr = make_unique<int[]>(5); // 长度5的int数组
    arr[0] = 1;

    (2)移动所有权

    unique_ptr<int> a = make_unique<int>(10);
    // unique_ptr<int> b = a; // 编译报错,禁止拷贝

    unique_ptr<int> b = move(a); // 转移所有权
    cout << (a == nullptr) << endl; // true,a 已置空
    cout << *b << endl; // 10

    (3)常用成员函数

    auto p = make_unique<int>(666);

    p.get(); // 返回内部裸指针 int*
    p.release(); // 释放所有权,返回裸指针,unique_ptr 置空(需手动delete)
    p.reset(); // 销毁当前管理对象,置空
    p.reset(new int(999)); // 释放旧内存,接管新内存
    if (p) {} // 重载bool,判断是否持有有效资源
    *p; p->func(); // 重载解引用、箭头,和裸指针用法一致

    3. 使用场景

  • 函数局部临时堆对象(默认首选);
  • 容器存储独占资源(vector<unique_ptr<T>>);
  • 类成员独占资源,无需共享;
  • 工厂函数返回动态对象(移动语义自动优化,无拷贝开销)。
  • 4. 自定义删除器

    unique_ptr 删除器作为模板参数,支持自定义释放逻辑(管理文件句柄、socket 等):

    // 自定义释放函数
    void file_del(FILE* f) {
    cout << "关闭文件" << endl;
    fclose(f);
    }

    // 指定删除器类型
    unique_ptr<FILE, decltype(file_del)*> fp(fopen("test.txt", "r"), file_del);

    数组版本默认调用 delete[],无需手动处理数组释放。


    三、std::shared_ptr 共享智能指针

    1. 核心特性

    共享所有权:多份 shared_ptr 可同时持有同一个对象,内部维护原子引用计数器。

    • 引用计数规则:
    • 创建 shared_ptr 计数 = 1;
    • 拷贝 / 赋值给新 shared_ptr,计数 + 1;
    • 销毁 shared_ptr(离开作用域、reset),计数 – 1;
    • 计数归 0 时,自动释放堆内存。
    • 原子计数器:多线程安全增减计数,但对象本身访问不保证线程安全;
    • 额外开销:堆上分配控制块(计数 + 删除器),性能弱于 unique_ptr。

    2. 基础用法

    (1)创建(优先 make_shared)

    // make_shared:一次性分配对象+控制块,内存更高效
    auto s1 = make_shared<int>(200);
    shared_ptr<int> s2 = s1; // 拷贝,计数+1

    cout << s1.use_count() << endl; // 2,查看当前引用数

    (2)核心接口

    auto sp = make_shared<string>("shared test");
    sp.get(); // 获取裸指针
    sp.reset(); // 计数-1,计数0则释放
    sp.reset(new string()); // 接管新对象
    sp.use_count(); // 返回当前引用计数
    sp.unique(); // 判断计数是否等于1(C++17弃用)

    3. 经典坑:循环引用(内存泄漏根源)

    当两个对象互相持有对方 shared_ptr,引用计数永远无法归零,内存永久泄漏。

    struct Node {
    int val;
    shared_ptr<Node> next;
    Node(int v) : val(v) {}
    };

    void cycle_leak() {
    auto a = make_shared<Node>(1);
    auto b = make_shared<Node>(2);
    a->next = b;
    b->next = a;
    // 函数结束,a、b销毁,计数各减1,但仍为1,内存泄漏!
    }

    解决方案:搭配 std::weak_ptr 打破循环引用。

    4. 使用场景

  • 多个模块需要共享同一个资源;
  • 缓存、全局资源池;
  • 订阅回调、多持有者对象;
  • 注意:无共享需求时,一律用 unique_ptr。

  • 四、std::weak_ptr 弱智能指针(配套 shared_ptr)

    1. 核心特性

    • 不增加引用计数,仅作为 shared_ptr 的观察者;
    • 解决 shared_ptr 循环引用泄漏;
    • 不能直接解引用访问对象,必须通过 lock() 升级为 shared_ptr;
    • 可检测对象是否已销毁(expired())。

    2. 完整解决循环引用示例

    #include <memory>
    struct Node {
    int val;
    weak_ptr<Node> next; // 改用 weak_ptr 打破循环
    Node(int v) : val(v) {}
    };

    void fix_cycle() {
    auto a = make_shared<Node>(1);
    auto b = make_shared<Node>(2);
    a->next = b;
    b->next = a;
    // 函数退出,计数归零,内存正常释放
    }

    // weak_ptr 访问对象
    auto wp = a->next;
    if (!wp.expired()) { // 对象未销毁
    auto sp = wp.lock(); // 升级为shared_ptr,计数临时+1
    cout << sp->val << endl;
    }

    3. 常用接口

    weak_ptr<T> wp(sp); // 由shared_ptr构造
    wp.expired(); // true=对象已释放
    wp.lock(); // 返回shared_ptr,失效返回空shared_ptr
    wp.reset(); // 清空弱指针
    wp.use_count(); // 获取关联shared_ptr的引用数

    4. 使用场景

  • 打破 shared_ptr 循环引用;
  • 缓存弱引用(不阻止资源释放,资源销毁后自动失效);
  • 观察者模式,观察者不持有对象生命周期。

  • 五、三种智能指针完整对比表

    特性unique_ptrshared_ptrweak_ptr
    所有权模型 独占,不可拷贝 共享,可拷贝 无所有权,仅观察
    引用计数 有(原子)
    拷贝 / 赋值 禁止,仅移动 允许 允许(基于 shared_ptr)
    内存开销 极小(仅裸指针) 较高(控制块堆分配) 极小(存储控制块地址)
    线程安全 无计数,移动需手动同步 计数增减原子安全,对象不安全 无计数,线程安全判断失效
    循环引用问题 不存在(无法循环持有) 会内存泄漏 专门解决循环引用
    适用场景 绝大多数独占资源场景 多模块共享资源 配套 shared_ptr、打破循环
    C++ 标准支持 C++11(make_unique C++14) C++11

    六、最佳实践规范(博客重点总结)

  • 优先使用 unique_ptr,无共享需求绝不使用 shared_ptr,性能最优;
  • 创建智能指针统一用 make_unique / make_shared,避免裸 new:
    • 异常安全:内存分配失败不会造成裸指针泄漏;
    • 内存连续分配,减少堆碎片;
  • 尽量避免 get() 裸指针长期保存,防止悬垂;
  • 函数返回动态对象直接返回 unique_ptr,移动语义零开销;
  • 存在双向 / 环形依赖时,一方使用 weak_ptr 打破循环;
  • 多线程场景:shared_ptr 计数安全,但对象读写必须加锁;
  • 不要混用裸指针与智能指针管理同一资源,双重释放崩溃;
  • 数组资源优先 make_unique<T[]>,自动调用 delete[]。

  • 七、常见踩坑汇总

    坑 1:裸指针初始化多个 shared_ptr

    int* p = new int(10);
    shared_ptr<int> s1(p);
    shared_ptr<int> s2(p); // 两个独立控制块,析构时重复delete,程序崩溃

    解决:全程只用 make_shared,不分离裸指针。

    坑 2:函数传参裸指针给智能指针

    void func(shared_ptr<int> sp) {}
    int* p = new int(10);
    func(p); // 隐式构造shared_ptr,离开函数释放,外部p变成野指针

    解决:传参直接传 shared_ptr,禁止裸指针隐式转换。

    坑 3:weak_ptr 不判断 expired 直接 lock

    对象已销毁时 lock 返回空 shared_ptr,解引用直接崩溃,必须先判断。

    坑 4:unique_ptr 拷贝赋值

    编译直接报错,改用 std::move 转移所有权。


    八、自定义删除器完整示例

    智能指针默认释放逻辑:

    • unique_ptr<T>:析构调用 delete ptr;数组特化 unique_ptr<T[]> 自动 delete[] ptr
    • shared_ptr<T>:析构调用 delete ptr

    删除器:用户自定义的可调用对象,接管资源释放逻辑,替换默认 delete

    一、unique_ptr 删除器核心规则

    1. 语法规则

    unique_ptr 删除器是模板类型参数,类型固定在编译期,结构:

    template< class T, class Deleter = default_delete<T> > class unique_ptr;

    • 第二个模板参数为删除器类型,默认 std::default_delete<T>(封装 delete);
    • 必须在定义时显式指定删除器类型;
    • 无运行时开销,删除器对象存储在 unique_ptr 内部,不额外堆分配。
    2. 四种删除器写法
    (1)函数指针删除器

    template<typename T>
    void ArrayDel(T* p) {
    cout << "数组释放 delete[]" << endl;
    delete[] p;
    }

    // Deleter类型:void(*)(Date*) 无捕获函数指针
    unique_ptr<Date, void(*)(Date*)> up(new Date[5], ArrayDel<Date>);

    (2)无捕获 lambda

    无捕获 lambda 可隐式转为函数指针,也可用decltype推导类型:

    auto del = [](Date* p){ delete[] p; };
    unique_ptr<Date, decltype(del)> up(new Date[5], del);

    (3)有捕获 lambda

    不能转函数指针,只能decltype推导:

    int logFlag = 1;
    auto del = [logFlag](Date* p){
    if(logFlag) cout << "释放数组" << endl;
    delete[] p;
    };
    unique_ptr<Date, decltype(del)> up(new Date[5], del);

    (4)仿函数( 结构体重载 ( ) )

    struct FileDel {
    void operator()(FILE* f) const {
    if(f) fclose(f);
    }
    };
    // 仿函数作删除器
    unique_ptr<FILE, FileDel> fp(fopen("a.txt","r"));

    3. 短板

            删除器属于类型一部分,同 T、不同删除器的unique_ptr是完全不同类型,无法互相赋值 / 移动。

    二、shared_ptr 删除器核心规则

    1. 语法规则

    shared_ptr 模板只有一个类型参数,删除器不写入模板:

    template< class T > class shared_ptr;

    • 删除器存放在堆上的控制块中,运行时存储;
    • 任意可调用对象均可传入,声明无需修改模板;
    • 拷贝、赋值不受删除器类型影响,不同删除器的shared_ptr<T>可互相赋值;
    • 代价:控制块堆分配,轻微性能损耗。
    2. 四种删除器通用写法

    // 1. 函数指针
    shared_ptr<Date> sp1(new Date[5], ArrayDel<Date>);

    // 2. 内联lambda(最常用)
    shared_ptr<Date> sp2(new Date[5], [](Date* p){delete[] p;});

    // 3. 仿函数
    shared_ptr<FILE> sp3(fopen("a.txt","r"), FileDel{});

    // 4. 带捕获lambda
    int flag = 1;
    shared_ptr<Date> sp4(new Date[5], [flag](Date* p){
    if(flag) cout << "释放数组" << endl;
    delete[] p;
    });

    关键特性

    拷贝shared_ptr时,会同步复制控制块里的删除器;所有共享对象销毁时,统一调用同一个删除器。

    三、unique_ptr vs shared_ptr 删除器对比

    维度unique_ptr 删除器shared_ptr 删除器
    是否模板参数 是,影响类型 否,不影响类型
    存储位置 unique_ptr 栈内,无堆开销 堆控制块,额外内存开销
    类型兼容性 删除器不同则类型不同,无法转移 删除器不同仍为同类型,可赋值共享
    写法复杂度 必须显式声明 Deleter 类型 直接传可调用对象,极简
    适用场景 独占资源、追求极致性能 共享资源、外部句柄管理

    赞(0)
    未经允许不得转载:171主机测评 » 【C++】 智能指针
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址