在C++中,如果手动分配和释放内存,很容易会引起内存泄漏的问题,第一是代码必须显示的写出资源分配和释放,很容易忘记写释放这部分,第二是如果在分配内存之后产生异常,无法执行释放内存的代码。
针对第二点,也可以通过try catch的方式,如果执行产生异常,那就在catch里去释放,但这样对分配单个资源有效,如果是分配多个资源,在catch里释放时就要判断每个资源是否分配成功,比较麻烦,另外如果在某个函数调用里去分配资源,在分配资源的时候分配失败了,回到了上层函数,那么就无法正常释放资源(在函数里定义的指针和分配的资源都是局部对象)。
通过RAII的思想就可以解决上述问题,RAII简单来说就是资源获取即为初始化,也就是在将资源绑定到一个对象上(智能指针就是这个对象,不过是一个模板类,而且重载了*和->运算符,可以像指针一样使用的对象),在该对象析构函数中释放资源,因此出对象作用域时,对象自动析构,也就自动将资源释放了。
因此,就有了最简单的智能指针的定义,首先它是一个模板类,里面有一个类的指针,然后需要重载*和->运算符,返回引用和指针。类似这样:
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr() { cout << "delete:" << _ptr << endl; delete _ptr; _ptr = nullptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
但这样带来的一个明显的问题是,在拷贝构造或者赋值时,两个智能指针会指向同一块地址,这样就会造成多次析构,造成程序崩溃。对这个问题,C++98中的auto_ptr(C++98中唯一的智能指针)的解决方法是将所有权转移,也就是拷贝或赋值时将原来的指针置空,但是这样又带来了新的问题,就是原来的指针不能用了,因此auto_ptr是一个比较失败的设计。它的实现如下:
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& p)
{
_ptr = p._ptr;
p._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
_ptr = p._ptr;
p._ptr = nullptr;
}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
后来C++中的unique_ptr提供了另一种思路,即禁止拷贝和复制,这个智能指针独占一种资源,它的模拟实现如下:
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
拷贝构造函数和赋值的操作符重载函数后加上了delete,表示不能调用该函数,并且不能定义该函数。
那么unique_ptr就比较死板,如果想拷贝,想赋值呢?shared_ptr来了,它通过计数的方式,每次拷贝或赋值计数加一,每次析构计数减一,只有减到0的时候才去释放资源,由于计数也必须在拷贝或赋值时传递,因此将计数以指针的形式传递,只有在通过类构造时去new一个计数,而在拷贝或赋值时均传递计数的指针,在计数为0释放资源时,同步释放计数的资源。它的模拟实现如下:
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& p)
{
_ptr = p._ptr;
_pCount = p._pCount;
(*_pCount)++;
}
void Release()
{
if (--(*_pCount) == 0) // 当计数为0,需要释放pCount
{
if (_ptr) // 如果_ptr为空,只要释放pCount
{
delete _ptr;
_ptr = nullptr;
}
cout << "~shared_ptr()" << endl;
delete _pCount;
_pCount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& p)
{
// 指向资源不同时,使两指针指向同一资源,并且计数增加
if (_ptr != p._ptr) // 当指向资源相同时,没有必要进行赋值
{
Release(); // 先释放该指针之前指向的空间
_ptr = p._ptr;
_pCount = p._pCount;
* _pCount++;
}
}
~shared_ptr()
{
Release();
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
int* _pCount;
};
但是shared_ptr仍然不完美,因为循环引用时会带来问题,典型的例子是双向链表,双向链表的节点包含之前前后两个节点的指针,如果都用智能指针来表示。定义两个双向链表类的对象p1和p2,都用智能指针来表示,如果仅定义,那么用sharedptr没有问题,但是如果将p2赋值给p1的后继指针,那么p2的count将加1,变为2,再将p1赋值给p2的前指针,那么p1的count也将加1,变为2,此时结束,两个对象的count都会减1变成1,但是不会触发释放内存。
那么如何解决这个问题呢,如果把链表节点类里的前后指针只用于指向,而不增加或减小计数就可以了,这个就是weak_ptr,它的模拟实现如下:
template <class T>
class weak_ptr
{
public:
weak_ptr(T* ptr)
:_ptr(ptr)
{}
weak_ptr(weak_ptr<T>& p)
{
_ptr = p._ptr;
}
weak_ptr<T>& operator=(weak_ptr<T>& p)
{
_ptr = p._ptr;
return *this;
}
weak_ptr<T>& operator=(shared_ptr<T>& p)
{
_ptr = p.get();
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
// Node的前后指针类型改为weak_ptr
struct Node
{
Node()
: _prev(nullptr)
, _next(nullptr)
, _val(0)
{}
myPtr::weak_ptr<Node> _prev;
myPtr::weak_ptr<Node> _next;
int _val;
};
weak_ptr作为弱指针,即不参与资源的管理,只是用来指向资源,类似于容器里的迭代器。
另一个问题又来了,上述实现中都是使用的delete,但是如果资源分配时使用的是malloc,或者是打开的一个文件,资源释放时就不能用delete了,而是需要定制删除方法,所谓定制化删除器,在C++中其实现原理也很简单,是通过一个模板类仿函数,作为模板参数被传入。unique_ptr的定制化删除器可以实现如下:
template <class T>
struct DefaultDelete
{
void operator()(T* p)
{
delete p;
cout << "delete:" << p << endl;
}
};
template <class T, class Delete = DefaultDelete<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
~unique_ptr()
{
Delete del;
//cout << "delete:" << _ptr << endl;
del(_ptr);
_ptr = nullptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};