【C++自学笔记】详细理解智能指针

一、智能指针的使用及原理

1、RAII

RAII是一种利用对象生命周期来控制程序资源(内存、文件句柄、网络连接、互斥量等等)的简单技术;

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象,有两大好处:

  • 不需要显示的访问;
  • 采用这种方式,对象所需要的资源在其生命期内始终有效;

2、std::auto_ptr

C++98版本库中提供的auto_ptr的智能指针;

class Date {
public:
	Date() {
		cout << "Date()" << endl;
	}
	~Date() {
		cout << "~Date()" << endl;
	}
	
	int _year;
	int _mouth;
	int _day;
};

void Test() {
	auto_ptr<Date> ap(new Date);
	auto_ptr<Date> copy(ap);
	//ap->_year = 2018;
}

auto_ptr的实现原理:管理权限转移,下面简化模拟实现了一份 AutoPtr 来了解它的原理;

template<class T>
class AutoPtr {
public:
	AutoPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	~AutoPtr() {
		if (_ptr) {
			delete _ptr;
		}
	}
	AutoPtr(AutoPtr<T>& ap) 
		:_ptr(ap._ptr)
	{
		ap._ptr = NULL;
	}
	AutoPtr<T> operator=(AutoPtr<T>& ap) {
		if (this != &ap) {
			delete _ptr;
			if (_ptr) {
				delete _ptr;
			}
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
private:
	T* _ptr;
};

一旦发生拷贝,就将 ap 中的资源转移到当前对象中,然后另 ap 与其所管理的资源断开联系;

3、std::unique_ptr

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;
	}
private:
	//C++98,只(私有)声明不实现
	UniquePtr(UniquePtr<T> const&);
	UniquePtr& operator=(UniquePtr<T> const&);

	//C++11,防拷贝 delete
	UniquePtr(UniquePtr<T> const&) = delete;
	UniquePtr& operator=(UniquePtr<T> const&) = delete;
private:
	T* _ptr;
};

4、std::shared_ptr

C++11推出的更靠谱且支持拷贝的智能指针;

1、shared_ptr的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源;

  • shared_ptr 在其内部,给每个资源都会维护着一份计数,用来记录该份资源被几个对象共享;
  • 在对象被销毁时(调用析构函数的时候),就说明自己不使用该资源了,对象引用计数减一;
  • 如果引用计数时0,就是说明自己时最后一个使用该资源的对象,必须释放该资源
  • 如果不是0了,就说明除了自己还有其他的对象在使用该份资源,不能释放资源,否则其他的对象就成了野指针;

template<class T>
class SharedPtr {
public:
	SharedPtr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pRefCount(new int(1))
		,_pMutex(new mutex)
	{}
	~SharedPtr() {
		Release();
	}
	SharedPtr(const SharedPtr<T>& sp) 
		:_ptr(sp._ptr)
		,_pRefCount(sp._pRefCount)
		,_pMutex(sp._pMutex)
	{
		AddRefCount();
	}
	SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
		if (_ptr != sp._ptr) {
			Release();

			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pMutex = sp._pMutex;

			AddRefCount();
		}
		return *this;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	int UseCount() {
		return *_pRefCount;
	}
	T* Get() {
		return _ptr;
	}
	void AddRefCount() {
		//加锁或者使用加1原子操作
		_pMutex->lock();
		++(*_pRefCount);
		_pMutex->unlock();
	}
private:
	void Release() {
		bool deleteflag = false;
		//引用计数-1,如果减到0,则释放资源
		_pMutex->lock();
		if (--(*_pRefCount) == 0) {
			delete _ptr;
			delete _pRefCount;
			deleteflag = true;
		}
		_pMutex->unlock();

		if (deleteflag == true) {
			delete _pMutex;
		}
	}
private:
	T* _ptr;
	int* _pRefCount; //引用计数
	mutex* _pMutex;	 //互斥锁
};

引入一段测试函数,试一试:

void Test() {
	SharedPtr<int> sp1(new int(10));
	SharedPtr<int> sp2(sp1);
	*sp2 = 20;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;

	SharedPtr<int> sp3(new int(10));
	sp2 = sp3;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	cout << sp3.UseCount() << endl;

	sp1 = sp3;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	cout << sp3.UseCount() << endl;

}

大致的执行过程如下所示:

  1. 申请了一块资源 A,由 sp1 指向该资源,此时 A 资源的引用计数是1;
  2. 拷贝构造了sp2,也是指向资源 A,所以此时的引用计数需要加1,此时 A 资源的引用计数是2;
  3. 申请了另一块资源B,由sp3指向该资源,此时 B 资源的引用计数是1;
  4. 执行 sp2 = sp3,此时sp2 和 sp3 指针都指向 B资源,所以此时的引用计数是2,而A资源,现在只有sp1,所以计数为1;
  5. 执行 sp1 = sp3,此时 sp1 、 sp2 、sp3都指向B资源,所以此时的引用计数是3;

2、std::shared_ptr的线程安全问题:

  • 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或者--,这个操作不是原子的,引用计数原来是1,++了两次,也可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题,所以只能在指针中引用计数++、--是需要加锁的,也就说引用计数的操作是线程安全的;
  • 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题;

3、std::shared_ptr的循环引用:

struct ListNode {
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;

	~ListNode() {
		cout << "~ListNode()" << endl;
	}
};

void Test() {
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	node1->_next = node2;
	node2->_prev = node1;

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
}

 

  1. node1和node2两个智能指针对象指向两个节点,引用计数变为1,不需要手动delete;
  2. node1的_next 指向 node2,node2 的_prev指向 node1,引用计数变成2;
  3. node1 和 node2 析构,引用计数减到1,但是_next还指向下一个节点,_prev还指向上一个节点;
  4. _next 属于node 的成员,node1 释放了,_next才会析构,而node1 由_prev管理,_prev 属于 node2成员,构成循环引用,谁也不会释放;

为了解决循环引用问题,引入了 weak_ptr 


struct ListNode {
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;

	~ListNode() {
		cout << "~ListNode()" << endl;
	}
};

void Test() {
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;


	node1->_next = node2;
	node2->_prev = node1;

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
}

 

weak_ptr的作用就是:node1->_next = node2;和 node2->_prev = node1 时 weak_ptr 的 next 和prev 不会增加node1 和node2的引用计数;

4、对于不是new 出来的对象(删除器)


template<class T>
struct FreeFunc {
	void operator()(T* ptr) {
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template<class T>
struct DeleteArrayFunc {
	void operator()(T* ptr) {
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};

void Test() {
	FreeFunc<int> freeFunc;
	shared_ptr<int> sp1((int*)malloc(4),freeFunc);

	DeleteArrayFunc<int> deleteArrayFunc;
	shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);

}

发布了79 篇原创文章 · 获赞 28 · 访问量 7767

猜你喜欢

转载自blog.csdn.net/weixin_43753894/article/details/99742281