为什么要设置删除器

C++11 加入STL的 shared_ptr 和 unique_ptr,已经是我们编码的常客了。用的多自然就会了解到它们的删除器,比如很多C语言库(GDAL, GLFW, libcurl等等)创建的指针不能简单的使用 delete 释放,当我们想使用智能指针管理这些库创建的资源时,必须设置删除器:

代码
//使用重载了operator()的类作为删除器
struct CurlCleaner
{
void operator()(CURL *ptr) const
{
curl_easy_cleanup(ptr);
}
};
std::unique_ptr<CURL, CurlCleaner> curlu(curl_easy_init(), CurlCleaner{});//第二个参数可省略,因为CurlCleaner可默认构造
std::shared_ptr<CURL> curls(curl_easy_init(), CurlCleaner{}); //使用函数指针作为删除器
void GLFWClean(GLFWwindow *wnd)
{
glfwDestroyWindow(wnd);
}
std::unique_ptr<GLFWwindow, decltype(&GLFWClean)> glfwu(glfwCreateWindow(/*省略*/), GLFWClean);//第二个参数必须传入实际调用的函数地址
std::shared_ptr<GLFWwindow> glfws(glfwCreateWindow(/*省略*/), GLFWClean);
//上述两个构造函数中的第二个参数都进行了函数名到函数指针的隐式转换 //使用lambda作为删除器
auto GDALClean=[](GDALDataset *dataset){ GDALClose(dataset); };
std::unique_ptr<GDALDataset, decltype(GDALClean)> gdalu(GDALOpen(/*省略*/), GDALClean);//lambda无法默认构造,必须传入一个实例
std::shared_ptr<GDALDataset> gdals(GDALOpen(/*省略*/), GDALClean);

上面是三种最常使用的自定义删除器形式,也可以利用 std::function 的强大适配能力来包装可调用对象作为删除器,此处不展开。

标准库提供的默认删除器

内置类型和析构函数为 public 的类类型,无需指定删除器,智能指针会在引用计数归零时自动调用 delete 对管理的指针进行释放,使得语法相对简洁:

代码
std::unique_ptr<int> pi(new int(42));
std::shared_ptr<float> pf(new float(0.0f));
std::unique_ptr<std::vector<int>> pveci(new std::vector<int>());
std::unique_ptr<std::list<int>> plsti(new std::list<int>());

很长一段时间内,我以为智能指针只有 delete 一个默认的删除器,所以每次在管理 new[] 得到的指针时,都会为它编写调用 delete[] 的删除器,直到翻看智能指针的源码,发现它们的默认删除器其实有一个针对数组形式指针的特化版本:

代码
template<class _Tp>
struct default_delete//默认删除器主模板
{
...
void operator()(_Tp *_Ptr) const noexcept
{
...
delete _Ptr;//使用delete释放指针
}
...
}
template<class _Tp>
struct default_delete<_Tp[]>//针对数组形式的特化版本
{
...
void operator()(_Tp *_Ptr) const noexcept
{
...
delete[] _Ptr;//使用delete[]释放指针
}
...
} //unique_ptr
template<class _Tp, class _Dp = default_delete<_Tp>/*默认删除器*/>
class unique_ptr{...}; //shared_ptr
template <class, class _Yp>//辅助类主模板,普通指针应用该版本
struct __shared_ptr_default_delete : default_delete<_Yp> {}; template <class _Yp, class _Un, size_t _Sz>//数组形式特化,匹配固定长度的数组形式,如std::shared_ptr<int[10]>
struct __shared_ptr_default_delete<_Yp[_Sz], _Un> : default_delete<_Yp[]> {}; template <class _Yp, class _Un>//数组形式特化,匹配不定长度的数组形式,如std::shared_ptr<int[]>
struct __shared_ptr_default_delete<_Yp[], _Un> : default_delete<_Yp[]> {}; template<class _Tp>
class shared_ptr
{
...
template <class _Yp,/*检查_Yp指针是否可转换为_Tp指针(比如子类指针到基类指针)、_Yp类型是否可应用delete与delete[]操作*/>
explicit shared_ptr(_Yp* __p) : __ptr_(__p) {
...
typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>/*根据_Yp类型选择合适的默认删除器*/, _AllocT> _CntrlBlk;
__cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT());
...
}
...
}; //用户代码
std::unique_ptr<int[]> piu(new int[10]);//匹配int[]版本,删除器编译为使用delete[]释放指针
std::shared_ptr<int[]> pis(new int[10]);//构造函数内选择使用delete[]释放指针的删除器

以上代码节选自 llvm-mingw 的标准库,查看了一下手头上的几个版本的标准库实现,发现 unique_ptr 的实现大致类似。值得一提的是,unique_ptr 自身也有针对数组形式的特化版本 unique_ptr<_Tp[]>,由于知晓管理的是数组形式的指针,这个特化版本不提供 operator-> 访问符号,取而代之的是 operator[] 来访问数组数据。

llvm-mingw shared_ptr 默认删除器的选择是通过辅助模板类 __shared_ptr_default_delete 的特化来实现的;MSVC 版本中 shared_ptr 的构造函数则直接使用 if consexpr(虽然是 C++17 开始支持,但是发现 C++14 版本的代码中已经使用)判断实例化指针类型是否为数组形式选择相应删除器;GCC 的 shared_ptr 逻辑相对复杂一些,其 shared_ptr 继承自 __shared_ptr,而 __shared_ptr 有一个类型为 __shared_count 的成员 _M_refcount, 该类有一系列重载的构造函数,其中几个是:

代码
struct __sp_array_delete
{
template<typename _Yp>
void operator()(_Yp *__p) const
{
delete[] __p;
}
}; r1: template<typename _Ptr>/*默认使用delete版本的删除器,省略实现*/
explicit __shared_count(_Ptr __p)
r2: template<typename _Ptr>/*委托给r1*/
__shared_count(_Ptr __P, false_type) : __shared_count(__p){}
r3: template<typename _Ptr, typename _Deleter, typename _Alloc, typename = typename __not_alloc_shared_tag<_Deleter>::type>
__shared_count(_Ptr __p, _Deleter __d, _Alloc __a)/*可指定删除器、内存分配器的版本*/
r4: template<typename _Ptr>/*委托给r3*/
__shared_count(_Ptr __p, true_type) : __shared_count(__p, __sp_array_delete{}, allocator<void>()){} //__shared_ptr的接受一个指针参数的构造函数
template<typename _Yp, /*检查_Yp *是否和转换为类的实例化指针类型*/>
explicit __shared_ptr(_Yp *__p): _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type()){...}

通过代码我们大致可以推测,__shared_count 这个类是用来管理引用计数和删除器的类。

可以看到,如果 __shared_ptr 构造函数接受的指针类型为普通指针,会调用 __shared_count(__p, false_type) 将 _M_refcount 构造为使用 delete 释放指针的版本;而当它接受的指针类型为数组形式指针时,__shared_count(__p, true_type) 则会被调用,构造的 _M_refcount 存储的删除器是 __sp_array_delete 类型,这个类型使用 delete[] 释放指针。

总结

1.销毁前需要额外资源释放操作的类型,使用智能指针管理时必须设置自定义删除器

2.标准库为智能指针提供了两个默认版本的删除器,可简化智能指针的代码编写

C++ 智能指针的删除器的更多相关文章

  1. 【STL学习】智能指针之shared_ptr

    前面已经学习过auto_ptr,这里补充另外一种智能指针,比auto_ptr要更强力更通用的shared_ptr. shared_ptr 简介及使用选择  几乎所有的程序都需要某种形式的引用计数智能指 ...

  2. C++ 智能指针浅析

    C++ 智能指针浅析 为了解决 C++ 中内存管理这个老大难问题,C++ 11 中提供了三种可用的智能指针.(早期标准库中还存在一种 auto_ptr,但由于设计上的缺陷,已经被 unique_ptr ...

  3. c++ 智能指针(转)

    智能指针的使用 智能指针是在 <memory> 标头文件中的 std 命名空间中定义的. 它们对 RAII 或“获取资源即初始化”编程惯用法至关重要. 此习惯用法的主要目的是确保资源获取与 ...

  4. C++几个技巧:智能指针在消息传递中的使用,元组,及lambda删除器

    1.SendMessage/PostMessage中传递对象参数 (1)方法1:使用shared_ptr 发送端: PostMessage(MyhWnd, CWM_SOME_ERROR, 0, rei ...

  5. 智能指针shared_ptr的用法

    为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...

  6. 关于智能指针boost::shared_ptr

    boost库中的智能指针shared_ptr, 功能强大, 且开销小,故受到广大coder的欢迎. 但在实际的使用过程中,笔者也发现了一些不足. 1.定制的删除器 shared_ptr除了可以使用默认 ...

  7. C++ Primer : 第十二章 : 动态内存之shared_ptr与new的结合使用、智能指针异常

    shared_ptr和new结合使用 一个shared_ptr默认初始化为一个空指针.我们也可以使用new返回的指针来初始化一个shared_ptr: shared_ptr<double> ...

  8. [转]C++智能指针的创建

    zero 坐在餐桌前,机械的重复“夹菜 -> 咀嚼 -> 吞咽”的动作序列,脸上用无形的大字写着:我心不在焉.在他的对面坐着 Solmyr ,慢条斯理的吃着他那份午餐,维持着他一贯很有修养 ...

  9. 智能指针 shared_ptr 解析

    近期正在进行<Effective C++>的第二遍阅读,书里面多个条款涉及到了shared_ptr智能指针,介绍的太分散,学习起来麻烦.写篇blog整理一下. LinJM   @HQU s ...

  10. 智能指针剖析(下)boost::shared_ptr&其他

    1. boost::shared_ptr 前面我已经讲解了两个比较简单的智能指针,它们都有各自的优缺点.由于 boost::scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不 ...

随机推荐

  1. C#中的StreamWriter和"谁创建谁释放"原则

    C# 类库中的 StreamWriter 类在释放时会同时关闭其所依赖的基础流对象,这是为了确保所有缓冲数据都被写入基础流中,并且在不再需要 StreamWriter 对象时,基础流对象也能够被及时释 ...

  2. deepseek-llamafactory模型微调并转为gguf

    模型微调测试 基础设施配置 使用云计算平台 使用vscode进行配置 打开系统盘文件夹 llamafactory基础配置 git clone --depth 1 https://github.com/ ...

  3. AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?

    一.前言:AI编程时代的双雄争霸 2025年3月,字节跳动推出的Trae以"国内首个AI原生IDE"之名杀入战场,直指海外明星产品Cursor的软肋.这场工具革命背后,是免费与付费 ...

  4. go minio 设置访问权限

    bucket 权限 桶默认可以有三种 Access Policy 策略:public.custom.private. public:不经过任何认证可以直接访问资源 custom:自定义策略 Acces ...

  5. k8s node节点报错 dial tcp 127.0.0.1:8080: connect: connection refused

    前言 在搭建好 kubernetes 环境后,master 节点拥有 control-plane 权限,可以正常使用 kubectl. 但其他 node 节点无法使用 kubectl 命令,即使同步过 ...

  6. linux ln命令详解

    介绍 ln是linux的一个重要命令,它的功能是为某一个文件在另外一个位置建立一个同步的链接.当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在 ...

  7. Joker 智能开发平台再推重磅新品,引领开发新潮流

    自 Joker 智能开发平台的前端可视化版本惊艳亮相后,便迅速在行业内掀起热潮,其创新的理念与出色的性能,为开发者和企业打造了高效.便捷的开发新路径,备受瞩目与好评.如今,Joker 智能开发平台即将 ...

  8. 详细讲述了CPU的调度原理,本篇讲一下内存的分配过程。

    运行在ESXi主机上的虚拟机分配内存之和可以超过物理机的实际内存大小,这个技术叫做超额分配(overcommitment),即使单个虚拟机的内存分配值都可以超分.但是超分的结果就是可能会引起内存资源竞 ...

  9. 交换机批量配置生成器(SecureCRT vbs脚本)

    交换机批量配置生成器(SecureCRT vbs脚本) QQ交流群:(4817315) 一.工具介绍 本工具主要是针对简化网络工程师重复繁琐的工作而开发.工具只是将重复工作通过自己配置生成脚本代码来执 ...

  10. DevOps的工作岗位的要求

    ## 为什么需要DevOps 不是每个人都能理解可靠的版本管理和牢固的构建系统的重要性. 也不是任何人能使得软件的发布达到可靠性,可重复性和可审计的高标准.Devops的职责就是将软件的构建和发布的流 ...