【C++】智能指针

时间:2022-05-27 01:08:26

一、引入

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

void fun()
{
	int* p = new int[10];
	cout << div() << endl;
	delete[]p;
}

int main()
{
	try
	{
		fun();
	}
	catch (const char* errstr)
	{
		cout << errstr << endl;
	}
	catch (...)
	{
		cout << "未知错误" << endl;
	}
	return 0;
}

在上一章【C++】异常中为了这种会导致内存泄漏的情况,我们的办法是在fun函数内在try+catch重复throw出异常。

void fun()
{
	int* p = new int[10];
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete[]p;
		cout << "delete[]p 1" << endl;
		throw "除0错误";
	}
	cout << "delete[]p 2" << endl;
	delete[]p;
}

【C++】智能指针
但是如果有两个空间需要释放:

void fun()
{
	int* p1 = new int[10];
	int* p2 = new int[15];
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete[]p1;
		delete[]p2;
		throw "除0错误";
	}
	delete[]p1;
	delete[]p2;
}

我们要知道new也是会抛异常的,如果p1抛出了异常,那么p2就没有被释放掉,造成内存泄漏。
而智能指针就能很好的解决这个问题。

二、智能指针

2.1 智能指针保存与释放资源RAII

template <class T>
class SmartPtr
{
public:
	// 保存资源
	SmartPtr(T* ptr)
		: _ptr(ptr)
	{}
	// 释放资源
	~SmartPtr()
	{
		delete[]_ptr;
		cout << _ptr << endl;
	}
private:
	T* _ptr;
};

有了这个以后不管谁抛异常我们就不需要再考虑资源回收的问题了:

void fun()
{
	int* p1 = new int[10];
	SmartPtr<int> sp1(p1);
	SmartPtr<int> sp2(new int[15]);
	cout << div() << endl;
}

【C++】智能指针
当我们把堆上的资源交给智能指针后,出了作用域后就会直接释放,就算抛了异常也会出作用域后销毁。
而智能指针的构造函数相当于保存资源、析构函数相当于释放资源

RAII:

RAII是一种利用对象生命周期来控制程序资源的资源。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
把资源的声明周期和对象的生命周期绑定到一起
这样就有两点好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

而智能指针既然是指针就要像指针一样使用,还要有其他的操作。

2.2 智能指针的其他操作

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

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

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

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

这样我们就可以像一个指针一样使用它

1.3 智能指针拷贝问题

原生指针的拷贝就是指向同一块空间,但是原生指针销毁的时候不会清理资源,而智能指针就会对同一块空间析构两次

先来看看库中的智能指针怎么解决这个问题的。

1.4 auto_ptr管理权转移

int main()
{
	std::auto_ptr<int> ptr1(new int);
	std::auto_ptr<int> ptr2(ptr1);
	return 0;
}

【C++】智能指针
????????????
【C++】智能指针
可以看到auto_ptr使用拷贝构造就是把原来的资源转移走
这样就有可能会造成空指针访问

1.5 unique_ptr防拷贝

unique_ptr的方法简单粗暴:直接不让拷贝

int main()
{
	std::unique_ptr<int> ptr1(new int);
	std::unique_ptr<int> ptr2(ptr1);
	return 0;
}

【C++】智能指针

1.6 shared_ptr引用计数❗️❗️

当两个指针同时指向一块空间时,就增加一个引用计数。

1.6.1 引用计数的实现

在开辟空间的时候在堆上申请一块空间,指针同时指向资源和堆上的空间,堆上的空间就可以用来计数。
【C++】智能指针

// SmartPtr.h
template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
		, _pcnt(new int(1))
	{}

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

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

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

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
	int* _pcnt;
};

// SmartpPtr.cpp
int main()
{
	SmartPtr<int> ptr1(new int);
	SmartPtr<int> ptr2(ptr1);
	return 0;
}

【C++】智能指针

1.6.2 赋值问题

这里首先要解决的是自己给自己赋值的问题,不能直接this != &sp,因为有可能是不一样的智能指针指向同一块资源空间
正确写法:if (_ptr != sp->_ptr)
其次就要解决释放左边的空间,这里注意不能直接释放,而是要看引用计数是否为0,为0才能释放。

SmartPtr<T>& operator=(const SmartPtr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		(*_pcnt)--;
		if (*_pcnt == 0)
		{
			this->~SmartPtr();
			//delete _pcnt;
			//delete _ptr;
		}
		_ptr = sp._ptr;
		_pcnt = sp._pcnt;
		(*_pcnt)++;
	}
	return *this;
}

1.6.3 多线程拷贝问题

假设我们现在是多线程的情况要对一个智能指针进行拷贝
我们先在智能指针类放一个能获取引用计数的成员函数:

// 获取引用计数值
int use_count()
{
	return *_pcnt;
}

然后多线程开始不停的用临时的智能指针进行拷贝:

void test()
{
	const int N = 100000;
	SmartPtr<int> sp1(new int[10]);

	std::thread t1([&]() {
		for (int i = 0; i < N; i++)
		{
			SmartPtr<int> sp2(sp1);
		}
		});
	std::thread t2([&]() {
		for (int i = 0; i < N; i++)
		{
			SmartPtr<int> sp3(sp1);
		}
		});
	t1.join();
	t2.join();
	cout << sp1.use_count() << endl;
}

按道理来说t1和t2线程内部定义的智能指针都是临时对象,拷贝完就会销毁,按道理说最后输出应该为1。但是我们来看看结果:
【C++】智能指针
不仅如此有时候程序还会崩溃。
原因:

我们知道++--都不是原子性的,假设现在引用计数是1,本来应该是线程1和线程2都会把引用计数++,结果变成3,但是它们同时++,导致结果变成了2,然后线程1和线程2临时对象销毁,引用计数都要–,结果导致引用计数变成0,销毁资源,造成野指针

为了解决这种情况我们就可以进行加锁,让线程串行访问。
所以我们要加一个成员变量(锁)。而因为要同时保护引用计数++--所以必须是同一把锁。
而因为锁是防拷贝的,所以我们要使用指针。
所有引用计数改变的地方都要保护起来。

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
		, _pcnt(new int(1))
		, _pmutex(new std::mutex)
	{}

	SmartPtr(const SmartPtr<T>& sp)
		: _ptr(sp._ptr)
		, _pcnt(sp._pcnt)
		, _pmutex(sp._pmutex)
	{
		_pmutex->lock();
		(*_pcnt)++;
		_pmutex->unlock();
	}

	~SmartPtr()
	{
		_pmutex->lock();
		(*_pcnt)--;
		_pmutex->unlock();
		if (*_pcnt == 0)
		{
			delete _ptr;
			delete _pcnt;
			delete _pmutex;
			//cout << _ptr << endl;
		}
	}

	SmartPtr<T>& operator=(const SmartPtr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			_pmutex->lock();
			(*_pcnt)--;
			_pmutex->unlock();
			if (*_pcnt == 0)
			{
				this->~SmartPtr();
				//delete _pcnt;
				//delete _ptr;
			}
			_ptr = sp._ptr;
			_pcnt = sp._pcnt;
			_pmutex = sp._pmutex;
			_pmutex->lock();
			(*_pcnt)++;
			_pmutex->unlock();
		}
		return *this;
	}

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

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

	// 获取引用计数值
	int use_count()
	{
		return *_pcnt;
	}
private:
	T* _ptr;
	int* _pcnt;
	std::mutex* _pmutex;
};

void test()
{
	const int N = 100000;
	SmartPtr<int> sp1(new int[10]);

	std::thread t1([&]() {
		for (int i = 0; i < N; i++)
		{
			SmartPtr<int> sp2 = sp1;
		}
		});
	std::thread t2([&]() {
		for (int i = 0; i < N; i++)
		{
			SmartPtr<int> sp3 = sp1;
		}
		});
	t1.join();
	t2.join();
	cout << sp1.use_count() << endl;
}

【C++】智能指针
多次测试结果全部为1。

这里要注意的是share_ptr本身是线程安全的,但是它指向的资源不一定是线程安全的

1.6.4 循环引用问题❗️❗️

现在我们写一个链表链接的代码:

struct ListNode
{
	int val;
	ListNode* left;
	ListNode* right;

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

void test()
{
	ListNode* n1 = new ListNode;
	ListNode* n2 = new ListNode;
	n1->right = n2;
	n2->left = n1;
	delete n1;
	delete n2;
}

【C++】智能指针
现在我们不想自己delete资源,可以考虑使用智能指针。
而智能指针不能赋值给自定义指针,所以我们要改变指针的类型。

struct ListNode
{
	int val;
	SmartPtr<ListNode> left;
	SmartPtr<ListNode> right;

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

void test()
{
	SmartPtr<ListNode> n1 = new ListNode;
	SmartPtr<ListNode> n2 = new ListNode;
	n1->right = n2;
	n2->left = n1;
}

【C++】智能指针
但是我们看到结果并没有释放掉资源,这是怎么回事呢?

【C++】智能指针
对于n1资源,有n1指向和n2的left指向,所以引用计数为2,n2资源同理。当n1和n2出了作用域,两个的引用计数都变成1。
【C++】智能指针
但此时right和left是随着对象的销毁才能销毁,但是对象想要销毁,引用计数就要减为0,引用计数减为0,就要指针销毁,这样就成了个死循环。这里要注意的是如果只链接了一个就不会有这种问题。

而为了解决这个问题,我们引入了weak_ptr

1.7 weak_ptr不管资源

这里的weak_ptr没有引用计数,不支持RAII,只指向资源,不管理资源。

struct ListNode
{
	int val;
	std::weak_ptr<ListNode> left;
	std::weak_ptr<ListNode> right;

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

void test()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	n1->right = n2;
	n2->left = n1;
}

【C++】智能指针

1.7.1 weak_ptr简单实现

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

		weak_ptr(const SmartPtr<T>& p)
			: _ptr(p.get())
		{}

		weak_ptr<T>& operator=(const SmartPtr<T>& p)
		{
			_ptr = p.get();
			return *this;
		}
	private:
		T* _ptr;
	};
}

struct ListNode
{
	int val;
	yyh::weak_ptr<ListNode> left;
	yyh::weak_ptr<ListNode> right;

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

void test()
{
	SmartPtr<ListNode> n1(new ListNode);
	SmartPtr<ListNode> n2(new ListNode);
	n1->right = n2;
	n2->left = n1;
}

【C++】智能指针

三、定制删除器

普通的内置类型确实可以让智能指针自动释放,但是如果不是内置类型呢?

void test()
{
	std::shared_ptr<std::string> n(new std::string[10]);
}

这样直接会导致程序崩溃,因为delete类型不匹配([])。
【C++】智能指针
这里的del就是定制删除器,定制删除器就是一个可调用对象

template <class D>
class Delete
{
public:
	void operator()(const D* del)
	{
		delete[]del;
		cout << "delete[]del" << endl;
	}
};

void test()
{
	std::shared_ptr<std::string> n(new std::string[10], 
		Delete<std::string>());
}

【C++】智能指针

3.1 模拟实现定制删除器

这里我们不能像库里那样在构造的时候把参数传进去,因为要删除是在析构函数中,无法从构造函数传递到析构函数。所以我们可以给整个类增加一个模板参数增加一个新的成员变量

而如果要增加一个模板参数,我们为了让前面的代码运行,所以增加一个默认的删除器。

template <class T>
class DefultDelete// 默认
{
public:
	void operator()(T* ptr)
	{
		delete ptr;
	}
};

template <class D>
class Delete
{
public:
	void operator()(const D* del)
	{
		delete[]del;
		cout << "delete[]del" << endl;
	}
};

template <class T, class D = DefultDelete<T>>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
		, _pcnt(new int(1))
		, _pmutex(new std::mutex)
	{}

	SmartPtr(const SmartPtr<T>& sp)
		: _ptr(sp._ptr)
		, _pcnt(sp._pcnt)
		, _pmutex(sp._pmutex)
	{
		_pmutex->lock();
		(*_pcnt)++;
		_pmutex->unlock();
	}

	~SmartPtr()
	{
		_pmutex->lock();
		(*_pcnt)--;
		_pmutex->unlock();
		if (*_pcnt == 0)
		{
			//delete _ptr;
			_del(_ptr);
			delete _pcnt;
			delete _pmutex;
			//cout << _ptr << endl;
		}
	}

	SmartPtr<T>& operator=(const SmartPtr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			_pmutex->lock();
			(*_pcnt)--;
			_pmutex->unlock();
			if (*_pcnt == 0)
			{
				this->~SmartPtr();
				//delete _pcnt;
				//delete _ptr;
			}
			_ptr = sp._ptr;
			_pcnt = sp._pcnt;
			_pmutex = sp._pmutex;
			_pmutex->lock();
			(*_pcnt)++;
			_pmutex->unlock();
		}
		return *this;
	}

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

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

	// 获取引用计数值
	int use_count()
	{
		return *_pcnt;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcnt;
	std::mutex* _pmutex;
	D _del;
};

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

		weak_ptr(const SmartPtr<T>& p)
			: _ptr(p.get())
		{}

		weak_ptr<T>& operator=(const SmartPtr<T>& p)
		{
			_ptr = p.get();
			return *this;
		}
	private:
		T* _ptr;
	};
}

struct ListNode
{
	int val;
	yyh::weak_ptr<ListNode> left;
	yyh::weak_ptr<ListNode> right;

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


void test()
{
	SmartPtr<ListNode> n1(new ListNode);
	SmartPtr<ListNode, Delete<ListNode>> n2(new ListNode[5]);
}

【C++】智能指针