导读:本期聚焦于苏锦程创作的《C++智能指针如何实现自定义删除器?unique_ptr与shared_ptr高级用法解析》,敬请观看详情。直接把FILE指针或socket句柄塞进智能指针,程序一退出就可能崩溃,因为默认删除器只认识delete。这个问题在封装第三方资源时尤其突出。自定义删除器允许我们指定释放动作,让unique_ptr和shared_ptr不仅能管理new出来的内存,还能安全关闭文件、释放套接字、归还内存池对象。本文从删除器的类型机制讲起:unique_ptr把删除器作为模板参数,影响对象大小;shared_ptr把删除器放进控制块,做到类型擦除。随后演示函数指针、lambda、函数对象三种写法,并比较它们对智能指针体积和灵活性的影响。最后结合FILE句柄、数组、动态库资源等场景,梳理空指针判断、数组删除、捕获状态等容易踩坑的细节。读完可以明白什么时候该用unique_ptr,什么时候shared_ptr更适合带自定义删除器的资源管理。

C++智能指针的默认释放动作是调用delete或delete[],这对于直接由new创建的对象没有问题。但如果资源来自fopen、malloc、socket或者某个第三方库的分配函数,就必须使用对应的释放函数。自定义删除器就是用来指定智能指针析构时执行哪一段清理逻辑。

自定义删除器在unique_ptr和shared_ptr中的实现思路并不相同:前者把删除器类型作为模板参数的一部分,后者则把删除器放入控制块,做到运行时类型擦除。本文通过多个可编译示例展示常见写法,并分析它们对对象大小、灵活性和异常安全的影响。

C++智能指针如何实现自定义删除器?unique_ptr与shared_ptr高级用法解析

为什么默认删除器不够用

标准库为std::unique_ptr<T>提供了默认删除器std::default_delete<T>,它内部只做一件事:delete ptr。如果T是数组类型,则特化版本会调用delete[] ptr。这意味着std::unique_ptr<FILE>在离开作用域时会执行delete file,而不是fclose。FILE指针一般来自C标准库的fopen,用delete释放属于未定义行为。

类似问题也出现在Windows编程中。例如HANDLE通常通过CloseHandle关闭,HMODULE通过FreeLibrary卸载,socket描述符通过close或closesocket关闭。如果把这类资源直接交给智能指针默认管理,要么编译不过,要么运行期崩溃。自定义删除器可以统一管理这些资源的生命周期,不需要手写每个类的析构函数。

另一个典型场景是自定义内存池。池中的对象不能简单用delete,而需要调用池提供的归还接口。把归还动作交给删除器后,调用方就可以像使用普通智能指针一样安全地使用池对象。删除器的引入让智能指针从内存管理器扩展为通用的RAII资源管理器。

unique_ptr自定义删除器的三种实现

std::unique_ptr的第二个模板参数是删除器类型,默认是std::default_delete<T>。当需要自定义删除器时,通常有三种写法:函数指针、lambda表达式和函数对象。三者都可以让unique_ptr在析构时执行指定清理动作,但对象大小和调用开销不同。

#include <cstdio>
#include <memory>

struct FileCloser {
    void operator()(FILE* f) const noexcept {
        if (f) {
            std::fclose(f);
        }
    }
};

int main() {
    std::unique_ptr<FILE, FileCloser> file(std::fopen("data.txt", "r"));
    return 0;
}

上面的函数对象FileCloser不包含任何成员变量,并且在unique_ptr内部通常可以享受空基类优化,因此不会增大unique_ptr的体积。如果删除器是有状态的,例如需要记录日志或统计释放次数,那么这些状态会直接存储在unique_ptr对象中。

函数指针写法最直接,但会增加一个指针的存储开销:

std::unique_ptr<FILE, decltype(&std::fclose)> file(
    std::fopen("data.txt", "r"),
    &std::fclose
);

这里decltype(&std::fclose)推导出函数指针类型,删除器作为第二个构造参数传入。由于函数指针无法携带额外状态,灵活性较低。无捕获lambda可以转换为函数指针,但作为删除器类型时仍会退化成类似函数指针的大小;有捕获lambda则会把捕获变量保存到对象中。通常情况下,函数对象是最推荐的选择,既没有额外运行期开销,又能自由携带状态。

对于数组资源,std::unique_ptr提供了T[]特化版本,默认调用delete[]。如果要用自定义释放函数,注意删除器的参数仍然是元素指针。例如:

auto arrayDeleter = [](int* p) { delete[] p; };
std::unique_ptr<int[], decltype(arrayDeleter)> arr(new int[10], arrayDeleter);
arr[0] = 42;

该写法与普通对象类似,但访问元素时可以直接使用operator[]。如果仍然写成std::unique_ptr<int>却传入new int[10],删除器就会被错误调用,这是常见的坑。

shared_ptr的删除器机制与类型擦除

与unique_ptr不同,std::shared_ptr的删除器不是模板类型参数,而是保存在控制块里的可调用对象。因此shared_ptr<FILE>无论使用何种删除器,类型都相同。这让多个资源指针可以放入同一个容器,也允许在运行期动态决定释放策略。

std::shared_ptr<FILE> file(
    std::fopen("data.txt", "r"),
    [](FILE* f) {
        if (f) {
            std::fclose(f);
        }
    }
);

删除器随控制块一起被引用计数管理,即使某个shared_ptr副本先析构,控制块也不会提前释放,最终最后一个shared_ptr销毁时才会调用删除器。这种类型擦除的代价是控制块分配和额外函数调用,但换来的是更高的设计灵活性。例如可以在不改变接口的情况下,让同一个shared_ptr<T>类型分别管理来自不同分配器的资源。

shared_ptr管理数组时,需要主动使用delete[],否则默认仍是delete。C++17引入了std::shared_ptr<T[]>,配合操作符[]访问元素,行为更清晰:

std::shared_ptr<int[]> sp(
    new int[10],
    [](int* p) { delete[] p; }
);
sp[0] = 1;

注意C++17之前的shared_ptr没有数组特化,operator*和operator->对数组意义不大,只能通过get()获取原始指针再索引。如果代码库需要兼容C++14及更早版本,建议用std::vector代替动态数组,再配合智能指针。

另外,shared_ptr的删除器不会改变shared_ptr对象自身的大小,始终是控制块指针加数据指针的两倍大小。如果追求极致性能和内存占用,unique_ptr加函数对象是更好的选择;如果需要容器存储、跨模块传递或运行期绑定释放逻辑,shared_ptr更加合适。

实战场景与常见避坑点

自定义删除器的典型应用之一是封装C风格文件接口。假如一个类需要打开多个文件,传统做法是手动在析构函数里循环关闭,容易漏掉异常路径。用unique_ptr管理FILE句柄后,析构自动完成,代码更简洁:

#include <cstdio>
#include <memory>
#include <vector>

struct FileCloser {
    void operator()(FILE* f) const noexcept {
        if (f) std::fclose(f);
    }
};

int main() {
    std::vector<std::unique_ptr<FILE, FileCloser>> files;
    files.emplace_back(std::fopen("a.txt", "r"));
    files.emplace_back(std::fopen("b.txt", "r"));
    // 不需要手动 fclose
    return 0;
}

这里std::vector存储unique_ptr<FILE, FileCloser>,每个元素析构时会自动检查并关闭文件。即使中途抛出异常,已构造的文件句柄也会被正确释放。注意不要在删除器中吃掉异常,也不要删除空指针前不判断:大多数C标准库函数对空指针不友好,增加判空是必要的。

另一个高频场景是管理动态库句柄。Windows下HMODULE通过FreeLibrary卸载,Linux下dlopen返回的void*通过dlclose释放。可以分别定义平台相关的删除器:

#ifdef _WIN32
#include <windows.h>
using ModulePtr = std::unique_ptr<HMODULE, decltype(&FreeLibrary)>;
inline ModulePtr make_module(const char* name) {
    HMODULE h = LoadLibraryA(name);
    return ModulePtr(h, &FreeLibrary);
}
#else
#include <dlfcn.h>
using ModulePtr = std::unique_ptr<void, decltype(&dlclose)>;
inline ModulePtr make_module(const char* name) {
    void* h = dlopen(name, RTLD_LAZY);
    return ModulePtr(h, &dlclose);
}
#endif

这个例子同时展示了如何把平台差异封装在工厂函数中,对外只暴露统一的ModulePtr。如果删除器可能为空或平台函数有特定调用约定,最好再包一层函数对象,避免依赖原始函数指针。

常见注意事项:删除器应尽量为noexcept,因为析构函数默认不应抛出异常;unique_ptr自定义删除器后类型会随删除器改变,跨接口传递时可能带来签名变化;shared_ptr虽然类型稳定,但需要注意循环引用和线程安全。此外不要用std::function作为unique_ptr的删除器类型,因为它会额外引入堆分配和间接调用,除非确实需要运行期更换删除器。

C++智能指针自定义删除器shared_ptr修改时间:2026-09-18 02:51:09

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58643.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。