it编程 > 编程语言 > C/C++

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

30人参与 2026-04-14 C/C++

为什么需要自定义删除器

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

典型适用场景

反面案例

以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}; // 正确
}

优缺点分析

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

缺点:

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

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

实现步骤

实战代码(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));
    }
};

优缺点分析

优点:

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

避坑指南

结合笔者在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++智能指针自定义删除器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

您想发表意见!!点此发布评论

推荐阅读

高性能C++ 日志实战:spdlog 核心架构解析与最佳实践指南

04-14

C++接口内部内存分配问题设计方案

04-13

C++引用及基本用法全解

04-13

C++指针、引用与取地址运算符对比分析

04-15

C++ 类的定义和实例化全解

04-13

C++中的priority_queue容器使用及说明

04-10

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论