ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

一文带你掌握C++中智能指针如何自定义删除器 前言智能指针默认用delete释放资源。但现实中的资源远不止new出来的对象malloc出来的内存要用freefopen出来的FILE*要用fclose系统句柄Windows 的HANDLE、POSIX 的socketfd要用CloseHandle/close通过 C 接口库申请、必须交还给该库释放的内存如libcurl、sqlite3不能用delete引用计数型的手工对象需要计数减一而不是释放。这些场景都必须告诉智能指针该怎么释放这就是删除器deleter存在的意义。删除器不是一个可有可无的高级技巧它是把智能指针从只能是 new/delete 的糖升级为通用资源管理器的关键。一、删除器的本质一个可调用对象删除器的全部要求只有一条给定一个指针能被调用。也就是满足d(ptr)这个表达式合法。因此普通函数指针、函数对象functor、lambda 都能当删除器。// 形式 1自由函数 void myFree(int* p) { std::free(p); } // 形式 2函数对象 struct FileCloser { void operator()(std::FILE* fp) const { if (fp) std::fclose(fp); } }; // 形式 3lambdaC11 起最常用 auto deleter [](int* p) { std::free(p); };二、unique_ptr 的删除器编译期的一部分unique_ptr的删除器是类型的一部分也就是说templateclass T, class Deleter std::default_deleteT class unique_ptr;结论有两条非常重要不同类型删除器的unique_ptr是不同类型不能互相赋值无捕获 lambda / 无状态函数对象在类型系统里是空类empty class经空基类优化empty base optimization, EBO后不占空间sizeof与裸指针相同。auto d1 [](int* p) { delete p; }; auto d2 [](int* p) { delete p; }; // 类型不同每个 lambda 都是独立类型 std::unique_ptrint, decltype(d1) p1(new int(1), d1); std::unique_ptrint, decltype(d2) p2(new int(2), d2); // p1 std::move(p2); // ❌ 编译错误类型不兼容如果删除器有捕获例如捕获了日志句柄它就不再是空类sizeof(unique_ptr)会随之增大int counter 0; auto loggingDeleter [counter](int* p) { counter; delete p; }; std::unique_ptrint, decltype(loggingDeleter) p(new int(3), loggingDeleter); // sizeof(p) sizeof(int*) sizeof(int*) 存了捕获的引用编译期改变删除器的技巧unique_ptr可以只改删除器类型而不改指针类型用一个辅助模板就能做类型擦除式的转换templateclass T, class D std::unique_ptrT, D adopt(T* raw, D d) noexcept { return std::unique_ptrT, D(raw, std::move(d)); }三、shared_ptr 的删除器运行期的一部分这是二者最本质的区别。shared_ptr的删除器不是模板参数templateclass T class shared_ptr; // 删除器在哪里在控制块里删除器连同分配器被存在控制块中类型被抹除type-erased因此删除器不同不影响shared_ptr的类型可以放进同一个容器代价是控制块变大并且shared_ptr构造时要多一层间接调用无法内联shared_ptr没有release()不能像unique_ptr那样交还裸指针。std::shared_ptrint a(new int(1), [](int* p) { delete p; }); std::shared_ptrint b(new int(2), [](int* p) { delete p; }); std::shared_ptrint c(new int(3), [](int* p) { std::free(p); }); std::vectorstd::shared_ptrint v{a, b, c}; // ✅ 类型相同可以放一起关键make_shared 不能传删除器auto p std::make_sharedint(42, [](int*){ /*...*/ }); // ❌ 编译错误因为make_shared把对象和控制块放在同一块内存里、用一次new完成删除器需要额外的存储空间破坏了这个前提。需要自定义删除器时只能用shared_ptrT(raw, deleter)的构造形式。std::shared_ptrint p(new int(42), [](int* q) { std::cout bye\n; delete q; }); // ✅四、unique_ptr vs shared_ptr 删除器对比维度unique_ptrshared_ptr删除器位置模板参数类型的一部分控制块运行期存储是否影响类型是不同删除器 不同类型否类型恒为shared_ptrT空间开销无状态删除器为 0EBO控制块至少多一个指针调用能否内联能静态分发不能虚函数 / 函数指针是否可切换删除器类型不可除非重新构造可以通过构造传入能否与make_shared配合可以unique_ptr无 make 限制不能五、代码实战三种真实场景场景 1管理 malloc / free 的缓冲区// malloc_demo.cpp — g -stdc17 malloc_demo.cpp -o malloc_demo #include cstdlib #include cstring #include cstdio #include memory int main() { // ❌ 错误默认删除器会调用 delete而内存是 malloc 来的 - UB // std::unique_ptrchar[] bad((char*)std::malloc(64)); // ✅ 正确自定义删除器 struct FreeDeleter { void operator()(void* p) const noexcept { std::free(p); } }; std::unique_ptrchar, FreeDeleter buf((char*)std::malloc(64)); std::strcpy(buf.get(), hello allocator); std::printf(%s\n, buf.get()); // 作用域结束自动 free return 0; }场景 2管理 FILE*RAII 文件句柄// file_demo.cpp — g -stdc17 file_demo.cpp -o file_demo #include cstdio #include memory #include string #include iostream // 用 lambda 作为删除器因为要指定类型用 using 起个短名字 using FilePtr std::unique_ptrstd::FILE, int(*)(std::FILE*); FilePtr openFile(const std::string path, const char* mode) { std::FILE* fp std::fopen(path.c_str(), mode); if (!fp) return FilePtr(nullptr, std::fclose); // 注意即使为空也要给删除器 return FilePtr(fp, std::fclose); } int main() { auto fp openFile(data.txt, w); if (!fp) { std::cerr open failed\n; return 1; } std::fputs(RAII 文件写入\n, fp.get()); return 0; // 自动 fclose即使中途 return / 抛异常也不会漏 }注意FilePtr(nullptr, std::fclose)这一行unique_ptr的get() nullptr时不会调用删除器标准保证所以传nullptr给fclose的风险其实不存在但保险起见也可以写一个判空删除器。场景 3删除器有状态统计与日志// stateful_demo.cpp — g -stdc17 stateful_demo.cpp -o stateful_demo #include iostream #include memory #include string struct Logger { std::string tag; int* counter; void operator()(int* p) const { (*counter); std::cout [ tag ] 释放一个 int累计 *counter \n; delete p; } }; int main() { int freed 0; Logger lg{alloc, freed}; { std::unique_ptrint, Logger a(new int(1), lg); std::unique_ptrint, Logger b(new int(2), lg); } // a、b 析构时各调用一次删除器 std::cout 总共释放 freed 次\n; return 0; }场景 4用 shared_ptr 的删除器实现句柄池归还shared_ptr的删除器还有个强大用法它不一定要真的释放内存。可以把归还到池中写成删除器从而让shared_ptr的引用计数自动决定归还时机。// pool_demo.cpp — g -stdc17 pool_demo.cpp -o pool_demo #include iostream #include memory #include vector #include cstdlib struct Pool { std::vectorint* freeList; int* acquire() { if (!freeList.empty()) { int* p freeList.back(); freeList.pop_back(); std::cout 复用旧块\n; return p; } std::cout 新申请一块\n; return (int*)std::malloc(sizeof(int)); } void release(int* p) { std::cout 归还到池\n; freeList.push_back(p); } ~Pool() { for (int* p : freeList) std::free(p); } }; int main() { Pool pool; { std::shared_ptrint a(pool.acquire(), [pool](int* p) { pool.release(p); }); *a 42; { auto b a; // 共享计数 2 std::cout count a.use_count() \n; } // 计数回 1不归还 std::cout count a.use_count() \n; } // 计数归 0 - 归还 std::cout freeList 大小 pool.freeList.size() \n; return 0; }这段代码的妙处在于删除器捕获了pool的引用但引用计数决定何时调用。即使中途抛异常只要所有shared_ptr离开作用域归还动作一定发生。场景 5自定义分配器 自定义删除器对齐内存// aligned_demo.cpp — g -stdc17 aligned_demo.cpp -o aligned_demo #include cstdlib #include iostream #include memory #include new int main() { // 使用 posix_memalign / _aligned_malloc 这类接口分配的内存 // 必须配套对应的释放函数不能 delete void* raw nullptr; if (posix_memalign(raw, 64, 128) ! 0) { return 1; } std::shared_ptrint p((int*)raw, [](int* q) { std::cout 释放对齐内存: (uintptr_t)q % 64 \n; free(q); }); std::cout 地址 64 字节对齐: ((uintptr_t)p.get() % 64 0) \n; return 0; }编译时需要-stdc17Windows 上posix_memalign换成_aligned_malloc/_aligned_free。常见坑点坑点 1删除器类型和指针类型不匹配// ❌ 编译错误删除器签名必须是 void(T*)这里收的是 void* auto d [](void* p) { free(p); }; std::unique_ptrchar, decltype(d) p((char*)malloc(8), d);✅ 正确写法删除器参数类型必须与unique_ptr的元素类型一致。auto d [](char* p) { free(p); }; std::unique_ptrchar, decltype(d) p((char*)malloc(8), d); // ✅坑点 2有捕获 lambda 的 unique_ptr 不能拷贝甚至不能赋值int n 0; auto d [n](int* p) { n; delete p; }; std::unique_ptrint, decltype(d) a(new int(1), d); std::unique_ptrint, decltype(d) b a; // ❌ 永远编译错误lambda 的闭包类型没有默认构造函数也没有拷贝赋值C20 前无捕获 lambda 才有默认构造。想放进容器或做赋值改用函数对象struct Counter { int* n; Counter(int* p nullptr) : n(p) {} void operator()(int* p) const { if (n) (*n); delete p; } }; std::unique_ptrint, Counter a(new int(1), Counter(n)); // ✅坑点 3删除器里抛异常删除器在析构路径上被调用抛异常会导致栈展开时二次异常直接std::terminate。auto d [](int* p) { throw std::runtime_error(x); delete p; }; // ❌ 极度危险✅ 正确写法删除器必须声明为noexcept内部错误用日志记录或转换成不抛出的形式。标准库的default_delete就是noexcept的。坑点 4shared_ptr 用 make_shared 又想加删除器auto p std::make_sharedFILE(...); // ❌ make_shared 不接受删除器必须在构造时直接传入std::shared_ptrstd::FILE p(std::fopen(a.txt, r), std::fclose); // ✅坑点 5删除器生命周期短于智能指针std::unique_ptrint, decltype(myFree) make() { // ❌ 若把删除器写成局部 lambda 的引用返回后就悬空 return std::unique_ptrint, decltype(myFree)(ptr, myFree); // ✅ 函数指针最安全 }原则删除器要么是无捕获、无状态的要么是值语义持有的按值存储不要持有临时对象的引用。坑点 6数组与自定义删除器混用std::unique_ptrint[] arr(new int[10]); // ✅ 自动用 delete[] std::unique_ptrint bad(new int[10]); // ❌ 用 deleteUB std::unique_ptrint, void(*)(int*) b2(new int[10], [](int* p) { delete[] p; }); // ❌ lambda 无捕获可转函数指针但类型要写对正确写法是把删除器参数类型写成int*unique_ptrint, D的D接收int*或者直接用unique_ptrint[]。总结需求推荐做法free的内存unique_ptrT, FreeDeleter或值捕获的 lambda系统句柄 / C 库资源函数对象可复用、可默认构造shared_ptr 需要删除器构造时传第二参数放弃make_shared删除器需要状态值语义的函数对象不要引用临时量只想记录 / 归还不真正释放在删除器里做归还逻辑靠引用计数触发记住三条判据unique_ptr的删除器是类型的一部分零开销、不可混用类型shared_ptr的删除器是控制块的一部分类型无关、有额外开销删除器是析构路径上的代码必须noexcept、必须值语义。
返回列表