c++智能指针再探

201 阅读4分钟

c++智能指针初探中遗留了一个多次释放同一资源的问题。假设,如果让我们来处理这个问题我们会怎么做?先来看看系统提供给我们的几种智能指针分别有什么功能和用来解决什么问题。

auto_ptr

auto_ptr的部分源代码如下:

template<class _Ty>
	class auto_ptr {	
        // wrap an object pointer to ensure destruction
public:
	typedef _Ty element_type;

	explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
		: _Myptr(_Ptr) {	
                // construct from object pointer
		}
	auto_ptr(auto_ptr& _Right) noexcept
		: _Myptr(_Right.release()) {	
                // construct by assuming pointer from _Right auto_ptr
		}
		
	_Ty * release() noexcept {	
                // return wrapped pointer and give up ownership
		_Ty * _Tmp = _Myptr;
		_Myptr = nullptr;
		return (_Tmp);
		}
private:
	_Ty * _Myptr;	// the wrapped object pointer
};

auto_ptr的重点在它的拷贝构造函数,_Right.release()函数中,把_Right的_Myptr 赋为nullptr,也就是换成当前auto_ptr持有资源地址,意味着当auto_ptr包装的资源在执行赋值或拷贝操作后,原来的auto_ptr修饰的变量将不在持有资源的引用:

int main()
{
	auto_ptr<int> p1(new int);
	/*
	经过拷贝构造,p2指向了new int资源,
	p1现在为nullptr了,如果使用p1,相当于
	访问空指针了,很危险
	*/
	auto_ptr<int> p2 = p1;
	*p1 = 10;
	return 0;
}

tip:能不用auto_ptr就不用!

scope_ptr

scope_ptr的部分源代码如下:

template<class T> class scoped_ptr // noncopyable
{
private:
    T * px;
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);
    typedef scoped_ptr<T> this_type;
    void operator==( scoped_ptr const& ) const;
    void operator!=( scoped_ptr const& ) const;
 
public:
    typedef T element_type;
    explicit scoped_ptr( T * p = 0 ): px( p ) {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_constructor_hook( px );
#endif
    }
 
#ifndef BOOST_NO_AUTO_PTR
    explicit scoped_ptr( std::auto_ptr<T> p ) BOOST_NOEXCEPT : px( p.release() ) {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_constructor_hook( px );
#endif
    }
 
#endif
    ~scoped_ptr() {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_destructor_hook( px );
#endif
        boost::checked_delete( px );
    }
};

从上面代码中可以看出两点:

  1. scope_ptr 私有化了拷贝构造函数和赋值函数,不支持这两种操作,从根本上杜绝浅拷贝的发生
  2. 私有化逻辑比较运算符

从scope_ptr的源代码中可以看出,它相比较auto_ptr而言就是私有化了拷贝构造和赋值函数,也就意味着砍掉一些功能来避免一些的错误,如果在编码过程中不想资源的所有权被转移,那么就可以使用scope_ptr。

unique_ptr

unique_ptr的源代码比较长,在这就不贴出来了,有兴趣的可以去看看,它主要做了三件事:

  1. 直接去掉了拷贝构造函数和赋值函数,同样是为了根治浅拷贝。
  2. 提供了带右值引用参数的拷贝构造和赋值:
template<class _Ty,
	class _Dx>	// = default_delete<_Ty>
	class unique_ptr
		: public _Unique_ptr_base<_Ty, _Dx> {	// non-copyable pointer to an object
public:
	typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
	typedef typename _Mybase::pointer pointer;
	typedef _Ty element_type;
	typedef _Dx deleter_type;

	unique_ptr(unique_ptr&& _Right) noexcept
		: _Mybase(_Right.release(),
			_STD forward<_Dx>(_Right.get_deleter())) {	// construct by moving _Right
		}
	
	unique_ptr& operator=(unique_ptr&& _Right) noexcept {	// assign by moving _Right
		if (this != _STD addressof(_Right)) {	// different, do the move
			reset(_Right.release());
			this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
			}
		return (*this);
		}

}

  1. 提供了reset重置资源,swap交换资源等函数: 把unique_ptr原来的旧资源释放,重置新的资源_Ptr
	void reset(pointer _Ptr = pointer()) noexcept {	
		pointer _Old = get();
		this->_Myptr() = _Ptr;
		if (_Old != pointer())
			{
			this->get_deleter()(_Old);
			}
		}

unique_ptr相比较auto_ptr和scope_ptr而言,没有砍掉功能,也避免了野指针释放的问题,,因此建议在使用不带引用计数的智能指针时,可以优先选择unique_ptr智能指针。

上述三种智能指针都是自己玩,并且一味的“封锁”自己,避免资源的共享造成的问题。因为在c++智能指针初探中,我们就发现了,其实资源共享是会出现资源释放时野指针的问题。但是资源共享是不可规避的,所以,c++大神们就想到了通过引用计数的办法来解决这个问题:

每一个智能指针都会给资源的引用计数加1,当一个智能指针析构时,同样会使资源的引用计数减1,这样最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了

c++库提供了两种采用了引用计数的智能指针:shared_ptr和weak_ptr.

shared_ptr

在shared_ptr的源代码中,维护了一个资源引用计数器:_Ptr指向资源,_Rep指向资源引用的对象。

private:
	element_type * _Ptr{nullptr};
	_Ref_count_base * _Rep{nullptr};

从目前来看shared_ptr的算是一个完美的智能指针了,但是就算如此我们也不能无脑用它,因为,它存在两个问题:

线程安全性

在shared_ptr中,为了避免线程安全性问题,它底层采用CAS操作(有兴趣的可以研究下),所以它是线程安全的。

循环引用

但是循环引用不可避免,来看如下代码:

#include <iostream>
#include <memory>
using namespace std;

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr<B> _ptrb; // 指向B对象的智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr<A> _ptra; // 指向A对象的智能指针
};
int main()
{
	shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
	shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
	ptra->_ptrb = ptrb;// A对象的成员变量_ptrb也指向B对象,B的引用计数为2
	ptrb->_ptra = ptra;// B对象的成员变量_ptra也指向A对象,A的引用计数为2

	cout << ptra.use_count() << endl; // 打印A的引用计数结果:2
	cout << ptrb.use_count() << endl; // 打印B的引用计数结果:2

	return 0;
}

出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和B对象的引用计数从2减到1,达不到释放A和B的条件(释放的条件是A和B的引用计数为0),因此造成两个new出来的A和B对象无法释放,导致内存泄露,这个问题就是“强智能指针的交叉引用(循环引用)问题”,为了解决这个问题,就要使用weak_ptr.

weak_ptr

弱智能指针weak_ptr区别于shared_ptr之处在于:

  • weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在
  • weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  • weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源

在shared_ptr和weak_ptr使用如果选择时,需要记住如下口诀: 定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr。 上述代码修改如下就好了:

#include <iostream>
#include <memory>
using namespace std;

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr<B> _ptrb; 
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	weak_ptr<A> _ptra;
};
int main()
{
    // 定义对象时,用强智能指针
	shared_ptr<A> ptra(new A());
	shared_ptr<B> ptrb(new B());
	// A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
	ptra->_ptrb = ptrb;
	// B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
	ptrb->_ptra = ptra;

	cout << ptra.use_count() << endl; // 打印结果:1
	cout << ptrb.use_count() << endl; // 打印结果:1

	return 0;
}

智能指针的学习就到这里,如果问题,欢迎留言评论!