news 2026/10/22 13:41:20

详解C++ 智能指针的删除器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
详解C++ 智能指针的删除器

前言


智能指针的默认行为是“用delete销毁所指对象”。但现实中的资源不只有new出来的对象:std::fopen得到的文件指针要用std::fclose关闭,std::malloc得到的内存要用std::free释放,数组要用delete[],系统句柄要用对应的关闭函数。把这类指针直接塞进std::unique_ptr<T>或std::shared_ptr<T>,析构时调用的是delete,分配方式与释放方式不匹配,属于未定义行为(undefined behavior, UB)。


删除器(deleter)就是用来替换默认销毁动作的可调用对象。它让智能指针从“只管理 new 出来的对象”扩展为“管理任意需要确定性释放的资源”,这是 RAII 真正落地的地方。


围绕删除器有两个常见误解:一是认为unique_ptr和shared_ptr的删除器写法一样;二是认为std::make_unique、std::make_shared可以顺带指定删除器。前者错在删除器在两种智能指针里的地位完全不同,后者错在这两个工厂函数根本不接受删除器参数。本文把这两种智能指针的删除器分开讲清,并给出文件、内存缓冲区、数组等场景的完整写法。代码以 C++17 为基准,不声称经过编译或运行。


一、默认删除器与它的问题


std::unique_ptr<T>的默认删除器是std::default_delete<T>,它的operator()对指针执行delete。对数组类型,标准库提供了偏特化std::default_delete<T[]>,执行的是delete[]:


// 适用于 C++17
#include <memory>

struct Widget {
~Widget() = default;
};

void demo() {
std::unique_ptr<Widget> one(new Widget);
std::unique_ptr<Widget[]> many(new Widget[4]); // 使用 default_delete<Widget[]>
}

std::shared_ptr的情况不同:删除器不是模板参数,而是构造时传入、存放在控制块里的可调用对象。默认构造(例如std::shared_ptr<Widget> p(new Widget))同样使用delete,对数组则要用std::shared_ptr<Widget[]>(C++17 起支持数组特化,默认销毁动作是delete[])。


默认删除器只适用于“由new或new[]分配、由delete或delete[]释放”的对象。以下场景都必须自定义删除器:




资源来源正确释放方式默认删除器会做什么



std::fopenstd::fclose对FILE*执行delete,UB

std::mallocstd::free对指针执行delete,UB

new[]得到的数组delete[]对单个对象执行delete,UB

平台句柄(文件描述符、套接字等)对应的 close 函数对句柄值执行delete,UB

需要额外清理的对象自定义清理逻辑只调用析构,遗漏额外步骤



二、unique_ptr 的删除器:类型的一部分


std::unique_ptr的完整模板形式是std::unique_ptr<T, D>,D就是删除器类型。删除器是类型的一部分,两个unique_ptr只要删除器类型不同,就是完全不同的类型,不能互相拷贝赋值。


函数指针作为删除器


// 适用于 C++17
#include <cstdio>
#include <memory>

using FilePtr = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;

int main() {
FilePtr fp(std::fopen("data.bin", "rb"), &std::fclose);
if (!fp) {
return 1;
}
// 离开作用域时调用 std::fclose(fp.get())
return 0;
}

这里D是int (*)(std::FILE*),构造时必须同时传入指针和删除器。注意不能只写FilePtr fp(std::fopen(...));,因为函数指针类型没有默认值,默认构造出来是空指针,析构时调用空函数指针会崩溃(具体行为是 UB)。即使删除器类型可以默认构造,也建议显式传入,避免出现空删除器。


函数对象作为删除器


函数对象的可读性更好,也更容易携带状态:


// 适用于 C++17
#include <cstdio>
#include <memory>

struct FileCloser {
void operator()(std::FILE* fp) const noexcept {
if (fp != nullptr) {
std::fclose(fp);
}
}
};

using FilePtr = std::unique_ptr<std::FILE, FileCloser>;

int main() {
FilePtr fp(std::fopen("data.bin", "rb"));
if (!fp) {
return 1;
}
return 0;
}

FileCloser是无状态类型,常见实现会利用空基类优化,使unique_ptr的大小与裸指针相同。但这是实现层面的优化,不要把它当成可以依赖的 ABI 保证;如果删除器有状态,unique_ptr一定会多存一份状态。


lambda 作为删除器


lambda 也能做删除器,但类型要借助decltype:


// 适用于 C++17
#include <cstdio>
#include <memory>

int main() {
auto closer = [](std::FILE* fp) {
if (fp != nullptr) {
std::fclose(fp);
}
};

std::unique_ptr<std::FILE, decltype(closer)> fp(std::fopen("data.bin", "rb"), closer);
return fp ? 0 : 1;
}

无捕获 lambda 在 C++20 起可以默认构造,因此 C++20 下可以不传closer实例;C++17 则必须把实例传进构造函数。有捕获的 lambda 一定要保证捕获对象的生命周期覆盖智能指针,否则删除器执行时访问的是悬垂引用。


数组与自定义数组删除器


// 适用于 C++17
#include <cstdlib>
#include <memory>

struct FreeArray {
void operator()(int* p) const noexcept {
std::free(p);
}
};

int main() {
std::unique_ptr<int[]> numbers(new int[16]);
numbers[0] = 1;

int* raw = static_cast<int*>(std::malloc(sizeof(int) * 16));
if (raw == nullptr) {
return 1;
}
std::unique_ptr<int[], FreeArray> buffer(raw);
buffer[0] = 2;
return 0;
}

std::unique_ptr<int[]>的默认删除器是std::default_delete<int[]>,执行delete[];std::unique_ptr<int[], FreeArray>则把删除器换成std::free。两种写法的模板参数不同,不能混用。


unique_ptr 删除器的几个约束



  • 删除器类型必须能配合T*调用,且通常应当可移动构造;unique_ptr本身不可拷贝,但可以移动。

  • 删除器一旦确定就不能更换。reset()只接受新的指针,不接受新的删除器;想换删除器只能构造另一个类型的unique_ptr并转移指针。

  • get_deleter()可以取回删除器的指针,类型不匹配时返回空指针。

  • 删除器在unique_ptr析构、reset、赋值导致旧指针被释放时调用,调用时不应抛异常。析构函数默认是noexcept的,删除器抛出异常会导致std::terminate。

  • std::make_unique没有接受删除器的重载,自定义删除器只能直接构造unique_ptr。


三、shared_ptr 的删除器:类型擦除的控制块


std::shared_ptr<T>的模板参数只有T,删除器不是类型的一部分。它被拷贝进控制块(control block),与引用计数放在一起,通过类型擦除保存。因此:


// 适用于 C++17
#include <cstdio>
#include <memory>

struct FileCloser {
void operator()(std::FILE* fp) const noexcept {
if (fp != nullptr) {
std::fclose(fp);
}
}
};

int main() {
std::shared_ptr<std::FILE> fp(std::fopen("data.bin", "rb"), FileCloser{});
std::shared_ptr<std::FILE> copy = fp; // 共享同一个控制块
return fp ? 0 : 1;
}

fp和copy的类型完全相同,都是std::shared_ptr<std::FILE>,但控制块里保存着FileCloser。当最后一个持有者销毁时,控制块调用一次删除器,然后释放控制块自身。删除器只会执行一次,这与引用计数归零的语义一致。


shared_ptr 删除器的写法


// 适用于 C++17
#include <cstdlib>
#include <memory>

struct FreeDeleter {
void operator()(int* p) const noexcept {
std::free(p);
}
};

int main() {
int* raw = static_cast<int*>(std::malloc(sizeof(int) * 8));
if (raw == nullptr) {
return 1;
}
std::shared_ptr<int> data(raw, FreeDeleter{});
std::shared_ptr<int> alias = data;

FreeDeleter* d = data.get_deleter<FreeDeleter>();
(void)d;
return 0;
}

get_deleter<D>()返回控制块中删除器的指针;如果实际删除器不是D类型,返回空指针。这个接口常用于需要访问删除器状态的场景,例如从池中分配的对象要还回哪个池。


shared_ptr还有reset(p, d)重载,可以在重置指针的同时替换删除器:


// 适用于 C++17
std::shared_ptr<int> p;
p.reset(static_cast<int*>(std::malloc(sizeof(int))), FreeDeleter{});

但更常见的做法是一开始就构造好,避免中途更换导致的所有权混乱。


从 unique_ptr 转换


std::shared_ptr可以从std::unique_ptr构造(移动语义),并且会接管它的删除器:


// 适用于 C++17
#include <cstdio>
#include <memory>

struct FileCloser {
void operator()(std::FILE* fp) const noexcept {
if (fp != nullptr) {
std::fclose(fp);
}
}
};

int main() {
std::unique_ptr<std::FILE, FileCloser> owner(std::fopen("data.bin", "rb"));
std::shared_ptr<std::FILE> shared(std::move(owner));
// owner 变为空,shared 使用 FileCloser 释放
return shared ? 0 : 1;
}

这比“先release()再传裸指针给shared_ptr”安全得多:后者如果shared_ptr构造失败,指针就泄漏了;而且很容易忘记同时传删除器。


数组与 shared_ptr


C++17 起std::shared_ptr<T[]>是合法的,默认删除器使用delete[],并提供operator[]:


// 适用于 C++17
#include <memory>

int main() {
std::shared_ptr<int[]> values(new int[8]);
values[0] = 42;
return values[0] == 42 ? 0 : 1;
}

如果写成std::shared_ptr<int> values(new int[8]);,默认删除器会对数组指针执行delete,属于 UB。C++20 起可以用std::make_shared<int[]>(8)创建数组,但在此之前只能直接构造。


shared_ptr 删除器的约束



  • 删除器必须是可拷贝构造的,因为控制块需要保存它;unique_ptr只要求可移动构造。

  • 删除器在最后一个shared_ptr释放时调用,可能发生在任意线程;删除器自身的状态如果需要并发访问,必须自行同步。

  • 删除器不应抛出异常。控制块的销毁路径不允许异常逃逸。

  • std::make_shared不接受删除器;需要自定义删除器时只能直接构造shared_ptr。

  • 用同一个裸指针构造两个独立的shared_ptr,会生成两个控制块,最终 double free。正确做法是先构造一个,再拷贝或赋值给其他shared_ptr。


四、两种智能指针的删除器对比




维度std::unique_ptr<T, D>std::shared_ptr<T>



删除器是否属于类型是,D是模板参数否,类型擦除后存入控制块

删除器存放位置智能指针对象内部堆上的控制块

对对象大小的影响有状态删除器会增加大小不改变shared_ptr本身大小

删除器拷贝要求可移动构造可拷贝构造

能否更换删除器不能,类型固定可通过reset(p, d)更换

工厂函数能否指定make_unique不能make_shared不能

取回删除器get_deleter()get_deleter<D>()

典型开销零额外分配,可能内联控制块一次分配,删除器间接调用



选择的原则很简单:独占所有权用unique_ptr,需要共享所有权才用shared_ptr。不要因为“删除器写起来方便”而给所有对象都上shared_ptr,引用计数和控制块的代价是实打实的。


完整示例:用删除器管理文件与缓冲区


// 适用于 C++17
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <memory>

struct FileCloser {
void operator()(std::FILE* fp) const noexcept {
if (fp != nullptr) {
std::fclose(fp);
}
}
};

struct FreeDeleter {
void operator()(unsigned char* p) const noexcept {
std::free(p);
}
};

using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
using BufferPtr = std::unique_ptr<unsigned char[], FreeDeleter>;

int main() {
FilePtr input(std::fopen("input.bin", "rb"));
if (!input) {
std::cerr << "cannot open input.bin\n";
return 1;
}

constexpr std::size_t kBufferSize = 4096;
BufferPtr buffer(static_cast<unsigned char*>(std::malloc(kBufferSize)));
if (!buffer) {
std::cerr << "out of memory\n";
return 1;
}

std::size_t total = 0;
std::size_t n = 0;
while ((n = std::fread(buffer.get(), 1, kBufferSize, input.get())) > 0) {
total += n;
}

std::cout << "read " << total << " bytes\n";
return 0;
}

FilePtr在离开作用域时调用FileCloser,BufferPtr调用FreeDeleter。两条资源路径都不依赖手写goto cleanup,即使中途return或抛异常也能正确释放。注意BufferPtr的元素类型是unsigned char,删除器接收的也是unsigned char*,与std::fread需要的void*兼容。


常见坑点



  1. ❌std::unique_ptr<std::FILE> fp(std::fopen("a.bin", "rb"));,析构时对FILE*执行delete。


✅ 提供调用std::fclose的删除器,并用std::unique_ptr<std::FILE, FileCloser>。



  1. ❌std::shared_ptr<int> p(static_cast<int*>(std::malloc(64)));,用delete释放malloc的内存。


✅ 构造时传入调用std::free的删除器,或改用new/std::vector。



  1. ❌ 用同一个裸指针构造两个shared_ptr,产生两个控制块。


✅ 只构造一次,其余位置拷贝或赋值;需要从unique_ptr转移时用移动构造。



  1. ❌ 删除器里抛异常,或删除器声明为可能抛出。


✅ 删除器标记noexcept,内部消化错误;析构路径不允许异常逃逸。



  1. ❌std::unique_ptr<int> p(new int[10]);,delete与new[]不匹配。


✅ 用std::unique_ptr<int[]>或std::vector<int>;shared_ptr同理用shared_ptr<int[]>。



  1. ❌ 以为std::make_unique<T>(args...)或std::make_shared<T>(args...)能附加删除器。


✅ 需要自定义删除器时直接构造智能指针,把指针和删除器一起传入。



  1. ❌ 用有捕获的 lambda 做删除器,捕获了已经销毁的局部变量。


✅ 捕获按值,或保证捕获对象生命周期覆盖智能指针;无状态清理优先用函数对象。



  1. ❌ 给unique_ptr传一个无状态删除器后,又试图在运行时替换成另一种删除逻辑。


✅ 删除器类型在编译期固定;需要不同释放方式就定义不同的unique_ptr类型。


总结




场景推荐写法



new出来的单个对象std::unique_ptr<T>或std::make_unique<T>

new[]出来的数组std::unique_ptr<T[]>、std::vector<T>

fopen得到的文件unique_ptr<FILE, FileCloser>或shared_ptr<FILE>(fp, FileCloser{})

malloc得到的内存带free删除器的unique_ptr/shared_ptr

共享所有权std::shared_ptr<T>,删除器随构造传入

需要类型擦除的清理逻辑shared_ptr加自定义删除器



删除器的本质是把“如何释放”从类型系统的默认规则里拿出来,交给调用者显式表达。unique_ptr把删除器编进类型,换来零额外开销和编译期检查;shared_ptr把删除器藏进控制块,换来类型统一和运行期灵活。无论选哪种,都要保证分配方式与释放方式严格配对、删除器不抛异常、删除器捕获的对象活得足够久。这三条守住了,智能指针才真正比裸指针安全。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 13:41:17

通信原理中的概率论:从随机过程到误码率推导

你在学通信原理时&#xff0c;是不是也遇到过这种情况&#xff1a;前面几章讲傅里叶变换、讲信号与系统&#xff0c;还算能跟上&#xff0c;一进到随机过程、高斯白噪声、误码率推导&#xff0c;整个人就懵了。其实这不是你数学不行&#xff0c;而是缺了一层“概率论视角”。通…

作者头像 李华
网站建设 2026/10/22 13:40:42

Spark Executor内存配置原理与实战调优指南

1. 为什么Executor内存配置总像在走钢丝&#xff1f; “Spark &#xff08;一&#xff09;&#xff1a;Executor内存”——这个标题乍看平平无奇&#xff0c;像是教科书目录里被翻烂的一页。但如果你真在生产环境里调过Executor内存&#xff0c;大概率经历过这样的深夜&#xf…

作者头像 李华
网站建设 2026/10/22 13:40:38

Visio 2007流程图绘制软件安装包:32位与64位双版本选择与避坑指南

简介&#xff1a;Visio 2007 是微软推出的专业流程图与矢量绘图工具&#xff0c;面向企业用户、设计师、技术人员及需要绘制专业图表的学习者&#xff0c;可解决组织结构图、流程图、网络图、工程图、UML 模型图、数据库模型等绘制需求&#xff0c;适用于项目管理、软件开发、工…

作者头像 李华
网站建设 2026/10/22 13:39:29

AI Agent 生产落地:四个最易被忽视的工程代价解析

最近圈子里聊 AI Agent 的密度&#xff0c;几乎到了逢人必谈的程度。从 Copilot 到各种五花八门的自主智能体&#xff0c;概念和 Demo 满天飞。但有一个问题被有意无意地忽略了&#xff1a;当 Agent 从技术演示走进真实业务&#xff0c;你的基础设施到底能不能扛住&#xff1f;…

作者头像 李华
网站建设 2026/10/22 13:38:23

AI辅助论文写作全流程:4类工具组合实操指南

写论文这件事&#xff0c;放在几年前还是图书馆蹲一个月、打印店排队交稿的苦差事。现在AI工具成群结队冒出来&#xff0c;选题、查文献、写初稿、润色降重都有对应帮手&#xff0c;我的感觉是&#xff1a;只要你用得是地方&#xff0c;论文生产周期确实能缩短一半以上。我前后…

作者头像 李华
网站建设 2026/10/22 13:38:20

MPC在轮式四足机器人运动控制中的仿真实现与调参实践

简介&#xff1a;面向机器人工程与仿生技术领域&#xff0c;该仿真项目针对轮式四足机器人在复杂地形下的动态运动控制&#xff0c;适合科研人员、算法工程师及机器人专业学生。以模型预测控制&#xff08;MPC&#xff09;为核心&#xff0c;涵盖地面反作用力优化、关节力矩计算…

作者头像 李华