智能指针介绍
智能指针(RAII)是利用对象的生命周期来管理资源的技术。
RAII,Resource Acquisition Is Initialization 顾名思义,就是在初始化对象的时候获取资源,在这个对象进行析构时会帮我们释放资源,这样做的好处有很多:
- 不需要显示的释放资源
- 可以避免因为没有及时释放资源而造成的内存泄漏
- 资源的生命周期与对象相同
智能指针原理
下面简单的实现一个智能指针
//1. 首先为了能让智能指针管理任意类型的资源, 将其设置为模板类
template<class T>
class RAIIPtr {
public:
//在构造的时候传入需要管理的资源
RAIIPtr(T* ptr = nullptr)
:_ptr(ptr){}
//析构时释放资源
~RAIIPtr() {
if (_ptr) {
delete _ptr;
}
}
//重载 * -> 使之能像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
protected:
T* _ptr;
};
C++标准库中的智能指针
-
std::auto_ptr
设计思想:一旦发生拷贝,就将资源转移
下面模拟实现auto_ptr:template<class T> class AutoPtr { public: AutoPtr(T* ptr = nullptr) : RAIIPtr<T>(ptr) {} ~AutoPtr() { if (_ptr) { delete _ptr; } } //转移资源并将原指针悬空 AutoPtr(AutoPtr<T>& sap) :_ptr(sap) { sap._ptr = nullptr; } AutoPtr<T>& operator=(AutoPtr<T>& sap) { //避免需要管理的资源被释放 if (*this != sap) { //将原资源释放 if (_ptr) { delete _ptr; } _ptr = sap._ptr; sap._ptr = nullptr; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
从auto_ptr的模拟实现中可以理解到其原理,使用时如果旧的auto_ptr对象给新的auto_ptr对象赋值,则会导致原来的对象被悬空,这时候再对原来的对象进行解引用则会出错
-
std::unique_ptr
设计思想:每个指针对象独一无二,不能被拷贝
模拟实现:template<class T> class UniquePtr { public: UniquePtr(T * ptr = nullptr) : _ptr(ptr) {} ~UniquePtr() { if (_ptr) { delete _ptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } UniquePtr(const UniquePtr<T>&) = delete; UniquePtr<T>& operator=(const UniquePtr<T> &) = delete; private: T* _ptr; };
因为禁止了拷贝,unique_ptr相对来说比较安全
-
std::shared_ptr
shared_ptr支持拷贝并且比auto_ptr更加的安全
设计思想:采用引用计数的方式实现多个shared_ptr对象之间共享资源
shared_ptr内部应实现以下几个功能:- 拷贝构造时引用计数自加
- 赋值运算符重载时将源资源的引用计数自减,并将目标资源的引用资源自加
- 相同的资源要共享同一份引用计数,不同的资源引用计数不同,因此要在堆上开辟空间保存引用计数
下面模拟实现一个shared_ptr
template<class T> class SharedPtr { public: SharedPtr(T* ptr = nullptr) : _ptr(ptr) , p_RefCount(new int(1)) , p_mutex(new std::mutex) {} ~SharedPtr() { DecRefCount(); } SharedPtr(SharedPtr<T>& sp) : _ptr(sp._ptr) , p_RefCount(sp.p_RefCount) , p_mutex(sp.p_mutex) { AddRefCount(); } SharedPtr<T>& operator=(SharedPtr<T>& sp) { if (_ptr != sp._ptr) { DecRefCount(); _ptr = sp._ptr; p_RefCount = sp.p_RefCount; p_mutex = sp.p_mutex; AddRefCount(); } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: //引用计数自加 void AddRefCount() { p_mutex->lock(); ++(*p_RefCount); p_mutex->unlock(); } //引用计数自减 void DecRefCount() { bool IsDelete = false; p_mutex->lock(); if (--(*p_RefCount) == 0) { delete p_RefCount; delete _ptr; IsDelete = true; } p_mutex->unlock(); //释放锁资源 delete p_mutex; } T* _ptr; int* p_RefCount; std::mutex* p_mutex; };
在引用计数++或–的过程中可能会因为线程安全的问题使得引用计数出错,所以要在进行这些操作的时候上锁
虽然保证了线程安全,但shared_ptr还是有其他问题存在的,在一些特殊场景下依旧会导致内存泄漏,比如说循环引用的场景struct ListNode { int data; std::shared_ptr<ListNode> prev; std::shared_ptr<ListNode> next; }; int main() { std::shared_ptr<ListNode> node1(new ListNode); std::shared_ptr<ListNode> node2(new ListNode); node1->next = node2; node2->prev = node1; return 0; }
在析构时,先析构node2,但是由于node1中的next成员也保存了一份node2的资源,此时node2的引用计数为2,析构时并不会释放node2当中的节点资源。
因为node2当中保存的ListNode节点资源没有释放,也就不会调用ListNode的析构函数,所以node2的成员prev并没有释放,node1的引用计数不变
同理,node1调用析构时也不会释放node2的资源,这样就造成了内存泄漏