C++智能指针

C++11前的智能指针

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

		return a / b;
}

void f1()
{
	int* p = new int;
	cout << div() << endl;
	delete p;
	cout << p << endl;
}

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

  return 0;
}

如果发生了除0错误,在f1中没有到delete p就抛异常了,这时候只能在f1中也抛异常,这样才能让p释放。

void f1()
{
	int* p = new int;
	try
	{
		cout << div() << endl;
	}
	catch(...)
	{
		delete p;
		cout << p << endl;
		throw;
	}
	delete p;
	cout << p << endl;
}

但这样处理不太好看,C++中可以用智能指针来处理。

RAII: 是一种利用对象生命周期来控制程序资源。构造函数获取资源,析构函数释放资源。
一个基本的智能指针包含两点:1、RAII。2、重载operator* 和operator-> 用起来像指针一样。

实现一个基本的智能指针:

template<class T>
class SmartPtr
{
public:
  // 1、RAII
	// 2、重载operator* 和 operator->  用起来像指针一样
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

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

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

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

private:
	T* _ptr;
};

在这里插入图片描述注意:这里的(*spp).first就是spp->first,而它原本是spp->->first,中间还有一个pair,但这样可读性太差,编译器优化成只有一个箭头就可以。

在这里插入图片描述
这里拷贝了一个智能指针,或传参时拷贝了。多个智能指针对象管理一个资源,析构时会析构多次,而这个智能指针没有写拷贝构造函数,默认构造函数会完成内置类型(T* 指针)的浅拷贝,程序崩溃。

C++98 : 拷贝时,管理权转移(auto_ptr)
在这里插入图片描述永远只有一个对象管理资源。

赋值运算符重载也是如此,将管理权转移,把自己置空。
在这里插入图片描述但是auto_ptr的管理权转移会导致被拷贝的对象悬空,如果不小心访问了sp1就访问了空指针,是一种不好的设计。

boost库(第三方库):scoped_ptr,shared_ptr,weak_ptr
C++11吸收了boost库的精华:unique_ptr,shared_ptr,weak_ptr

unique_ptr

设计思路:有些场景下面,智能指针仅仅用于管理资源,不需要拷贝。

防拷贝、赋值:

unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

如果使用它拷贝就直接报错。

shared_ptr

在这里插入图片描述
如果count是在栈上,那么每个对象都有一个count,少一个智能指针对象就要把每个对象的count都减1才能一致,增加对象时也一样,不是只有一个count。我们要的是每个对象都使用同一个计数。

如果使用静态变量,当有多个资源对象时,每个资源对象应该有独立的计数,但静态count是所有智能指针对象共享了,也不行。
在这里插入图片描述所以我们给一个在堆上的计数变量int* count;
如果使用了拷贝构造那么智能指针对象使用的也是同一个count资源
在这里插入图片描述在这里插入图片描述
析构:
在这里插入图片描述设计思路:多个智能指针对象管理一块资源,这块资源对应一个引用计数,析构时减减计数,计数等于0时表示是最后一个管理对象,就释放资源。

赋值重载:
要注意判断是不是自己给自己赋值,如果是以下的情况,没有判断是不是自己给自己赋值,计数减为0将释放sp6,count将会被置成随机值。
在这里插入图片描述

//sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	//if(*this != sp): 对象没有默认重载!=符号
	//if(this != &sp):如果其中的指针_ptr指向同一块资源,但不是同一个对象,那--又++就白操作了,也不好
	if(_ptr != sp._ptr)
	{
		if(--(*pCount) == 0)
		{
			delete _pCount;
			delete _ptr;
		}

		_ptr = sp._ptr;
		_pCount = sp._pCount;
		++(*pCount);
	}
}

现代写法:sp1 = sp4,先用sp4传值拷贝构造一个sp,计数++,sp的计数换给了sp1,sp1原来的计数换给了sp,sp是个局部对象,函数结束调用析构函数释放,减少了一个计数。
在这里插入图片描述

线程安全问题

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

	// sp2(sp1)
	shared_ptr(shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	{
		++(*_pCount);
	}
	
	// sp1 = sp4 现代写法
	shared_ptr<T>& operator=(shared_ptr<T> sp)
	{
		swap(_ptr, sp._ptr);
		swap(_pCount, sp._pCount);

		return *this;
	}

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

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

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

	int use_count()
	{
		return *_pCount;
	}

private:
	T* _ptr;
	int* _pCount;
}; 

引用计数是智能指针的内部实现,但是资源的线程安全不是智能指针能管的。智能指针对象拷贝析构过程中引用计数的线程安全需要保障。以上代码就会引发线程安全问题。

struct Date
{
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

void SharePtrFunc(my::shared_ptr<Date>& sp, size_t n)
{
	for(size_t i = 0; i < n; i++)
	{
		//这里智能指针拷贝会++计数,析构会--计数,这里是线程安全的。
		my::shared_ptr<Date> copy(sp);

		//这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,不一定是加了2n
		copy->_year++;
		copy->_month++;
		copy->_day++;
	}
}

void test_shared_ptr_thread_salf()
{
	my::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	cout << p.use_count() << endl;

	const size_t n = 10000;
	thread t1(SharePtrFunc, p, n);
	thread t2(SharePtrFunc, p, n);

	t1.join();
	t2.join();

	cout << p.get() << endl;
	cout << p.use_count() << endl;

	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
}

我们可以专门抽象出来控制pCount

void add_ref()
{
	_pMtx->lock();
	++(*_pCount);
	_pMtx->unlock();
}

void release_ref()
{
	bool flag = false;
	_pMtx->lock();
	if (--(*_pCount) == 0 && _ptr)
	{
		D del;
		del(_ptr); // 使用删除器释放即可

		//delete _ptr;
		delete _pCount;
		flag = true;
		cout << "释放资源:" << _ptr << endl;
	}
	_pMtx->unlock();

	if (flag == true)
	{
		delete _pMtx;
	}
}

完整代码

template<class T, class D = DefaultDel<T>>
class shared_ptr
{
	template<class T>
	friend class weak_ptr;
public:
	explicit shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pCount(new int(1))
		, _pMtx(new mutex)
	{}

	void add_ref()
	{
		_pMtx->lock();
		++(*_pCount);
		_pMtx->unlock();
	}

	void release_ref()
	{
		bool flag = false;
		_pMtx->lock();
		if (--(*_pCount) == 0 && _ptr)
		{
			D del;
			del(_ptr); // 使用删除器释放即可

			//delete _ptr;
			delete _pCount;
			flag = true;
			cout << "释放资源:" << _ptr << endl;
		}
		_pMtx->unlock();

		if (flag == true)
		{
			delete _pMtx;
		}
	}

	// sp2(sp1)
	shared_ptr(shared_ptr<T, D>& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
		, _pMtx(sp._pMtx)
	{
		add_ref();
	}

	// sp1 = sp4
	shared_ptr<T, D>& operator=(shared_ptr<T, D> sp)
	{
		swap(_ptr, sp._ptr);
		swap(_pCount, sp._pCount);

		return *this;
	}

	~shared_ptr()
	{
		release_ref();
	}

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

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

	T* get()
	{
		return _ptr;
	}

	int use_count()
	{
		return *_pCount;
	}

private:
	T* _ptr;
	int* _pCount;
	mutex* _pMtx;
};

循环引用

在这里插入图片描述
n2要销毁,它的引用计数要到0,而它被n1的_next管理,n1的_next要销毁就要让n1销毁,而n1被n2的_prev管理,n1销毁就要让n2的_prev销毁,而n2的_prev要销毁就要让n2销毁,这样两个节点都无法销毁,形成循环引用。

weak_ptr

它可以用一个shared_ptr去构造它。

在会产生循环引用的位置,把shared_ptr换成weak_ptr。

weak_ptr不是一个RAII智能指针,它不参与资源的管理,它是专门用来解决循环引用的问题的。可以把一个shared_ptr用来初始化一个weak_ptr,但是weak_ptr不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

struct ListNode
{
	 int _data;
	 weak_ptr<ListNode> _prev;
	 weak_ptr<ListNode> _next;
	 ~ListNode(){ cout << "~ListNode()" << endl; }
};

int main()
{
 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(shared_ptr)赋值给node1->_next(weak_ptr),不会增加node2(shared_ptr)的引用计数
 node2->_prev = node1;
 
 cout << node1.use_count() << endl;
 cout << node2.use_count() << endl;
 return 0;
}

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

简单模拟实现:

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

	weak_ptr(shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	{}

	weak_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		_pCount = sp._pCount;

		return *this;
	}
private:
	T* _ptr;
	int* _pCount;
};

定制删除器

前面说的指针都是new一个出来的,但如果指针是其他方式生成的呢?

std::shared_ptr<pair<int, int>> sp1(new pair<int, int>[10]);
std::shared_ptr<string> sp2(new string[10]);
std::shared_ptr<string> sp3((string*)malloc(sizeof(string)));

此时就要用到删除器进行删除,如

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

void test_deletor()
{
	std::shared_ptr<string> sp(new string[10], DeleteArray<string>());
}

定制删除器最重要的是在析构函数时调用

我们模拟实现删除器的传递位置跟std的不太一样
std的框架设计底层用一个类专门管理资源技术和释放,所以它可以在构造函数传参,把删除器类型传递给专门管理资源引用计数的这个类。
我们是一体化的,只能shared_ptr实例化给删除器,析构函数才能拿到删除器。

template<class T>
struct DefaultDel
{
	void operator()(T* ptr)
	{
		delete ptr;
	}
};

template<class T, class D = DefaultDel<T>>
class shared_ptr{...}void test_deletor()
{
	bit::shared_ptr<string> sp1(new string);

	bit::shared_ptr<string, DeleteArray<string>> sp2(new string[10]);

	auto ffree = [](string* ptr){free(ptr); };
	bit::shared_ptr<string, decltype(ffree)> sp4((string*)malloc(sizeof(string)));

	auto ffclose = [](FILE* ptr){fclose(ptr); };
	bit::shared_ptr<FILE, decltype(ffclose)> sp5(fopen("test.cpp", "r"));
}

总结:

  1. 不要使用auto_ptr,他是早期的设计缺陷。
  2. 不拷贝的场景建议使用unique_ptr。
  3. 如果要拷贝或者定制删除器,建议使用shared_ptr。不过要注意循环引用的问题。

智能指针的一些常见问题:

  1. 什么是RAII
  2. 了解智能指针的发展历史
  3. 模拟实现一个简洁的智能指针 – unique_ptr
  4. 什么是循环引用?如何解决?解决的原理是什么

相关知识:

内存泄漏,资源泄漏:一个内存或资源,不使用了还没释放,就会导致资源泄漏/内存泄漏。

内存泄漏危害:1、进程僵尸了,资源无法释放。2、长期运行的服务器一直泄漏。

如何避免内存泄漏:
1、事前预防:养成良好的编码规范;RAII思想或智能指针来管理资源。
2、事后查错:内存泄漏工具。Linux:valgrind

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值