目录
我们接着进行智能指针的学习和试题研究。
可能问题五:
模拟实现一下weak_ptr或者unique_ptr或者shared_ptr
问题分析:
这个问题首先就明明是三个问题,因为面试时间比较短不可能连续模拟实现三个智能指针的。所以依照重要性原则,一般会重点实现shared_ptr,由于全部实现起来比较多,所以一般面试为了控制时间也会让你实现某个部分,unique_ptr和weak_ptr也会讲的。有的同学可能会认为不会让你自我实现,只需要懂得用就可以了,但是如下图越是大厂的面试题,可能或者一定是自我实现的部分(画蓝框的)的题还是相当多的!!!
答案格式:
这个问题放在了最后面说明是最难的,确实呀实践类型的题目都挺难的,但是只要知道逻辑和库里面的实现原理再加上面试的时候不紧张其实也没有那么难的。那么我们就先从最重要的shared_ptr讲起。
shared_ptr的模拟实现
我们先看看库里面是怎么写的,做为一个类有什么成员函数和成员。
用红色框框括起来的部分就是比较重要的成员函数,也就是我们等下要实现的函数,其实我们也不需要和库里面写的完全一样,主打一个差不多就可以了,大致逻辑对就行了,因为库里面在实现shared_ptr的时候还要考虑和别的智能指针兼容的问题,我们就不用考虑这么多了,我们主打一个能通过面试官的考核就行。
我们发现shared_ptr是由很多的不同部分组成的,所以我们就分部分进行解答,正好分部分也是考点。
部分1:引用计数的设计(分考点1)
引用计数_count是设计在类里面的成员变量,但是作为一个类里面的成员变量可以有多种设计方式,可以是静态成员变量,普通的成员变量或者new开辟的在堆里面的动态成员变量,到底选择哪一种呢,我们可以做如下分析:
画图表示更直观:
使用普通的成员变量:
使用静态成员变量:
我们会发现如果使用的是普通的开辟在栈里面的成员变量或者静态的全局变量都是跟着智能指针走的,但是我们的引用计数计数的是一个空间被多少个智能指针管理着,所以这个计数是肯定要跟着被管理的空间走的,以上两种表示方法在本质上就理解错了,追寻一个空间对应一个引用计数可知这个引用计数得另开辟一个空间管理,并且一个空间才开辟一个,也就是说只要遇到需要管理新空间时才新开辟智能指针。
那既然要新开辟空间就意味着,这个引用计数本质上就是一个指针,指向一个带开辟的空间。
代码实现:
int* _count;
部分2:作为类所必须的部分(分考点2)
这里主要是实现构造函数和析构函数
如果需要调用构造函数说明遇到了一个新的空间需要管理,这时也需要对引用计数进行开辟空间
由于需要管理资源管理的同时履行帮忙销毁的任务,所以需要将指向的空间连同开辟的引用计数一起销毁了,因为当一个资源需要销毁时其引用计数一定为0了。
代码实现:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{}
void release()
{
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
_ptr = nullptr;
_count = nullptr;
}
}
~shared_ptr()
{
release();
}
为什么析构函数要另起一个函数,这个问题之后会解答,构造函数这么写其实还是和库里有所不同的,因为库里认为如果智能指针管理一个空的空间那引用计数为0,我们这边并没有做这种情况的另加考虑,而是笼统的都初始化为1了,但是不影响我们的大逻辑是对的。
部分3:拷贝构造函数(分考点3)
shared_ptr是支持拷贝的,拷贝分为两种一种是管理别人正在管理的空间,相当于构造。如下:
代码实现1:
shared_ptr(const shared_ptr<T>& sp)//不需要传成员对象因为成员里面本来就有
:_ptr(sp._ptr)
, _count(sp._count)//多个智能指针共同使用一个引用计数
{
++(*_count);//那个空间的相当于多了一个智能指针在管理了,所以跟着的引用计数要加1
}
这个还看不懂的自己反思一下!!!
还有一种就是将当前管理的空间和别的空间进行互换,相当于换一块空间管理不再管理当前空间了
代码如下:
代码实现2:
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//不可以自己析构自己,因为没有意义
{
//~shared_ptr();//虽然可以但是不支持直接调用,这就是为什么析构函数这么写的原因了
release();//在转换指向对象时,需要先释放当前的指向对象,也就是相当于要现处理当前对象的引用计数
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
return *this;
}
部分4:模拟指针的行为(分考点4)
这个之前都讲过了直接看代码实现吧。
代码实现:
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
部分5:其他重要的成员函数(分考点5)
由于库里面实现的函数有很多,但是重要的比较核心的就那么几个:
get()
这个函数的作用是得到并返回指向管理这个空间的智能指针及指向这个空间的指针,这个函数有大用的我们在weak_ptr的实现里面会讲。
T* get() const//返回指向一个已被管理的空间的指针
{
return _ptr;
}
use_count()
这个函数的作用是返回一个空间被多少个智能指针所管理,也就是返回智能指针指向的引用计数的大小。
int use_count() const
{
return *_count;//返回指向空间的引用计数的个数
}
部分6:定制删除器(分考点6)
定制删除器是shared_ptr自我实现里面比较难的部分了,如果面试官没问就不需要在模拟实现的时候直接体现出来,还有一个原因就是写了很容易错,其实部分1到部分5已经足以体现智能指针管理资源和模拟指针的行为的功能了,这个仅作为加分项。
首先定制删除器肯定要设计成类模板参数进行传递的成员变量,这样便于析构函数调用,因为外面知道定制删除器的类型有点多,且当其为lambda时类型未知,主要是uuid不知道,所以这么多的类型设计成模板参数来能够表达并兼容各自类型很有必要。所以构造函数很好写的如下:
代码实现1:
template<class D>
shared_ptr(T* ptr, D del)//缺省值要从右边往左边给,所以ptr不能给缺省值
:_ptr(ptr)
,_count(new int(1))
,_del(del)//如果没有传定制删除器就会默认使用缺省值进行构造,构造函数是这样的
{}
其实就是多加一个模板参数而已,看不懂了自己反思一下!!!
好啦你既然加了一个模板参数,且这个删除器del是设计成员变量,那对于这么多个类型难道在成员对象那里也加入模板参数,这个方法其实是可行但是,这是unique_ptr的设计理念,shared_ptr不支持将删除器弄成模板的样子就是不支持再传一个模板参数,那怎么办,要同时能处理这么多类型又要不使用模板参数,这个问题其实可以简化成如果可以用一个东西同时封装多个类型的变量就可以解决这个问题了。我们之前C++11学过的包装器function就好像有这个功能吧,对这里定义删除器变量就用的包装器进行封装的!!!
代码实现2:
T* _ptr;//智能指针的内部相当于指针,管理空间的
int* _count;//引用计数
function<void(T* _ptr)> _del = [](T* _ptr) {delete _ptr; };
//由于删除器的类型很多并且库里面不支持再传一个模板参数
//所以只能使用包装器对象,因为这样可以兼容很多类型
//给一个lambda样式的缺省值是因为有时候没有传删除器,无法直接使用现成的初始化
有了定制删除器对管理空间进行析构的时候就直接调用定制删除器(function对象)就可以了,不需要使用delete毕竟不是所有的类型都可以用delete进行销毁的。代码如下:
void release()
{
if (--(*_count) == 0)
{
//delete _ptr;
_del(_ptr);
delete _count;
_ptr = nullptr;
_count = nullptr;
}
}
那这样将部分1到部分6全部整合在一起就是完整的shared_ptr了。整体代码如下
shared_ptr自己模拟实现的最终答案展示:
namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_count(new int(1))
,_del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
{
++(*_count);
}
void release()
{
if (--(*_count) == 0)
{
_del(_ptr);
delete _count;
_ptr = nullptr;
_count = nullptr;
}
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
return *this;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_count;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _count;
function<void(T* _ptr)> _del = [](T* _ptr) {delete _ptr; };
};
代码测试:
int main()
{
bit::shared_ptr<Date> sp1(new Date);
bit::shared_ptr<Date> sp2(sp1);
bit::shared_ptr<Date> sp3(new Date);
// 自己给自己赋值
sp3 = sp3;
sp1 = sp2;
sp1 = sp3;
sp2 = sp3;
bit::shared_ptr<FILE> sp5(fopen("test.cpp", "w"), Fclose());
bit::shared_ptr<int> sp6((int*)malloc(40), [](int* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
});
return 0;
}
经过测试发现我们写的实现逻辑没有什么问题!!!
其实你到面试的时候,一问到模拟实现shared_ptr就最先想到是一个名为shared_ptr的类,然后依照上面的部分1到5逐步回忆着其中的逻辑然后按顺序写出来,我相信你一定可以完成的。