C++学习记录——삼십 智能指针

文章目录

  • 1、为什么需要智能指针?
  • 2、内存泄漏
  • 3、智能指针的使用及原理
    • 1、RAII思想
    • 2、拷贝问题
      • 1、unique_ptr
      • 2、shared_ptr
        • 1、多线程
        • 2、循环引用
        • 3、定制删除器


1、为什么需要智能指针?

看一个场景

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

new是可能开辟失败,抛异常的。上述代码中,如果p1抛异常,那么可以外面的catch可以捕获到,打印出消息;如果p1异常,p2也要抛异常,那么在这之前,应当销毁p1,再去抛;同理,到了div()那里如果也抛异常,那么得销毁p1和p2,整体就得这样写

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	try
	{
		int* p2 = new int;
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}
	delete p1;
	delete p2;
}

一下子就能看出来,这太麻烦了,如果有多个new呢?

2、内存泄漏

C++学习记录——삼십 智能指针_第1张图片

C++学习记录——삼십 智能指针_第2张图片

Windows和Linux都有检测内存泄漏的工具,不过Windows下的VLD不太靠谱,Linux中valgrind是比较出名的

Linux下几款C++程序中的内存泄露检查工具

为了预防内存泄漏,常用的办法就是用智能指针或者事后检测。

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

1、RAII思想

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			cout << _ptr << endl;
			delete _ptr;
		}
	}

private:
	T* _ptr;
};

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

和封装锁的思路来类似,都是RAII。用临时变量来构造,出了作用域就自动销毁。

RAII利用对象生命周期来控制程序资源,对象构造时获取资源,析构时释放资源

上面的SmartPtr不像一个指针,它不能解引用数据,不过我们可以写对应的函数。

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	cout << *sp1 << endl;//如果模板参数是自定义类型的话就可以用->了。

2、拷贝问题

智能指针如何拷贝?

int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(sp1);
	return 0;
}

采用默认拷贝会浅拷贝,导致同一空间重复释放。这里应当如何写拷贝构造?是要用深拷贝吗?其实不是,我们要的浅拷贝,sp1和sp2指向同一个资源,以前的链表等这些迭代器结构不需要释放资源,而智能指针需要管理资源,所以不能单纯地浅拷贝,但是又不能要深拷贝。

C++98时已经有智能指针了,那个版本中有一个auto_ptr,它的方法是管理权转移,我们写到SmartPtr类中

	//管理权转移
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}
	auto_ptr<int> sp2(sp1);

这样看就像是用ap指向sp1,然后用sp1的_ptr来初始化sp2的_ptr,然后把sp1的_ptr给置空。虽然看起来是可以的,能解决问题,但有很大隐患,这会导致sp1悬空,如果不知道管理权转移的实际写法,那么下面代码中如果有*sp1就出问题了。程序员用它的时候需要时刻提醒自己,被拷贝对象已经悬空了,不能去解引用它。

在C++11之前,有个可移植的C++库——Boost库,不是标准库,但也胜似标准库,是有C++标准委员会库工作组成员发起的,C++中有很多标准都从Boost中吸收过来,像右值引用,线程库。Boost库有scoped_ptr,weak_ptr,_shared_ptr,C++11中把scoped_ptr改名成unique_ptr。

1、unique_ptr

它的思路是防拷贝

	//防拷贝
	//C++98思路:只声明不实现,但是还可以在外面强行定义,所以会把它放在私有里
	//C++11思路:函数后= delete
	//这里拷贝构造和赋值都写上
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

不需要拷贝的场景就用它。

2、shared_ptr

引用计数的思路。有多少个指针指向一个空间,那么这个空间的引用计数就是多少。当一个指针要释放时,如果引用计数大于0,那就不做操作,如果等于0,那就做一次释放资源,这个空间的指针也都用完了。

引用计数这个变量不能放在静态区,因为如果static修饰后,它属于类的每个对象,但我们要的是指向同一空间的所有指针。定义一个int* pcount。

	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		++(*_pcount);
	}

赋值函数,比如sp1 = sp3,那么sp1的引用计数需要–,因为它要指向新空间了;假设sp1的空间还有别的指针指向,而sp3的空间只有sp3这一个指针,sp3 = sp1,那么就是sp3–。

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			if (--(*_pcount) == 0)//处理空间上只有一个指针的情况
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

1、多线程

整体改成这样的形式来配合加锁

	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	void Release()
	{
		if (--(*_pcount) == 0)
		{
		    if(_ptr)//如果为空那就不需要释放
		    {
		        delete _ptr;
		    }
			delete _pcount;
		}
	}

	void AddCount()
	{
		++(*_pcount);
	}

	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		AddCount();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			AddCount();
		}
		return *this;
	}

多线程比较常见的场景就是线程安全问题。同一个数会出现多次操作,导致结果不是我们想要的。多线程情况下,像传给接收引用的参数时,要写成ref(…),ref是库中的函数,否则会被认为是传值传参。

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}

	void Release()
	{
		_pmtx.lock();
		if (--(*_pcount) == 0)
		{
		    if(_ptr)
		    {
		        delete _ptr;
		    }
			delete _pcount;
		}
		_pmtx.unlock();
	}

	void AddCount()
	{
		_pmtx.lock();
		++(*_pcount);
		_pmtx.unlock();
	}

	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._pmtx)
	{
		AddCount();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp->_pmtx;
			AddCount();
		}
		return *this;
	}

	//防拷贝
	//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里
	//C++11思路:函数后= delete
	//unique_ptr(const unique_ptr& up) = delete;
	//unique_ptr& operator=(const unique_ptr& up) = delete;

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* pcount;
	mutex* _pmtx;
};

在Release那里,到了引用计数减到0时,需要释放引用计数,释放锁。如果是在if里释放锁,那么外面的解锁操作就有问题了。 解决办法是可以设置一个状态位

	void Release()
	{
		_pmtx.lock();
		bool deleteFlag = false;
		if (--(*_pcount) == 0)
		{
		    if(_ptr)
		    {
		        delete _ptr;
		    }
			delete _pcount;
			deleteFlag = true;
		}
		_pmtx.unlock();
		if (deleteFlag)
		{
			delete _pmtx;
		}
	}

shared_ptr本身是线程安全的,因为计数是加锁保护的,它实例化的对象不是线程安全的,想要线程安全,那么在对对象操作时用锁保护就行。

2、循环引用

写一个场景,还是用上面的shared_ptr

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}

	void Release()
	{
		_pmtx.lock();
		bool deleteFlag = false;
		if (--(*_pcount) == 0)
		{
			if (_ptr)
			{
				delete _ptr;
			}
			delete _pcount;
			deleteFlag = true;
		}
		_pmtx.unlock();
		if (deleteFlag)
		{
			delete _pmtx;
		}
	}

	void AddCount()
	{
		_pmtx.lock();
		++(*_pcount);
		_pmtx.unlock();
	}

	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._pmtx)
	{
		AddCount();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp->_pmtx;
			AddCount();
		}
		return *this;
	}

	//防拷贝
	//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里
	//C++11思路:函数后= delete
	//unique_ptr(const unique_ptr& up) = delete;
	//unique_ptr& operator=(const unique_ptr& up) = delete;

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* pcount;
	mutex* _pmtx;
};

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;

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

int main()
{
	shared_ptr<ListNode> n1 = new ListNode;
	shared_ptr<ListNode> n2 = new ListNode;
	return 0;
}

当尝试连接两个节点时就发生了错误

	n1->_next = n2;
	n2->_prev = n1;

n1和n2是智能指针类型,而next和prev是ListNode类型的,无法赋值,那把ListNode里的两个指针换成shared_ptr< ListNode >类型的,但这样还不行,因为我们在定义next和prev时没有传参,是无参构造,所以在智能指针的类里应当写上缺省参数。

	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}
	
struct ListNode
{
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	int _val;

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

现在有一个问题

	n1->_next = n2;
	n2->_prev = n1;

如果两句都写,程序会不释放资源,而如果只写一句或者两句都不写,那就会释放资源,就会打印"~ListNode",使用库中的智能指针也是这样,这就是智能指针引起的循环引用问题。

n1和n2都有各自的next和prev,如果不相互连接,也就是什么都不写,那么next和prev随着n1n2销毁而销毁。

写了一句,比如n1->_next = n2,那么n2这个节点除了它本身,还有n1的next指向它,n2析构时,引用计数–,但是空间不销毁,n1析构时,里面的成员变量也会随着析构,那么整体也可以完好地退出。

但是两句都写就出问题了。

	n1->_next = n2;
	n2->_prev = n1;

出了作用域,n2先析构,引用计数–,但是还不能销毁空间,引用计数没有为0,也还有一个指针指向它;n1析构时,n1也是一样,也不能析构,引用计数–,现在这两个空间的引用计数都为1,n1的next指向n2的空间,n2的prev指向n1的空间,那么n1这个空间什么时候析构?要看prev,prev析构,n1这个空间就析构,但是n2这个空间由next指向,next析构,n2才能析构,prev才能析构,所以next和prev已经形成了相互制约的关系,没办法全部析构了。这就是循环引用,会导致内存泄漏。

为了解决这个问题,标准库中有个weak_ptr来辅助shared_ptr,也叫做弱指针。weak_ptr不是RAII的,也就是它不是常规的智能指针,但是支持像指针一样,专门用来解决shared_ptr的辅助引用问题。用weak这样写。

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;

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

weak_ptr不会增加引用计数。标准库中weak_ptr实现得很复杂,我们这里只模拟实现一个简单的

	template <class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};
struct ListNode
{
	zyd::weak_ptr<ListNode> _next;
	zyd::weak_ptr<ListNode> _prev;
	int _val;

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

int main()
{
	zyd::shared_ptr<ListNode> n1 = new ListNode;
	zyd::shared_ptr<ListNode> n2 = new ListNode;

	n1->_next = n2;
	n2->_prev = n1;
	return 0;
}

3、定制删除器

在实例化的时候,传new int[10]这样的话,可能会崩溃,是因为new []会在开辟的空间前再开辟一个存放元素个数的空间,但是delete的时候会从开辟的空间开始释放,而不包含那个存储个数的空间,所以本质上是释放的位置不对。

定制删除器本质上是一个可调用对象,函数指针,仿函数,lambda都可以。

template <class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "仿函数" << endl;
		delete[] ptr;
	}
};

int main()
{
	//zyd::shared_ptr n1 = new ListNode;
	//zyd::shared_ptr n2 = new ListNode;

	//n1->_next = n2;
	//n2->_prev = n1;
	std::shared_ptr(int) spa1(new int[10], DeleteArray<int>());//仿函数
	std::shared_ptr(int) spa2(new int[10], [](int* ptr) {delete[] ptr; });//lambda
	return 0;
}

库中的做法是把这个删除器放到构造函数里,实例化的时候传过来,保存起来,析构时用它去析构。这里的重点在于如何保存这个删除器。一个是我们可以在总的模板参数那里加一个模板参数,那么析构函数就可以直接用,也不用在构造函数那里在写上一个模板参数;或者用包装器。这里写包装器。

		template <class D>
		shared_ptr(const shared_ptr<T>& sp, D del)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
			, _del(del)
		{
			AddCount();
		}
    
        void Release()
		{
			_pmtx.lock();
			bool deleteFlag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					//delete _ptr;
					_del(_ptr);
				}
				delete _pcount;
				deleteFlag = true;
			}
			_pmtx.unlock();
			if (deleteFlag)
			{
				delete _pmtx;
			}
		}
        
    private:
	   	T* _ptr;
		int* _pcount;
		mutex* _pmtx;
		functional<void(T*)> _del;

这样写其实会有问题,如果用不到这个删除器就会调用默认构造,删除器没有初始化,到了析构时,删除器就是被编译器默认初始化的,用它来析构就容易出问题。我们可以用缺省

	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
		functional<void(T*)> _del = [](T* ptr) {
			cout << "lambda delete:" << ptr << endl;
			delete ptr;
		};

定制删除器当作了解,重点在于shared_ptr的实现。

本篇gitee

结束。

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