前言
在上一篇C++ 文章当中,对 智能指针的使用,做了很详细的介绍,对 C++11 和 C++98 库当中实现的一些常用智能指针做了很详细的介绍,但是智能指针的使用还有一些拓展用法。上篇文章链接:
C++ - 智能指针 - auto_ptr - unique_ptr - std::shared_ptr - weak_ptr-CSDN博客
智能指针定制删除器
我们再用 智能指针来管理空间的时候,对于这块空间的开辟方式有很多,比如:malloc ()函数,fopen 文件对象,new 堆上开辟,还有 new 开辟一个 数组;这些不同方式开辟的空间对于这些空间的释放方式是不一样的。
所以,我们再删除这些动态开辟的空间的时候,就不能直接一种方式来实现,要实现多种方式,使用者自己控制 释放空间的方式的话,这种实现其实在之前已经说过很多了,就是使用仿函数 ,lambda表达式 ,函数指针的方式来实现,但是,函数指针是不推荐使用的。
对于仿函数的使用可以参考,下述博客当中,红黑树的 迭代器在map 和 set 当中的使用:
C++ - 红黑树 介绍 和 实现-CSDN博客h
还有 下述对 优先级队列当中对 比较方式 的仿函数书写:
C++ - 优先级队列(priority_queue)的介绍和模拟实现 - 反向迭代器的适配器实现 - 仿函数_c++ priority_queue迭代器_chihiro1122的博客-CSDN博客
如下我们就用仿函数来实现,对 new 出来的数组空间释放:
template<class T>
struct freearray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
仿函数非常好实现,delete[] 就可以帮助我们释放数组空间。
这里不再使用 :再 shared_ptr 的函数模版参数当中传入仿函数类型,在shared_ptr 的成员函数当中创建一个 仿函数对象的方式,利用仿函数对象来调用 其中的operator()函数的方式来使用。
此时,如果是 库 当中的 shared_ptr 的话,我们就可以直接这样来控制 释放方式:
std::shared_ptr<A> st(new A[10], freearray<A>());
输出:
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
A(int a = 1)
delete:00000278A2EBA8A8
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
当然,还可以使用 lambda 表达式来实现:
std::shared_ptr<A> st2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
在 shared_ptr 内部是如何实现 上述的,其实是在 shared_ptr 类当中写了一个 构造函数的模版,这个构造函数模版可以多接收一个 对象,这个对象在外部就可以传入一个 仿函数类匿名对象,这个对象就可以帮助我们调用其中的 operator()函数。
但是,如果是传入 一个 lambda 表达式的话,这个表达式返回的是一个 lambda+uuid 为名字的 类对象,这个对象当中也重载了 operator()函数,所以,关于传入的 是仿函数 还是一个 lambda 表达式,都是可以像使用 operator() 的方式一样访问的。
但是,问题来了:operator()函数的调用,是需要一个类对象作为媒介的,也就是说,不管是 仿函数 还是 lambda 表达式,都是需要创建一个对象来进行调用的。
但是,我们外部传入的对象是在 构造函数的基础之上来进行传入的,但是 空间的释放操作是在 析构函数当中进行的,如何接收 构造函数的对象,传入 析构函数呢?
你肯定已经想到了,就是使用 成员变量,我们可以创建一个 成员变量,这个变量用于在构造函数当中接收 仿函数类对象 或者 lambda 返回的对象。
但是,这个成员变量的类型如何定义呢?我们在构造函数 当中用于接收对象的 形参的类型是 这个 构造函数 的一个 模版参数,但是在类当中定义一个 成员变量是需要具体类型或者是 类模版参数的,函数模版参数是不能用于构造 类 的成员变量的:
我们不能用构造函数当中的 D 这个函数模版参数来构造 shared_ptr 当中的成员变量。
这里,我们可以使用包装器实现。包装器,可以把 仿函数对象,lambda 表达式的返回值,函数指针,包装成一个 function 类对象,我们就可以直接使用这个类对象来调用其中的 仿函数对象的operator(),lambda 表达式, 或者是 函数指针:
private:
function<void(T*)> _del = [](T* ptr) { delete[] ptr; };
如上述所示:_del 这个成员变量就是我们想要的 ,用于接收对象的成员变量了。
这个 _del 有一个 缺省参数,是一个 lambda表达式的对象,在这个 lambda 表达式当中实现了 对于 new 数组的空间释放操作,也就是说,如果你不传入 shared_ptr 构造函数的第二个参数(释放空间的方法)的话,就默认是 使用 new 数组空间的释放方法。
完整代码(+测试例子):
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
int _a;
};
namespace My_shared_ptr
{
template<class T>
class shared_ptr
{
public:
// RAII
// 像指针一样
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
_del(_ptr);
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// sp3(sp1)
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
// sp1 = sp5
// sp6 = sp6
// sp4 = sp5
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 先判断 赋值和被赋值的两个指针是否是重复的
// 是就直接返回
if (_ptr == sp._ptr)
return *this;
// 判断当前 赋值指针在赋值出去之前
// 是否是所维护空间的唯一指针
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
// 开始赋值
_ptr = sp._ptr;
_pcount = sp._pcount;
// 引用计数++
++(*_pcount);
return *this;
}
// 返回引用计数
int use_count() const
{
return *_pcount;
}
// 拿到原生指针
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) { delete[] ptr; };
};
}
template<class T>
struct freearray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
//My_shared_ptr::shared_ptr<A> st(new A[10], freearray<A>());
std::shared_ptr<A> st2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
return 0;
}
内存泄漏问题
关于内存泄漏,在之前的博客当中已经说明得很清楚了,简单来说就是,《占着茅坑不拉shi》 (比喻有点恶心,但是非常形象)。
系统当中的某些变量,在之后的程序进行当中都不会使用了,但是,这些变量就是没有被释放,这就造成了一种内存泄漏。
这种内存泄漏问题,在我们日常编写代码的过程当中是体会不出来的,最典型的就是在服务器当中,因为所谓服务器,就是写有强大操作系统,等等比较好的硬件设施的,就可以通过计算机网络,给主机来共享数据,从而使得我们可以访问其中的数据,甚至于让我们使用到 服务器当中的操作系统。
这样的好处就在于,服务器的操作系统一般是非常牢固的,我们经常使用的服务器都是大公司维护,更加安全。
我们主机在使用的时候,就可以不用关系自己的操作系统是否牢固,直接可以通过网络和服务器连接,使用到服务器的强大操作系统。
这样来说的话,服务器的就是一直被动接受的状态,他需要时时接受每一个用户给出的相应(请求),所以服务器一般是一直开启的,关闭维修的情况都是服务器出现了重大错误,比如说:内存泄漏。
如何是一个泄漏很多空间 的 内存泄漏 是内存泄漏当中比较好的情况,因为当你服务器以 运行,就会很快的 内存泄漏,抛出你写的异常;但是如果是 一次 只是泄漏 1M ,很小的数字,那么,你只会感觉是服务器越来越卡,当有一天,突然抛出内存泄漏的异常,你是需要去查看日志的,时间过去这么久,日志该怎么查,代码量这么多,相信你已经感受到了。
具体可以看一下博客,对内存泄漏的详细介绍:
C/C++ 内存管理 new delete operator new与operator delete函数 内存泄漏-CSDN博客
像上述的 智能指针 也可以 很大帮助我们 提前预防 内存泄漏,帮助我们在最后释放空间。