目录
一.智能指针的概念
(一).智能指针的历史
(二).智能指针的使用
插曲.auto_ptr
①unique_ptr
③weak_ptr
二.智能指针的实现
三.智能指针的缺陷
(一).循环引用
(二).定制删除器
一.智能指针的概念
智能指针指在通过RAII(Resource Acquisition Is Initialization,即资源获取就是初始化)技术,免去手动释放指针指向资源的步骤,希望能自动释放内存资源。底层而言,就是通过封装一个类,使用析构函数来释放资源。
当类对象出作用域被销毁时,会自动调用析构函数从而完成释放资源。
(一).智能指针的历史
最初的智能指针是C++98提出来的auto_ptr,不过因为使用时缺陷很大,Beman G.Dawes(C++委员会成员之一)所成立的boost社区(专门面向C++程序员,提供许多免费好用的自制库)贡献了scoped_ptr、shared_ptr、weak_ptr被C++11采纳,修改为官方的unique_ptr、shared_ptr、weak_ptr。
(二).智能指针的使用
头文件<memory>
ps:虽然下面的auto_ptr、unique_ptr之类均是库模板类,但为了便于理解均称为指针。
插曲.auto_ptr
对于auto_ptr,小编认为它很鸡肋。虽然它也能够自动释放资源,但是一旦对类进行拷贝,那么会将资源转移给拷贝对象,而被拷贝者将失去资源(很像移动拷贝)。因此,一旦后续有调用被拷贝对象,那将引发巨大安全问题。
①unique_ptr
顾名思义,“独一无二”的指针,即只能有一个指针指向申请的资源空间,不存在两个指针同时指向一个空间,可以说这个就是C++11中对auto_ptr的改进,将它作为智能指针的一种形式。
与auto_ptr不同,unique_ptr禁止拷贝行为,当然赋值也是禁止的。
②shared_ptr
“可以分享”的指针,说明允许拷贝与赋值行为,即允许多个指针指向同一片空间。
同时不管有多少指针指向一个空间,这个空间只会释放一次。
std::shared_ptr<int> p1(new int);
std::shared_ptr<int> p2;
p2 = p1;//不会有任何问题,资源只会释放一次
③weak_ptr
“无权”的指针(小编认为这里翻译成无权更好,而不是虚弱),该指针不会释放指向的资源,用于解决shared_ptr的一些缺陷(循环引用),第三部分会详细讲解。
二.智能指针的实现
这里我们重点实现shared_ptr。
首先定义一个模板类,其中一个指针成员负责指向申请的空间,析构函数中delete该指针成员即可完成资源自动释放。
但是难点在于怎么拷贝,如果单纯拷贝的话那会引起资源重复释放的问题。
因此我们采用引用计数的方式解决。
专门动态开辟一个int空间用于记录我们申请的资源空间有多少个指针指向。
当调用构造、拷贝、赋值时计数++,调用析构时计数--,直到计数空间值为0时真正释放资源。
值得注意的是,赋值需要考虑被赋值者原资源空间以及原来的计数空间。
代码如下:
template<class T>
class Shared_ptr
{
typedef Shared_ptr<T> Self;
private:
bool Destory()//销毁
{
if (pCount == nullptr || (*pCount) == 0) return false;
(*pCount)--;//计数--
if ((*pCount) == 0 && _ptr)
{
delete _ptr;
delete pCount;
}
_ptr = nullptr;
pCount = nullptr;
return true;
}
public:
Shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, pCount(new int(1))
{}
Shared_ptr(const Self& sp)//拷贝构造
:_ptr(sp._ptr)
, pCount(sp.pCount)
{
(*pCount)++;
}
Self& operator=(const Self& sp)//赋值
{
if (_ptr == sp._ptr) return *this;
Destory();
_ptr = sp._ptr;
pCount = sp.pCount;
(*pCount)++;
return *this;
}
bool release()//释放本指针空间(还有其他指向时不释放空间)
{
return Destory();
}
~Shared_ptr()//析构
{
std::cout << "~" << std::endl;
Destory();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
bool expired()//检查是否过期(指向空间是否已被释放)
{
return pCount != nullptr && *pCount == 0;
}
size_t get_count() const
{
return *pCount;
}
private:
T* _ptr;//指向空间
int* pCount;//指向计数空间
};
三.智能指针的缺陷
(一).循环引用
shared_ptr隐藏了一个极大的bug。
看看下面代码:
class A {
public:
~A() {
std::cout << "析构" << std::endl;
}
std::shared_ptr<A> next;
std::shared_ptr<A> prev;
int i = 0;
};
int main()
{
std::shared_ptr<A> p1(new A);
std::shared_ptr<A> p2(new A);
p1->next = p2;
p2->prev = p1;
return EXIT_SUCCESS;
}
正常情况下,应该会打印两次“析构”,分别是调用p1和p2析构函数delete指向空间时。
但实际是一个都没打印:
这是因为出现了循环引用的情况。
p1指向空间的next指向p2空间,导致p2空间的计数变成2,同理p2指向空间的prev指向p1空间,p1空间计数变成2。
但是当析构时只有p1和p2出栈,也就是分别调用一次析构函数,两个空间的计数均-1,变成1。此时并不会真正释放空间,但“明面上”又找不到指向这两个空间的指针。因为这两个堆空间又分别被对方堆空间上的指针指向,算是一种“死锁”吧。
画图如下:
解决方式:
专门定义一个模板类智能指针,只能指向空间但不能改变计数数量即可。
即weak_ptr(名字也就是这么来的)。
代码如下:
ps:小编采用的是将weak_ptr作为shared_ptr的友元类,也可以是普通模板类。
class Shared_ptr
{
typedef Shared_ptr<T> Self;
template<class T>
friend class Weak_ptr;
. . .
}
template<class T>
class Weak_ptr
{
typedef Weak_ptr<T> Self;
typedef Shared_ptr<T> Sptr;
public:
Weak_ptr()
:_ptr(nullptr)
{}
Weak_ptr(const Self& wp)
:_ptr(wp._ptr)
,pCount(wp.pCount)
{}
Weak_ptr(const Sptr& sp)
:_ptr(sp._ptr)
,pCount(sp.pCount)
{}
~Weak_ptr()
{}
Self& operator=(const Self& wp)
{
_ptr = wp._ptr;
pCount = wp.pCount;
return *this;
}
Self& operator=(const Sptr& sp)
{
_ptr = sp._ptr;
pCount = sp.pCount;
return *this;
}
bool expired()
{
return *pCount == 0;
}
T& operator*()
{
assert(!expired());
return *_ptr;
}
T* operator->()
{
assert(!expired());
return _ptr;
}
private:
T* _ptr;
int* pCount;
};
(二).定制删除器
细心的可能发现了,析构函数只有delete一种方式,那如果我们需要的是delete[]呢?
比如这种情况:
std::shared_ptr<A> p1(new A[5]);
这时将报错:
但如果是内置类型比如int又不会报错了。
这是因为有析构函数的存在,如果自定义类型(weak_ptr)没有析构函数那也不会报错。
下面我们仔细梳理一下:
如果自定义类型没有析构函数,那么对于编译器而言只需要把它按内置类型处理即可。
但对于有析构函数的自定义类型,编译器会根据new空间的个数,在空间之前再开辟4个字节记录该类型开辟的数量。当调用delete[]时会根据这个记录调用对应次的析构函数。
但因为我们使用的是delete,根据记录本应调用n次,而delete只会调用一次,与记录有冲突从而报错。
解决方式:
根据需要删除的对象,确定需要delete或delete[],传一个仿函数给智能指针模板,删除时调用仿函数即可。
代码如下:
template<class T>//默认仿函数,默认使用delete
class DefaultDelete {
public:
void operator()(T* _ptr) {
delete _ptr;
}
};
template<class T, class D = DefaultDelete<T>>//传一个默认的仿函数
class Shared_ptr
{
private:
bool Destory()//销毁
{
. . .
if ((*pCount) == 0 && _ptr)
{
D()( _ptr);
delete pCount;
}
. . .
}
. . .
}
template<class T>//可以自制一个传delete[]
class Free {
public:
void operator()(T* _ptr) {
delete[] _ptr;
}
};
值得注意的是,官方的shared_ptr是在构造函数中传仿函数对象,而unique_ptr是传仿函数类型给模板参数。
我不是一个伟大的程序员,我只是一个具有良好习惯的优秀程序员—— Kent Beck
如有错误,敬请斧正