C++智能指针及其原理

智能指针介绍

智能指针(RAII)是利用对象的生命周期来管理资源的技术。
RAII,Resource Acquisition Is Initialization 顾名思义,就是在初始化对象的时候获取资源,在这个对象进行析构时会帮我们释放资源,这样做的好处有很多:

  1. 不需要显示的释放资源
  2. 可以避免因为没有及时释放资源而造成的内存泄漏
  3. 资源的生命周期与对象相同

智能指针原理

下面简单的实现一个智能指针

//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内部应实现以下几个功能:

    1. 拷贝构造时引用计数自加
    2. 赋值运算符重载时将源资源的引用计数自减,并将目标资源的引用资源自加
    3. 相同的资源要共享同一份引用计数,不同的资源引用计数不同,因此要在堆上开辟空间保存引用计数

    下面模拟实现一个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当中的节点资源。
    C++智能指针及其原理_第1张图片
    因为node2当中保存的ListNode节点资源没有释放,也就不会调用ListNode的析构函数,所以node2的成员prev并没有释放,node1的引用计数不变
    同理,node1调用析构时也不会释放node2的资源,这样就造成了内存泄漏

你可能感兴趣的:(C++学习)