【C++高阶】:自定义删除器的全面探索

island1314 2024-08-11 16:35:01 阅读 64

✨                                          我凌于山壑万里,一生自由随风起    🌏

📃个人主页:island1314

🔥个人专栏:C++学习

🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


目录

🚀前言

1. 删除器的基本概念

1.1 默认删除器

1.2 自定义删除器(定制删除器)

1.3 为什么需要自定义删除器

1.2.1 管理非堆内存资源

1.2.2 代码可读性和维护性

1.2.3 异常安全

1.4 自定义删除器的使用示例

2、自定义删除器的设计

2.1 函数对象(Functor)作为删除器

2.1.1 什么是函数对象

2.1.2 如何使用函数对象作为自定义删除器

2.2 Lambda表达式作为删除器

2.2.1 Lambda表达式的基础

2.2.2 如何使用Lambda表达式作为自定义删除器

4.3 与std::function结合

3 自定义删除器的需求场景

1.3.1 非堆内存资源

1.3.2 第三方库


🚀前言

这篇文章主要是对之前智能指针的一个小小的补充,没有看过智能指针的读者朋友们,可以参考下下面这篇博客

【C++高阶】:智能指针的全面解析-CSDN博客

1. 删除器的基本概念

在C++中,智能指针(Smart Pointers)如std::unique_ptr和std::shared_ptr默认使用delete或delete[]来释放内存。但有时,这种默认行为可能不适用于所有场景。这就是自定义删除器(Custom Deleters)进入游戏的地方。

比如:std::share_ptr 在实例化对象时,有两个参数:

<code>template <class U, class D>

shared_ptr (U* p, D del);

其中:

p:需要让智能指针管理的资源。del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

实际上,删除器就是一个被工具封装的动作,这个动作就是用特定的方式释放资源。

总的来说,当智能指针管理的资源不是通过new出来的时候,就需要用对象类型和定制删除器构造智能指针。

1.1 默认删除器

默认情况下,std::unique_ptr和std::shared_ptr使用以下方式进行删除:

delete ptr;

delete[] arr_ptr;

这些删除器在大多数情况下都很有用,但有时我们需要更多的灵活性。

1.2 自定义删除器(定制删除器)

实际上,不是所有的对象都是new出来的,也可能是new[],因此释放对象的资源也可能是delete[]。例如:

当你想在释放对象时执行一些额外的操作,例如关闭文件、释放资源、记录日志等。当你想使用一个不同于delete的函数来销毁对象,例如free、fclose、Release等。当你想管理一个不是通过new分配的对象,例如一个栈上的对象或一个全局变量。当你想管理一个不是单个对象而是一个数组或容器的对象。定制删除器可以让你更灵活地控制shared_ptr如何管理和释放它所指向的对象。

定制删除器可以让你更灵活地控制shared_ptr如何管理和释放它所指向的对象。

1.3 为什么需要自定义删除器

1.2.1 管理非堆内存资源

除了内存,智能指针还可以用于管理其他类型的资源,例如文件句柄、互斥锁或数据库连接。这些资源可能需要特定的释放机制。

1.2.2 代码可读性和维护性

使用自定义删除器可以提高代码的可读性和维护性。它使资源的获取和释放逻辑紧密地绑定在一起,从而减少了出错的机会。

1.2.3 异常安全

自定义删除器有助于实现异常安全(Exception Safety)。当构造函数可能抛出异常时,使用智能指针和自定义删除器可以确保资源被正确释放。

毕竟即使是最简单的代码也可能隐藏复杂性和潜在的错误。而自定义删除器提供了一种机制,可以在复杂的错误处理逻辑中保持清晰和简洁。

1.4 自定义删除器的使用示例

我们要想管理一个打开的文件,但是你不能使用delete来关闭它,而是使用fclose:

#include <iostream>

#include <memory>

#include <cstdio>

int main()

{

// 创建一个shared_ptr,管理一个打开的文件

// 使用fclose作为定制删除器

std::shared_ptr<FILE> file(fopen("test.txt", "w"), fclose);

// 写入一些内容到文件

fputs("Hello world", test.get());

// 当test离开作用域时,会调用fclose来关闭文件

}

这样就可以避免使用delete来释放一个不是通过new分配的对象,从而导致危险行为。

或者我们可以自己写一个Fclose的模板来使用,如下:

class Fclose {

public:

void operator()(FILE* ptr) {

cout << "fclose:" << ptr << endl;

fclose(ptr);

}

};

int main()

{

//使用自定义的fclose模板

std::unique_ptr<FILE, Fclose> up(fopen("test.txt", "r"));

std::shared_ptr<FILE> sp(fopen("test.txt", "r"), Fclose());

}

方法 适用场景 优点 缺点
默认删除器 堆内存 简单、高效 不够灵活
函数对象(Functor) 需要状态的复杂资源管理 灵活、可维护 可能增加内存开销
Lambda表达式 简单的自定义逻辑 简洁、现代 不能携带状态
    std::function 需要多态删除器 高度灵活 性能和内存开销

2、自定义删除器的设计

2.1 函数对象(Functor)作为删除器

🎈在C++中,函数对象(Functor)是一种非常灵活的机制,它允许我们将行为(behavior)封装为对象。这在设计自定义删除器时非常有用。

2.1.1 什么是函数对象

🎈函数对象是重载了<code>operator()的类或结构体。这意味着你可以像调用函数一样使用这些对象。

struct MyDeleter {

void operator()(int* ptr) {

delete ptr;

}

};

2.1.2 如何使用函数对象作为自定义删除器

🎈使用std::unique_ptr(唯一指针)或std::shared_ptr(共享指针)时,你可以将函数对象作为第二个模板参数传递。

std::unique_ptr<int, MyDeleter> p(new int, MyDeleter());

这种方式的优点是类型安全和高效。因为删除器是类型的一部分,编译器可以在编译时进行优化。

2.2 Lambda表达式作为删除器

🎈Lambda表达式(Lambda Expression)在C++11后成为了语言的一部分,它提供了一种更简洁、更直观的方式来定义简单的函数对象

2.2.1 Lambda表达式的基础

🎈Lambda表达式基本上是一个匿名函数。你可以这样使用它:

auto deleter = [](int* ptr) { delete ptr; };

2.2.2 如何使用Lambda表达式作为自定义删除器

🎈与函数对象类似,Lambda表达式可以直接作为std::unique_ptr或std::shared_ptr的删除器。

std::unique_ptr<int, decltype(deleter)> p(new int, deleter);

这种方式的优点是简洁和直观。你不需要定义一个完整的结构体或类,只需要一个简单的Lambda表达式。

4.3 与std::function结合

 

3 自定义删除器的需求场景

🌈虽然标准库提供的智能指针非常强大,但有时候它们还是不能满足所有需求。例如,当你需要管理的不仅仅是内存,可能是一个文件句柄(File Handle)或者数据库连接(Database Connection)时,标准的删除器就显得力不从心。

比如:你正在与一个老旧的C库交互,该库要求使用特定的函数来释放内存,例如。在这种情况下,使用默认的delete将不适用。

1.3.1 非堆内存资源

🌈在许多情况下,你可能需要管理的资源并不是通过 newdelete 分配的堆内存。这些资源可能是操作系统级别的,比如文件句柄或线程。这时,你需要一个更加灵活的删除器。

1.3.2 第三方库

🌈当你的代码需要与第三方库集成时,这些库可能有自己的资源管理机制。在这种情况下,使用自定义删除器可以让你的智能指针与第三方库的资源管理无缝对接。

“We cannot solve our problems with the same thinking we used when we created them.”

- Albert Einstein

这句话在这里意味着,当面对新的问题时,我们需要新的解决方案。自定义删除器就是这样一种解决方案,它让智能指针更加灵活,能适应更多的场景。

4. share_ptr的模拟实现+删除器

【C++高阶】:智能指针的全面解析-CSDN博客

我们之前在这篇文章已经实现了对share_ptr的基本实现,现在我们来给其加上定制删除器。

namespace qian {

template<class T>

class shared_ptr {

public:

shared_ptr(T* ptr = nullptr)

: _ptr(ptr)

,_pcount(new int(1))

{}

//定制删除器

template<class D> //由于这个D是给函数模板用的,而不是给整个类用的

shared_ptr(T* ptr, D del)

: _ptr(ptr)

, _pcount(new int(1))

, _del(del)

{}

shared_ptr(const shared_ptr<T>& sp)

:_ptr(sp._ptr)

, _pcount(sp._pcount)

{

++(*_pcount);

}

void release()

{

if (--(*_pcount) == 0)

{

//delete _ptr; //不能写delete,因为不一定都是new创建出来的

_del(_ptr);

delete _pcount;

_ptr = nullptr;

_pcount = nullptr;

}

}

// sp1 = sp3

// sp3 = sp3

shared_ptr<T>& operator=(const shared_ptr<T>& sp)

{

//虽然这样显示地写可以避免内存泄露,但是不建议这样用

//this->~shared_ptr();

if (_ptr != sp._ptr) //避免自己给自己赋值

{

release();

//下面这样写可能会存在会内存泄露,若不调用上面地析构的话

_ptr = sp._ptr;

_pcount = sp._pcount;

++(*_pcount);

}

return *this;

}

~shared_ptr()

{

/*if (--(*_pcount) == 0)

{

delete _ptr;

delete _pcount;

_ptr = nullptr;

_pcount = nullptr;

}*/

//换成这样写更时候显示调用析构

release();

}

T* get()

{

return _ptr;

}

int use_count()

{

return *_pcount;

}

T& operator*()

{

return *_ptr;

}

T* operator->()

{

return _ptr;

}

private:

T* _ptr;

int* _pcount; //引用计数

//D _del; //因此用不了这个D,这里是可调用对象,那我们就使用包装器来解决

function<void(T* ptr)> _del = [](T* ptr) {delete ptr; }; //用缺省值来初始化

};

}


📖总结

以上就是定制删除器的全部内容啦,感谢大家观看!

💞 💞 💞那么本篇到此就结束,希望我的这篇博客可以给你提供有益的参考和启示,感谢大家支持!!!祝大家天天开心。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。