前面我们的智能指针就是:
RAII。
像指针一样。
但是我们的智能指针的析构函数就只是 delete。
// 这里简单的看一下 shared_ptr 就知道了
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
那么如果我们想要 new[] 呢? 或者既然是 RAII 的思想,那么就应该其他的资源也可以管理,就比如说是 malloc 或者是文件文件描述符等...
所以我们还是需要定制删除器,也就是定制一个我们自己的 delete。
而定制删除器实际上也就是一个可调用对象,既可以是函数指针,也可以是仿函数,亦或者是 lambda 表达式。
我们现在看一下没有写定制删除器的智能指针,如果 new[] 会怎么办?
首先,我们前面说过 new 与 delete 搭配使用,new[] 与 delete[] 搭配使用。
void test8()
{
lxy::shared_ptr Pa(new A[3]);
}
结果:
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
析构函数:~A()
这里看到 A 的构造函数被调用的3次,说明全部都被构造好了。
但是析构函数值调用的了一次,因为 shared_ptr 默认就是 delete,所以只调用了一次析构,才会导致奔溃。
下面我们就可以写我们的定制删除器,那么我们想要该类再析构的时候调用可调用对象来析构,那么当然是我们再构造的时候,或者是模板的时候就传进入一个仿函数,或者是再构造函数的时候再传入可调用对象,但是STL 库里面是再构造函数的时候传入的,所以我们也在构造函数的时候传入。
但是,如果是我们仅仅传入的话,那么当析构的时候我们不知道去那里找这个可调用对象,但是我们可以将他保存起来。
那么我们如何保存呢?我们怎么知道传进来的是一个函数,还是仿函数,还是 lambda 表达式。
其实我们可以巧妙的利用C++11 的包装器!!
我们可以给 shared_ptr 中添加一个成员变量,也就是用来记录定制删除器的变量,那么我们可以将该变量声明为一个包装器类型的一个变量,那么我们如何声明呢?
因为我们再删除的时候,由于不知道 T* _ptr 是什么类型的,如果是 new[] 那么我们就需要用 delete[] 如果是 malloc 我们就需要 free ,如果是 FILE 那么我们就需要 close 所以我们可以将该函数传入一个 ptr 的指针,也就是需要管理的资源的指针,目的就是释放该管理的资源,而 shared_ptr 里面还有一个 int* 的计数器,该计数器不需要我们来管理,因为这个计数器他本身就是一个 new 出来的,我们只需要将其 delete 即可,而我们真正需要管理的就是我们想要让 shared)ptr 为我们管理的资源。
function _del = [](T* ptr) {delete ptr; };
这里就是该成员变量的声明,我们可以在构造函数的时候给他一个定制删除器。
下面我们就改写一下 shared_ptr 改写一些并不难。
我们只需要为 shared_ptr 类添加一个构造函数,也就是可以传入定制删除器的构造函数。
shared_ptr(T* ptr, function del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
也就是我们为该类添加一个这样的函数,但是为了方便,我买也可以直接为该构造函数添加一个模板。
template
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
我们可以写成这样。这里两种都是可以的。
那么上面这样改写就算结束了吗?
当然没有,我们先看下面的这段代码。
void test8()
{
auto del = [](A* ptr) {delete[] ptr; };
lxy::shared_ptr Pa(new A[3], del);
lxy::shared_ptr Pa1(Pa);
}
我们先看这样会不会报错。
结果:
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
析构函数:~A()
析构函数:~A()
析构函数:~A()
这样我们是没有问题的,即使我们拷贝了也没有问题,但是这样真的就没有问题吗?
我们拷贝后,拷贝的对象里面的 _del 成员变量是什么,是默认的 delete 所以我们还是需要为拷贝和赋值都添加一个。
这里为了验证我们所说的是正确的,我们可以看一下拷贝后失败的案例,但是上面为什么拷贝后还是成功的呢?
因为先创建的后析构,所以这里是因为 Pa1 先析构,但是 Pa1 里面的计数器减减后并没有到0,所以没有析构,而有计数器的那个对象是后析构的,所以他析构的时候,shared_ptr里面的 T* _ptr 析构会调用我们写的定制删除器,所以没有办法,那么我们要怎么让拷贝的对象会是最后一次析构呢?我们可以把一个对象赋值给Pa 这样 Pa 所管理的资源的引用计数就会减减,而这份资源的最后一次管理权也就会交到 Pa1 手中,所以只需要等Pa1 析构的时候就是这份资源释放的时候,而这份资源是 new[] 出来的,而 Pa1 里面的定制删除器是,默认的也就是简单的 delete 所以此时析构的话就会报错。
void test8()
{
auto del = [](A* ptr) {delete[] ptr; };
lxy::shared_ptr Pa(new A[3], del);
lxy::shared_ptr Pa1(Pa);
lxy::shared_ptr Pa2(new A[5], del);
Pa = Pa2;
}
这样 Pa2 给 Pa1 此时 Pa1 手中的那一份资源的管理全就会减减,然后只剩下 Pa1 手中的一份。
所以再 Pa1 析构的时候就会报错。
结果:
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
构造函数:A(int a = 0)
析构函数:~A()
这里我们看到了 只析构了一次。
那么为什么赋值也要给 del 也赋值呢?
因为再赋值的时候,如果一个new[] 需要用 delete[] 释放,而一个是 new 只需要 delete 就可以释放,所以如果不将定制删除其也赋值的话,那么就会出现问题。
上面这个的报错我们就不演示了。
其实有了定制删除器后,我们可以染发 shared_ptr 管理其他的资源,就不光光管理指针的,真正的实现了 RAII 的思想。
我们将打开的文件指针交给 shared_ptr 进行管理:
void test9()
{
auto del1 = [](FILE* fd) {fclose(fd); };
lxy::shared_ptr Pf(fopen("test.txt", "w"), del1);
fprintf(Pf.getPtr(), "%s", "hello world");
}
这里为了支持我们的写入,我们需要提供一个 getPtr 因为我们写入的时候,需要文件指针,而我们需要提供getPtr 或者是将 _ptr 放成公有,才可以,这里选择提供getPtr。
结果:
hello world
这就是文件里面写入的内容。