C++进阶---智能指针

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++进阶---智能指针相关的知识,希望对你有一定的参考价值。

智能指针

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这样不需要显式地释放资源,同时,采用这种方式,对象所需的资源在其生命期内始终保持有效


示例
在除0操作抛出异常的时候:如果在当前函数体内申请过资源,就需要进行一次try-catch以下并在catch内加上delete语句释放资源

double div()

	double a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("0");
	return a / b;

void f1()

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

	delete p;
	cout << p << endl;


int main()

	try
	
		f1();
	
	catch (exception& e)
	
		cout << e.what() << endl;
	
	return 0;

但如果将这个申请的资源交给智能指针管理,在生命周期结束后会自动调用它的析构函数释放资源达到效果:

double div()

	double a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("0");
	return a / b;

void f1()

	SmartPtr<int> sp(new int);
	*sp = 10;
	cout << *sp << endl;
	cout << div() << endl;


int main()

	try
	
		f1();
	
	catch (exception& e)
	
		cout << e.what() << endl;
	
	return 0;


smartptr:

template<class T>
class SmartPtr

public:
	// 1、RAII
	// 2、重载operator* 和 operator->  用起来像指针一样
	SmartPtr(T* ptr)
		:_ptr(ptr)
	
    ~SmartPtr()
	
		delete _ptr;
		cout <<"~smart"<< _ptr << endl;
	
	T& operator*()
	
		return *_ptr;
	
	T* operator->()
	
		return _ptr;
	
private:
	T* _ptr;
;

C++98的auto_ptr

上面的智能指针并没有解决所有问题,当尝试拷贝这个指针的时候,会出错(两次析构)


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

auto_ptr的使用

auto_ptr通过管理权转移的方式防止了两次析构的问题,图中可见当转移后原来的dt被置空了(delete空不会出错)

auto_ptr的问题:当对象拷贝或者赋值后,前面的对象悬空,当尝试修改dt时出错,这是一种不好的设计

auto_ptr模拟实现

auto_ptr的实现原理:管理权转移

template<class T>
	class auto_ptr
	
	public:
		// 1、RAII
		// 2、重载operator* 和 operator->  用起来像指针一样
		auto_ptr(T* ptr)
			:_ptr(ptr)
		
		// sp2(sp1) 管理权转移
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		
			sp._ptr = nullptr;
		
		// ap2 = ap3;
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		
			if (this != &ap)//防止自己赋给自己
			
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			
			return *this;//ap2
		
		~auto_ptr()
		
			if (_ptr)
			
				delete _ptr;
				cout << _ptr << endl;
			
		
		T& operator*()
		
			return *_ptr;
		
		T* operator->()
		
			return _ptr;
		
	private:
		T* _ptr;
	;

C++11的unique_ptr, shared_ptr, weak_ptr

C++11的unique_ptr, shared_ptr, weak_ptr 是由 boost库中的scope_ptr, shared_ptr, weak_ptr分别演变过来的

unique_ptr

unique_ptr的使用

针对不需要拷贝的场景:直接防拷贝

unique_ptr的模拟实现

模拟实现:

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

		// 防拷贝
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

		~unique_ptr()
		
			if (_ptr)
			
				delete _ptr;
				cout << _ptr << endl;
			
		

		T& operator*()
		
			return *_ptr;
		

		T* operator->()
		
			return _ptr;
		
	private:
		T* _ptr;
	;

注意:

// C++98防拷贝的方式:只声明不实现+声明成私有
unique_ptr(unique_ptr<T> const &);
unique_ptr & operator=(unique_ptr<T> const &);
// C++11防拷贝的方式:delete
unique_ptr(unique_ptr<T> const &) = delete;
unique_ptr & operator=(unique_ptr<T> const &) = delete;

shared_ptr

shared_ptr的使用

成员函数:

  • get:返回所管理资源的地址(指针),存储的指针指向 shared_ptr 对象解引用的对象,通常与其拥有的指针相同
  • use_count:返回所有的引用计数(包括自己)
  • unique:判断该shared_ptr是否是唯一管理资源的智能指针 (空指针非unique)
  • 重载*和->可以像指针一样使用

shared_ptr的模拟实现

采用计数的方式,拷贝一个count++,析构时–count,count等于0时表示是最后一个管理对象,就释放资源
注意:

  • 多个智能指针对象管理一个资源
    count不能为单个对象私有的,会出现不释放资源的问题,将其定义为static变量貌似可行(不能全局,全局变量有很多缺陷)
  • 多个智能指针管理多个资源:
    明显static变量就不行,会出现count为负的情况,这里应该使用int* 存储计数

operator=:对于将管理一个资源的sp对象赋值给另一个对象,应该注意讨论:
例如sp1=sp4:注意点看注释

shared_ptr<T>& operator=(const shared_ptr<T>& sp)

	//if (*this != sp) //未重载 != 不能比较
    //if (this != &sp) //原生指针可以直接比较
	if (_ptr != sp._ptr) //原生指针可以直接比较
	
		if (--(*_pCount) == 0)//对于任意条件--(*_pCount)均要执行
	    //若因为sp1改为管理sp4所管理的资源,若sp1是最后一个管理其原来管理的资源的sp对象,应释放该资源与计数
		
			delete _pCount;
			delete _ptr;
		
        //若剩余其他sp对象管理sp1资源,进行转移管理
		_ptr = sp._ptr;
		_pCount = sp._pCount;
		++(*_pCount);
		
	return *this;

现代写法:不需要考虑边界情况,同时可以减少不必要的操作(如赋值给自己,先-- 再++)

shared_ptr<T>& operator=(shared_ptr<T> sp)
//传值创建一个中间对象sp sp和sp4管理的是同一块资源(=左边this,()里为右边)
//例如之前sp4的_pCount为2 sp1的_pCount为1,此时sp的_pCount就为3 (拷贝构造的逻辑)
	swap(_ptr, sp._ptr); //sp和sp1交换
	swap(_pCount, sp._pCount); //sp和sp1交换
	return *this;
//sp出作用域自己销毁

总代码:

template<class T>
class shared_ptr

public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pCount(new int(1))
	
	shared_ptr(shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	
		++(*_pCount);
	
	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;
	
private:
	T* _ptr;
	int* _pCount;
;

线程安全

注意:

  • 访问资源的线程安全智能指针管不了,属于使用者管理
    智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
  • 智能指针对象拷贝析构的过程中引用计数的线程安全需要保证 (管理同一资源的应该是一把锁,拷贝构造时赋值)
    智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的


注意这里的ref(p)右值问题,参考:C++11 多线程(std::thread)详解


shared_ptr私有成员增加一个mutex* _pMtx;
在模拟shared_ptr时单独将原子操作分离出来

  • 增加计数:
void add_ref()

	_pMtx->lock();
	++(*_pCount);
	_pMtx->unlock();

  • 减少计数:(删除器见下面)
    注意:需要释放锁资源 (计数为0时,释放资源时)
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;
	

更改构造函数和拷贝构造的初始化列表 和 析构函数:
注意:std库中shared_ptr构造函数加了explicit关键字,防止传参的原生指针隐式类型转换为智能指针

explicit shared_ptr(T* ptr = nullptr)
	:_ptr(ptr)
	, _pCount(new int(1))
	, _pMtx(new mutex)


shared_ptr(shared_ptr<T, D>& sp)
	:_ptr(sp._ptr)
	, _pCount(sp._pCount)
	, _pMtx(sp._pMtx)

	add_ref();


~shared_ptr()

	release_ref();

循环引用

例如有一个类:包含一个该类的_prev智能指针一个该类的_next智能指针,主函数创建两个该类的智能指针,同时让他们链接起来:

看到并没有释放资源



当出作用域时b先销毁,countb减为1,a再销毁,counta减为1,均未调用析构函数,而a要销毁的条件是_next销毁,_next指向b,b要销毁的条件是_prev销毁,而_prev指向a,如此形成死循环,需要weak_ptr解决,见下

lambda+shared_ptr=内存泄漏

参考:Lambda + shared_ptr<> = memory leak
参考:lambda和shared_ptr的内存泄漏

定制删除器(MARK??lambda模板参数????)

std库中shared_ptr在构造函数的时候可以传入删除器

可以这样使用:

模拟实现

  • std的框架设计底层用一个类专门管理资源计数和释放,所以它可以再构造函数传参,把删除器类型传递给专门管理资源的这个类,但我们是一体化的,只能test::shared_ptr给删除器,析构函数才能拿到删除器
  • shared_ptr更改:定义一个默认删除器,作为D缺省值
template<class T>
struct DefaultDel

	void operator()(T* ptr)
	
		delete ptr;
	
;

template<class T, class D = DefaultDel<T>>
  • 拷贝构造与operator=的参数返回值更改:
shared_ptr(shared_ptr<T, D>& sp)
shared_ptr<T, D>& operator=(shared_ptr<T, D> sp)
  • release_ref内部更改为删除器删除:
D del;
del(_ptr); // 使用删除器释放即可

使用:

weak_ptr

weak_ptr的使用

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

  • weak_ptr的一个重要用途是通过lock获得this指针的shared_ptr,使对象自己能够生产shared_ptr来管理自己
  • weak_ptr只可以从一个shared_ptr或另一个 weak_ptr 对象来构造, 它的构造和析构不会引起引用记数的增加或减少

  • 重载了=号:注意右操作数可为shared_ptr
  • reset:对象变为空,就像默认构造的一样
  • use_count:同其他智能指针,注意这里不增加引用计数
  • expired: 检查是否为空或其所管理的资源是否还有其他shared_ptr在管理
    注意:expired的指针在locked时充当空的weak_ptr 对象,因此不能再用于恢复拥有的shared_ptr
    weak_ptr不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况,这时就不能使用weak_ptr直接访问对象
  • lock:如果未被expired,则返回带有由 weak_ptr 对象保留的信息的 shared_ptr

注意

  • weak_ptr并未重载*和->,不能像指针一样使用
  • weak_ptr在使用前需要检查合法性(调用expired()函数判断是否被destroy)

可以参考weak_ptr基本用法以及怎么解决循环引用
如何解决循环引用问题:以循环引用上图为例只需要将a或b的任意一个成员变量改为weak_ptr

weak_ptr的简单模拟

注意:weak_ptr不参与管理资源,是用来弥补shared_ptr的缺陷
先将weak_ptr设为shared_ptr的友元

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(weak_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;
	

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

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

以上是关于C++进阶---智能指针的主要内容,如果未能解决你的问题,请参考以下文章

正确地使用智能指针

c++动态内存管理与智能指针

C++中智能指针的原理使用实现

模拟实现c++标准库和boost库中的智能指针

C++ 智能指针 shared_ptr 分析

C++ --- C++智能指针