一文带你掌握C++中智能指针如何自定义删除器

 更新时间:2026年04月14日 09:36:57   作者:OxyTheCrack  
智能指针的自定义删除器用于管理非标准资源释放,如C库资源(redisFree)和非new分配的内存等,实现方式主要有两种,下面小编就和大家详细介绍一下吧

为什么需要自定义删除器

智能指针的核心作用是“自动管理资源”,其底层逻辑是:当智能指针对象生命周期结束时,自动调用析构函数释放其托管的资源。但默认的析构逻辑是调用delete,这在很多场景下并不适用。

典型适用场景

  • C语言库资源:比如hiredis库的redisContext,创建用redisConnect(),释放必须用redisFree(),而非delete;又如文件操作,fopen()创建的FILE*,必须用fclose()释放。
  • 非new分配的内存:用malloc/calloc分配的内存,需要用free()释放,无法用delete。
  • 自定义释放逻辑:释放资源时需要额外操作,比如关闭socket前发送终止信号、解锁互斥锁、注销资源等。
  • 避免资源泄漏:如果不指定自定义删除器,智能指针会用delete释放非new创建的资源,导致未定义行为(崩溃、内存泄漏)。

反面案例

以Redis连接为例,若直接使用默认智能指针,会导致严重问题:

#include <hiredis/hiredis.h>
#include <memory>

// 错误写法:未指定自定义删除器
std::unique_ptr<redisContext> ctx(redisConnect("127.0.0.1", 6379));

// 析构时会调用delete释放redisContext,而非redisFree()
// 后果:内存泄漏、连接未关闭、程序可能崩溃

这就是自定义删除器的核心价值:告诉智能指针“如何正确释放资源”。

自定义删除器的两种核心实现方式

自定义删除器的本质是“给智能指针传递一个可调用对象”,让智能指针在析构时调用该对象,完成资源释放。常用的实现方式有两种:函数指针型仿函数型,二者各有优劣,适用于不同场景。

方式一:函数指针型

将释放资源的函数(如redisFree)作为函数指针,传递给智能指针,作为删除器。

实现步骤

  1. 定义智能指针类型时,指定“托管类型”和“函数指针类型”(用decltype获取函数指针类型)。
  2. 创建智能指针对象时,传入“资源指针”和“删除器函数指针”。
  3. 注意:返回空智能指针时,必须显式传入删除器函数指针,否则会出现未定义行为。

实战代码(Redis连接池片段)

#include <hiredis/hiredis.h>
#include <memory>

// 1. 定义带函数指针删除器的智能指针
using redisPtr = std::unique_ptr<redisContext, decltype(&redisFree)>;

// 2. 创建智能指针(必须传入删除器)
redisContext* raw_ctx = redisConnect("127.0.0.1", 6379);
redisPtr ctx(raw_ctx, redisFree); // 正确:传入资源指针+删除器

// 3. 返回空智能指针(必须显式传入删除器)
redisPtr getEmptyConn() {
    // 错误:return nullptr; (未传入删除器,函数指针为野指针,析构崩溃)
    return redisPtr{nullptr, redisFree}; // 正确
}

优缺点分析

优点:实现简单,无需额外定义类/结构体,直接复用现有释放函数。

缺点:

  • 智能指针会额外存储函数指针,增加内存开销;
  • 不能直接return nullptr;
  • 函数指针无法捕获额外上下文(如需自定义释放逻辑,不够灵活)。

更推荐的写法:方式二:仿函数型

定义一个结构体(或类),重载()运算符(仿函数),将释放资源的逻辑写在运算符重载函数中。这种方式是企业级开发的首选,无额外内存开销,且灵活安全。

实现步骤

  • 定义仿函数结构体,重载()运算符,参数为“托管资源的指针”,函数体内实现释放逻辑。
  • 定义智能指针类型时,指定“托管类型”和“仿函数类型”。
  • 创建智能指针对象时,只需传入资源指针,无需显式传入删除器(仿函数作为类型一部分,自动绑定)。
  • 返回空智能指针时,可直接return nullptr,无需额外操作。

实战代码(Redis连接池完整片段)

#include <hiredis/hiredis.h>
#include <memory>
#include <queue>
#include <mutex>

// 1. 定义仿函数删除器(核心:重载()运算符)
struct RedisDeleter {
    // 释放逻辑:判断指针非空,调用redisFree释放
    void operator()(redisContext* ptr) const {
        if (ptr) {
            redisFree(ptr);
        }
    }
};

// 2. 定义带仿函数删除器的智能指针(无额外内存开销)
using redisPtr = std::unique_ptr<redisContext, RedisDeleter>;

// 3. Redis连接池类
class RedisConnectPool {
private:
    std::queue<redisPtr> connections_; // 队列存智能指针,自动释放
    std::mutex mutex_;
public:
    // 获取连接:直接return nullptr,无需传删除器
    redisPtr getConnection() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (connections_.empty()) {
            return nullptr; // 正确:仿函数删除器自动绑定,无未定义行为
        }
        auto conn = std::move(connections_.front());
        connections_.pop();
        return conn;
    }

    // 归还连接:智能指针自动管理,无需手动释放
    void returnConnection(redisPtr conn) {
        std::lock_guard<std::mutex> lock(mutex_);
        connections_.push(std::move(conn));
    }
};

优缺点分析

优点:

  • 无额外内存开销(仿函数作为类型一部分,不占额外空间);
  • 可直接return nullptr,不易踩坑;
  • 仿函数可捕获上下文(比如添加日志、额外释放操作),灵活度高;
  • 类型安全,不易混用。

缺点:需要额外定义一个仿函数结构体(代码量略有增加,但可复用)。

避坑指南

结合笔者在Redis连接池开发中的踩坑经验,总结4个高频坑点,避开这些就能写出安全的代码。

坑点1:仿函数重载()的返回值错误

自定义删除器的仿函数,()运算符必须是无返回值(void),因为智能指针调用删除器时,不会处理返回值。若返回bool等类型,会导致编译警告,甚至未定义行为。

// 错误写法:返回bool
struct RedisDeleter {
    bool operator()(redisContext* ptr) { // ❌ 错误,返回值无用且有风险
        if (ptr) redisFree(ptr);
    }
};

// 正确写法:无返回值
struct RedisDeleter {
    void operator()(redisContext* ptr) const { // ✅ 正确
        if (ptr) redisFree(ptr);
    }
};

坑点2:函数指针型删除器直接return nullptr

函数指针型删除器的智能指针,空指针必须显式传入删除器函数指针。因为智能指针需要同时初始化“资源指针”和“删除器函数指针”,只传nullptr会导致删除器为野指针,析构时崩溃。

// 错误(函数指针型)
redisPtr getEmptyConn() {
    return nullptr; // ❌ 未传入删除器,野指针崩溃
}

// 正确(函数指针型)
redisPtr getEmptyConn() {
    return redisPtr{nullptr, redisFree}; // ✅ 显式传入删除器
}

坑点3:混用不同删除器的智能指针

std::unique_ptr的删除器是“类型的一部分”——不同删除器的智能指针,是不同的类型,不能互相赋值、传递。

// 两种不同删除器的智能指针(不同类型)
using Ptr1 = std::unique_ptr<redisContext, decltype(&redisFree)>;
using Ptr2 = std::unique_ptr<redisContext, RedisDeleter>;

Ptr1 ptr1(redisConnect("127.0.0.1", 6379), redisFree);
Ptr2 ptr2 = ptr1; // ❌ 错误:类型不匹配,无法赋值

坑点4:手动调用reset()后重复释放

智能指针的reset()方法会释放当前托管的资源,若之后再调用pop()(队列中)或让智能指针生命周期结束,会导致双重释放吗?答案是:不会。

原因:reset()释放的是“托管的资源”,智能指针本身还活着;pop()会销毁智能指针,此时智能指针已为空,析构时不会再释放资源。但注意:手动reset()是多余的,智能指针会自动释放。

std::queue<redisPtr> q;
q.emplace(redisConnect("127.0.0.1", 6379));

// 多余但安全的写法
q.front().reset(); // 释放资源,智能指针变为空
q.pop(); // 销毁空智能指针,无操作

// 推荐写法(无需reset)
q.pop(); // 直接销毁智能指针,自动释放资源

两种删除器对比与选型建议

对比维度函数指针型仿函数型
内存开销有(存储函数指针)无(作为类型一部分)
使用复杂度简单(直接复用释放函数)略复杂(需定义仿函数)
返回空指针需显式传入删除器可直接return nullptr
灵活度低(无法捕获上下文)高(可添加自定义逻辑)
工程推荐度低(仅适用于简单场景)高(工业级标准写法)

选型建议

  • 简单场景(如单独使用一个C库资源,无需额外释放逻辑):可使用函数指针型。
  • 工程开发(如连接池、工具类、长期运行的服务):优先使用仿函数型,安全、灵活、无额外开销。

到此这篇关于一文带你掌握C++中智能指针如何自定义删除器的文章就介绍到这了,更多相关C++智能指针自定义删除器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论