首先什么是智能指针?
RAII:资源分配即初始化,通俗点来讲,就是定义一个类来封装资源的分配和释放,再构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
实现机制:是利用类的构造和析构函数(释放资源)是由编译器自动调用的。
智能指针不仅管理执行对象的释放问题,还可以像指针一样的使用。
C++标准库中主要有四个智能指针,分别是auto_ptr,shared_ptr,scoped_ptr和weak_ptr。auto_ptr是C++98标准化才引入的,scoped_ptr,shared_ptr和weak_ptr是C++11标准化才引入的。auto_ptr是对资源进行了转移,虽说简单,但使用起来都是坑,因此在任何情况下建议都不要使用;scoped_ptr是资源独占,防拷贝和赋值,将其拷贝函数和赋值操作的重载只给出私有的声明(面试中若写智能指针,可写scoped_ptr简单不易出错);shared_ptr是对资源的共享,可以拷贝和赋值(与weak_ptr结合使用),weak_ptr主要与shared_ptr配合使用,自己不能直接管理动态开辟的空间。
下面我主要想谈一下shared_ptr。
首先我们来看一下shared_ptr的代码实现:
template<class T>
class del
{
public:
void operator()(T* _ptr)
{
delete _ptr;
_ptr = NULL;
}
};
class Fclose
{
public:
FILE* fclose(FILE* ptr)
{
cout << "fclose" << endl;
fclose(ptr);
}
};
template<class T>
class Free
{
public:
T* free(T* ptr)
{
free(ptr);
ptr = NULL;
}
};
template<class T,class Des=del>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:ptr(_ptr)
, pcount(_pcount)
{
if (ptr != NULL)
{
*ptr = new int;
*pcount = 1;
}
}
shared_ptr(shared_ptr& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
if (_pcount)
{
(*_pcount)++;
}
}
~shared_ptr()
{
cout << "~shared_ptr()" << endl;
if (_ptr&&!(--*_pcount))
{
destroy();
}
}
shared_ptr& operator=(shared_ptr& sp)
{
if (!this == &sp)
{
if (_ptr && (--*pcount))
{
destroy();
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
void destroy()
{
del<T>()(_ptr);
delete _pcount;
_pcount = NULL;
}
T* _ptr;
int* _pcount;
};
虽然看起来好像没有什么问题了,但shared_ptr还存在三个问题:线程安全问题,需要定制删除器以及循环引用问题。
线程安全问题:
1.一个shared_ptr实体可被各个线程同时读取;
2.两个的shared_ptr实体可以被两个线程同时写入,“析构”算写操作;
3.如果要从各个线程读写同一个shared_ptr对象,那么需要加锁。
定制删除器:
在shared_ptr中被管理的资源都需要用delete来释放,当被管理的资源若是文件类型,或是用malloc开辟出来的动态空间,则无法使用delete来释放,若不做任何的处理,程序将奔溃,所以,我们要为其定制删除器。上面给出的代码就为其定制了删除器,使程序书写变得麻烦。
循环引用问题:
我们先来看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"
struct Node
{
Node(int value)
:_value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
}
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
int _value;
};
void Test2()
{
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
Test2();
return 0;
}
构建shared_ptr的两个对象sp1,sp2,先构造的后释放,后构造的先释放,先释放的是sp2,那么它的引用计数为2,减去1之后成为1,不能进行释放,因为sp1还在管理这段空间,但是sp2这个变量已经被销毁,因为它是栈上的变量,但sp2管理的堆上的空间并没有释放。接下来,释放sp1,同样,先将引用计数减去1,引用计数变成了1,因此也不会释放sp1管理的动态空间。只有当sp1,sp2引用计数变成0时,空间才允许被释放。也就是说sp2要释放,必须要sp1释放,但sp1要释放,必须要等sp2释放。最终它们两个对象都有释放空间,造成了内存的泄露。
下面根据下图,我们来理解一下sp1,sp2的构造过程,以及它们是如何利用shared_ptr智能指针来管理空间的。
这是使用shared_ptr所构建出来的对象。一部分用来管理引用计数,另一部分用来管理节点。
如图,便是上述双向链表最终构建出来的节点指针的指向。
首先是构造智能指针sp1,先对其引用计数开辟空间,并初始化user和weaks均为1,然后将节点的空间交给__ptr来管理,引用计数的空间交给_Ref来管理。然后在构建sp2,构造过程与sp1相同。代码中sp2->prev=sp1与sp1->next=sp2调用过程相同,都是将各自的引用计数空间中的user加至2,然后sp1中的_next节点中的_Ref管理sp2的引用计数;sp2中的_prev节点中的_Ref管理的sp1的引用计数,_ptr管理sp1节点。析构时,先析构sp2,但引用计数中的user为1,无法释放空间,再去析构sp1,与sp2相同,也无法对空间进行释放。所以,开辟出来的四块动态内存空间,都没有释放。
这就是shared_ptr的循环引用问题。
以上就是对shared_ptr进行的讨论,若还有哪些地方没有涉及到欢迎补充。