自定义删除器:让智能指针管住 FILE*、socket 与数据库连接

发布时间:2026/10/11 1:40:37
自定义删除器:让智能指针管住 FILE*、socket 与数据库连接 std::unique_ptr的默认删除器是delete所以它能漂亮地管住一块new出来的内存可现实里大量资源根本不是new出来的std::fopen返回FILE*、socket()返回文件描述符、数据库连接是句柄它们的释放函数是fclose/close/ 各自的 disconnect。直接塞进裸unique_ptr用默认删除器去delete不是编译错而是运行时把错误的释放函数套在错误的资源上。这篇讲怎么用自定义删除器custom deleter把任何资源都装进 RAII并实测它对sizeof的影响。1. 引子默认删除器管不了的资源看这段意图把FILE*交给unique_ptr的代码// 片段无 main展示问题#includememorystd::unique_ptrFILEf(std::fopen(a.txt,r));// 反例不要这么写// 析构时会调用 delete 去释放一个 FILE* —— 类型不匹配行为未定义unique_ptrT默认用std::default_deleteT里面是delete ptr。把FILE*交给它析构时拿delete去释放一个FILE*类型与释放方式全错。正确做法是给第二个模板参数传一个自定义删除器一个「知道怎么正确释放这个资源」的可调用对象。官方文档std::unique_ptr 删除器 — cppreference · default_delete — cppreference2. 三种删除器写法删除器的核心要求只有一个签名像void operator()(T*)的可调用对象。常见三种写法写法类型是否携带状态适合场景函数指针void(*)(T*)否但类型是「值」释放逻辑就是现成函数无状态 lambda / 空仿函数闭包类型 / 空 struct否大多数场景零开销std::functionvoid(T*)类型擦除包装可携带状态需要运行时换删除器有开销需要注意「删除器签名」里传进来的是指针void operator()(T*)中的T*就是unique_ptr的第一个模板参数。对FILE*而言T是std::FILE删除器收的是std::FILE*如果你的资源是个句柄而非指针比如 POSIX 的int文件描述符、Windows 的HANDLE就先想清楚「用什么类型当unique_ptr的第一个参数」常见做法是包一层薄薄的句柄类或者干脆用unique_ptrint, Closer这种「拿指向句柄的指针当句柄」的技巧但后者可读性差不推荐。下面是 C 里最常见的几类「非new资源」以及它们真正的释放函数先记住这张对照表选删除器时就不会拿错资源获取函数释放函数unique_ptr的T写什么FILE*std::fopenstd::fclosestd::FILEPOSIX 文件描述符openclose建议自己包一个Fd句柄类socketsocketclose/closesocket同上句柄类更清晰目录流opendirclosedirDIR动态库句柄dlopendlclosevoidunique_ptrvoid, Dsqlite3*sqlite3_opensqlite3_closesqlite3自定义 C API 句柄xxx_createxxx_destroy该句柄的结构体名规律很简单凡是「成对出现」的 create/destroy、open/close、alloc/free 函数就天然适合用自定义删除器包成 RAII。C Core Guidelines 的 C.149「用unique_ptr/shared_ptr代替裸的new/delete」讲的是同一个思想把「谁负责释放」写进类型里而不是写在文档和注释里靠人记。下面先用无状态 lambda把FILE*完整包成一个 RAII 对象真跑一遍写文件再读回来// deleter_file.cpp — 编译: g -stdc17 -Wall -O2 deleter_file.cpp -o df#includecstdio#includememoryintmain(){constchar*pathmiao_demo_file.txt;// 无状态 lambda 作为删除器离开作用域自动 fcloseautocloser[](std::FILE*f)noexcept{if(f)std::fclose(f);std::printf([deleter] fclose 已调用\n);};{// 写std::unique_ptrstd::FILE,decltype(closer)file(std::fopen(path,w),closer);if(!file){std::printf(打开文件失败\n);return1;}std::fprintf(file.get(),hello miao\n);}// 作用域结束 - lambda 删除器自动 fclose// 读验证文件被正确写入且被正确关闭std::unique_ptrstd::FILE,decltype(closer)in(std::fopen(path,r),closer);charbuf[64]{0};while(std::fgets(buf,sizeof(buf),in.get())){std::printf(读到: %s,buf);}std::printf(unique_ptrFILE, lambda 的 sizeof %zu\n,sizeof(std::unique_ptrstd::FILE,decltype(closer)));std::printf(裸指针 FILE* 的 sizeof %zu\n,sizeof(std::FILE*));std::remove(path);// 用完删掉临时文件std::printf(临时文件已删除\n);}[deleter] fclose 已调用 读到: hello miao unique_ptrFILE, lambda 的 sizeof 8 裸指针 FILE* 的 sizeof 8 临时文件已删除 [deleter] fclose 已调用fopen句柄全程没有手写fclose第一个unique_ptr离开内层作用域时删除器自动收尾第一个[deleter]打印第二个读完也自动收尾第二个[deleter]打印。关键看最后两行无状态 lambda 删除器下unique_ptrFILE的sizeof仍是 8和裸指针一样大。3. 关键性能点删除器类型决定 unique_ptr 的大小unique_ptrT, D对象内部嵌了一个D类型的删除器成员。不同D占的空间天差地别下面实测三种 std::function// deleter_sizeof.cpp — 编译: g -stdc17 -Wall -O2 deleter_sizeof.cpp -o ds#includecstdio#includememory#includefunctionalstructStatelessDeleter{// 空仿函数无状态voidoperator()(int*p)constnoexcept{deletep;}};voidfn_deleter(int*p)noexcept{deletep;}// 普通函数intmain(){usingP0std::unique_ptrint;// 默认删除器无状态usingP1std::unique_ptrint,StatelessDeleter;// 无状态自定义删除器usingP2std::unique_ptrint,void(*)(int*);// 函数指针删除器携带值usingP3std::unique_ptrint,std::functionvoid(int*);// std::function 删除器std::printf(默认删除器 sizeof %zu\n,sizeof(P0));std::printf(无状态自定义删除器 sizeof %zu\n,sizeof(P1));std::printf(函数指针删除器 sizeof %zu\n,sizeof(P2));std::printf(std::function 删除器 sizeof %zu\n,sizeof(P3));std::printf(裸指针 int* sizeof %zu\n,sizeof(int*));}默认删除器 sizeof 8 无状态自定义删除器 sizeof 8 函数指针删除器 sizeof 16 std::function 删除器 sizeof 40 裸指针 int* sizeof 8为什么P1无状态还是 8而P2函数指针变成 16这就是空基类优化EBOEmpty Base Optimization当删除器D是无状态类型默认default_delete、空仿函数、无捕获 lambda时它不占任何数据编译器把它「压没」unique_ptr的大小就等于一个裸指针。而函数指针本身是个有值的对象必须真真实实存一个机器字于是sizeof变成「被管理指针 函数指针」 16。更糟的是std::function它是类型擦除的大对象本机 40 字节还带来一次间接调用除非必须运行时更换删除器否则不要用来当删除器类型。对比表64 位平台1 机器字 8 字节形式删除器存储sizeof相对裸指针unique_ptrint默认删除器无状态EBO 吃掉的8相等零开销无状态自定义删除器EBO 吃掉的8相等零开销函数指针删除器内嵌一个指针成员16多 1 个机器字std::function删除器类型擦除大对象40多 4 个机器字 间接调用裸指针int*—8基准官方文档std::unique_ptr 布局与sizeof— cppreference · Compiler Explorer 可对比默认删除器与函数指针删除器生成的汇编验证 EBO 是否真的把删除器优化没了。内存布局画成图差异unique_ptr 内部布局64 位被管理指针 p 在前 无状态删除器默认 / lambda / 空仿函数 函数指针删除器 ┌──────────────┐ ┌──────────────┬──────────────┐ │ T* p │ 删除器被 EBO 吃掉了 │ T* p │ void(*)(T*) d │ │ (8 字节) │ → 整个对象只有 8 字节 │ (8 字节) │ (8 字节) │ └──────────────┘ └──────────────┴──────────────┘ sizeof 8 sizeof 164. shared_ptr 的删除器存进控制块不影响大小shared_ptr的删除器是在构造时传入的且类型被擦除后存进共享的控制块control block所以删除器类型根本不出现在shared_ptrT的类型里也就不影响它的大小// deleter_shared.cpp — 编译: g -stdc17 -Wall -O2 deleter_shared.cpp -o dsh#includecstdio#includememoryintmain(){// 删除器在构造时传入类型被擦除进控制块shared_ptrint 的类型里看不到它autospstd::shared_ptrint(newint(7),[](int*p){std::printf([shared deleter] 释放 %d\n,*p);deletep;});std::printf(sp 拥有 %d, use_count%ld\n,*sp,sp.use_count());autosp2sp;// 拷贝引用计数 1删除器随控制块一起共享std::printf(拷贝后 use_count%ld\n,sp2.use_count());std::printf(sizeof(shared_ptrint) %zu\n,sizeof(std::shared_ptrint));std::printf(sizeof(shared_ptrdouble) %zu\n,sizeof(std::shared_ptrdouble));std::printf(裸指针 int* %zu\n,sizeof(int*));}sp 拥有 7, use_count1 拷贝后 use_count2 sizeof(shared_ptrint) 16 sizeof(shared_ptrdouble) 16 裸指针 int* 8 [shared deleter] 释放 7注意两个要点①shared_ptrT永远是「两个指针」的大小被管理指针 控制块指针 16与删除器是什么毫无关系因为删除器在控制块里② 删除器只在最后一个引用释放时才真正执行所以[shared deleter]在最末尾打印且只打印一次。shared_ptr 内部布局 shared_ptr 对象(线程 A) 控制块(所有 shared_ptr 共享) ┌──────────┬──────────┐ ┌────────────┬────────────┬──────────┐ │ T* p │ ctrl* │ ---- │ 引用计数 │ 弱引用计数 │ 删除器 D │ └──────────┴──────────┘ └────────────┴────────────┴──────────┘ shared_ptr 对象(线程 B) │ ▲ 删除器随控制块共享类型被擦除 ┌──────────┬──────────┘ │ 不影响 shared_ptr 自身 sizeof │ T* p │ ctrl* ────────────────┘ └──────────┴──────────┘官方文档std::shared_ptr 控制块与删除器 — cppreference · C Core Guidelines · R.20 用 unique_ptr 表达独占、shared_ptr 表达共享所有权。5. 完整示例统一封装任意资源把上面要点串起来一个函数创建FILE*、用无状态 lambda 删除器交出去、调用方完全不用管关闭// deleter_full.cpp — 编译: g -stdc17 -Wall -O2 deleter_full.cpp -o dfl#includecstdio#includememory// 工厂返回已绑定 fclose 删除器的 unique_ptr调用方零心智负担autoopen_file(constchar*path){autocloser[](std::FILE*f)noexcept{if(f)std::fclose(f);};returnstd::unique_ptrstd::FILE,decltype(closer)(std::fopen(path,w),closer);}intmain(){autofopen_file(miao_full_demo.txt);if(f)std::fprintf(f.get(),managed by custom deleter\n);std::printf(文件句柄类型大小 %zu 字节与裸指针相同零开销\n,sizeof(f));std::remove(miao_full_demo.txt);}文件句柄类型大小 8 字节与裸指针相同零开销6. 延伸阅读std::unique_ptr — cppreference —— 删除器模板参数与特化全貌std::shared_ptr — cppreference —— 控制块、删除器与use_count细节C Core Guidelines · R.20–R.22 —— 何时用 unique_ptr / shared_ptrCompiler Explorer —— 对比 EBO 前后unique_ptr生成的汇编本知识库内的相关篇目《unique_ptr 完全指南独占所有权与零开销》 —— 讲透 std::unique_ptr 的独占所有权语义、make_unique/get/release/《make_unique / make_shared vs 裸 new三个理由与反直觉权衡》 —— 讲透为什么优先 std::make_unique / std::make_shared 而非裸 new《shared_ptr 完全指南引用计数、控制块与开销》 —— 讲透 std::shared_ptr 的共享所有权语义、控制块control block里究竟存了什7. 一句话总结非new资源用自定义删除器装进 RAII删除器就是个void operator()(T*)可调用对象无状态 lambda / 空仿函数被 EBO 吃掉unique_ptr仍是 1 个指针零开销函数指针删除器多 1 个机器字std::function删除器更大且多一次间接调用、不推荐shared_ptr的删除器在构造时传入、存进控制块、不影响其固定 16 字节大小。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询