正确做法是用无捕获Lambda或noexcept函数对象作删除器,如[](file* f){if(f)fclose(f);},类型参数需严格匹配,禁用std::function和拷贝,显式声明移动操作,构造时判空并避免make_unique。

std::unique_ptr自定义删除器怎么写才不泄漏
直接用 std::default_delete 会崩,因为第三方C库资源(比如 FILE*、sqlite3*、SSL_CTX*)几乎都不走 delete 或 delete[],而是调 fclose()、sqlite3_close()、SSL_CTX_free()。删错函数,资源就卡在那儿了。
正确做法是把清理函数包进一个可调用对象里,传给 std::unique_ptr 构造:
auto file = std::unique_ptr<FILE, void(*)(FILE*)>( fopen("log.txt", "w"), [](FILE* f) { if (f) fclose(f); } );
- 类型参数第二项必须和删除器签名严格匹配:返回
void,参数是裸指针类型 - 不能用
std::function做删除器类型——它带额外开销且可能抛异常,而std::unique_ptr的析构函数是noexcept - lambda 必须捕获为空(
[]),否则无法作为函数指针隐式转换;有状态的删除逻辑得改用函数对象类
封装成可复用的 RAII 类型时,别漏掉移动语义
如果多个地方都要管 sqlite3*,硬写一遍 std::unique_ptr<sqlite3 ...></sqlite3> 很容易出错。封装成新类型更安全,但默认生成的移动构造/赋值会被禁掉——除非你显式声明它们。
struct sqlite3_deleter { void operator()(sqlite3* db) const noexcept { if (db) sqlite3_close(db); } }; using sqlite3_ptr = std::unique_ptr<sqlite3, sqlite3_deleter>; <p>// ✅ 正确:移动可行,拷贝被禁 sqlite3_ptr make_db() { sqlite3* db; if (sqlite3_open("data.db", &db) == SQLITE_OK) { return sqlite3_ptr{db}; // 自动调用移动构造 } return nullptr; }
- 删除器类的
operator()必须是noexcept,否则std::unique_ptr移动构造可能被禁用 - 别试图加拷贝构造——C库资源基本不支持拷贝,强行实现只会掩盖资源误用
- 构造时传入空指针要能安全处理,比如
sqlite3_close(nullptr)是合法的,但SSL_CTX_free(nullptr)不是,得自己判空
用 make_unique + 自定义删除器?不行,得手写构造
std::make_unique 完全不支持传删除器,它只负责 new 和初始化,和删除逻辑无关。想用它,等于默认走 delete,对C资源就是灾难。
立即学习“C++免费学习笔记(深入)”;
- 所有C资源都必须用直接构造:
std::unique_ptr<t d>(raw_ptr, deleter)</t> - 如果初始化过程可能失败(比如
SSL_CTX_new()返回nullptr),先接裸指针,再判空构造,别让nullptr进入unique_ptr再触发删除器 - 别在构造参数里直接调C函数并传给
unique_ptr,容易产生“半初始化”问题:若后续步骤抛异常,裸指针已丢失,无法手动清理
删除器里调用C函数出错怎么办
C库清理函数本身也可能失败或有副作用(比如 curl_easy_cleanup() 会释放内部线程局部存储)。但 std::unique_ptr 的析构函数是 noexcept,你不能在里面 throw 异常,也不能让删除器崩溃。
- 所有删除器逻辑必须兜底:检查输入指针是否为空、忽略返回码(如
SSL_shutdown()的负值)、避免二次释放 - 日志或调试信息别往删除器里塞——析构时机不确定,
std::cout可能已被销毁 - 极少数需要反馈的场景(如强制 flush 失败),只能记录到静态变量或原子标志,不能中断析构流程
最麻烦的其实是跨模块资源:比如 DLL 加载的 C 函数地址,删除器绑定的是本模块的符号。如果资源在另一个动态库中分配,又在主程序里释放,得确保链接时符号可见性一致,否则运行时报 undefined symbol 或静默跳过。