深入理解C++中的智能指针

一,不带引用计数的智能指针

不带引用计数的智能指针有:auto_ptr, scoped_ptr, unique_ptr。其中:

auto_ptr:是C++11之前标准库就提供的

scoped_ptr, unique_ptr都是C++11提供的。

1,auto_ptr<T>

auto_ptr<T>不推荐我们去使用,因为及其容易使用错误。例如如下代码:

#include <iostream>
#include <memory>

using namespace std;


int main()
{
   auto_ptr<int> ptr1(new int);
   auto_ptr<int> ptr2(ptr1); 

   *ptr2 = 30;    
   cout<< *ptr1 <<endl;    //用户会以为通过ptr2改变了ptr1指向的值
}

在这段简单的代码中,由于是不带引用计数的,使用ptr1去初始化ptr2时,其实是先将ptr2指向ptr1的地方,然后会将ptr1置为nullptr。所以上面这段代码试图打印*ptr1是会发生错误的。ptr1已经被释放了。

下面是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的拷贝构造函数,
	_Right.release()函数中,把_Right的_Myptr
	赋为nullptr,也就是换成当前auto_ptr持有资源地址
	*/
	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的拷贝构造函数可以发现,auto_ptr解决浅拷贝问题的方式。该拷贝构造函数是调用release()成员方法,该成员方法是将原来的指针先赋给临时变量,再将其置为空,返回原来的指针赋予给构造对象。只有最后一个auto_ptr智能指针持有资源,原来的auto_ptr都被赋nullptr了。

2,scoped_ptr<T>

这个智能指针是C++11后提出的,但是使用的也特别少。主要是解决了auto_ptr可以随意转移资源这个能力。auto_ptr可以使用拷贝构造和拷贝赋值随意的转移资源的使用权,但是这样及其容易出现错误。为了解决这个问题。scoped_ptr<T>处理非常暴力,直接禁止了左值的拷贝构造和拷贝赋值。大概如下:

    //更加暴力----直接禁止拷贝构造和拷贝赋值
    scoped_ptr(const scoped_ptr<T>&) = delete;
    scoped_ptr<T>& operator=(const scoped_ptr<T>&) = delete;

下面是scoped_ptr<T>的主要源码,如下:

template<class T> class scoped_ptr // noncopyable
{
private:
    T * px;
	
	/*
	私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
	对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
	*/
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);
 
    typedef scoped_ptr<T> this_type;
		
	/*
	私有化逻辑比较运算符重载函数,不支持scoped_ptr的智能指针
	对象的比较操作
	*/
    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 ) // never throws
    {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_constructor_hook( px );
#endif
    }
 
#ifndef BOOST_NO_AUTO_PTR
	/*支持从auto_ptr构造一个scoped_ptr智能指针对象,
	但是auto_ptr因为调用release()函数,导致其内部指
	针为nullptr*/
    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() // never throws
    {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_destructor_hook( px );
#endif
        boost::checked_delete( px );
    }
};

scoped_ptr的源码可以看到,该智能指针由于私有化了拷贝构造函数operator=赋值函数,因此从根本上杜绝了智能指针浅拷贝的发生,所以scoped_ptr也是不能用在容器当中的,如果容器互相进行拷贝或者赋值,就会引起scoped_ptr对象的拷贝构造和赋值,这是不允许的,代码会提示编译错误。

auto_ptr和scoped_ptr这一点上的区别,有些资料上用所有权的概念来描述,道理是相同的,auto_ptr可以任意转移资源的所有权,而scoped_ptr不会转移所有权(因为拷贝构造和赋值被禁止了)。

3,unique_ptr<T>

    unique_ptr中是禁用了左值的引用的禁止拷贝构造和拷贝赋值。,但是提供了右值引用的拷贝构造和拷贝赋值

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

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

    unique_ptr(const unique_ptr<T>&&)  {}

    unique_ptr<T>& operator=(const unique_ptr<T>&&)  {}

unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作,或者在产生unique_ptr临时对象的地方,如把unique_ptr作为函数的返回值时,示例代码如下:

// 示例1
unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); // 使用了右值引用的拷贝构造
ptr2 = std::move(ptr); // 使用了右值引用的operator=赋值重载函数
// 示例2
unique_ptr<int> test_uniqueptr()
{
	unique_ptr<int> ptr1(new int);
	return ptr1;
}
int main()
{
	/*
	此处调用test_uniqueptr函数,在return ptr1代码
	处,调用右值引用的拷贝构造函数,由ptr1拷贝构造ptr
	*/
	unique_ptr<int> ptr = test_uniqueptr();
	return 0;
}

在实例2中test_uniqueptr()中创建了一个unique_ptr<int> ptr1,返回值是一个右值。主函数中的ptr会自动调用右值引用拷贝构造函数。当允许多个智能指针指向同一个资源的时候,

二,带引用计数的智能指针

带引用计数的智能指针有强智能指针:share_ptr,弱智能指针:weak_ptr。

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

对于整数的++或者- -操作,它并不是线程安全的操作,因此shared_ptr和weak_ptr底层的引用计数已经通过CAS操作,保证了引用计数加减的原子特性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针

智能指针的交叉引用(循环引用)问题

请看下面的这个代码示例:

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

class B; // 前置声明类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

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

代码打印结果:
A()
B()
2
2

可以看到,A和B对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr。
 

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

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

那么上面的代码怎么修改,也就是如何解决带引用计数的智能指针的交叉引用问题,代码如下:

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

class B; // 前置声明类B
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	weak_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
	// 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

	/*
	出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
	B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
	被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
	*/
	return 0;
}

代码打印如下:
A()
B()
1
1
~B()
~A()

上面例子中,在引用对象时,使用弱指针weak_ptr,这样析构时如果没有提升为shared_ptr,此时计数器为1,正常析构。

多线程访问共享对象问题

考虑如下代码:

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() {cout << "~A()" << endl;}
    void testA() { cout << "非常好用的方法!" << endl; }

private:
};

//子线程
void handler01(A *q)
{
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // q访问A对象的时候,需要侦测一下A对象是否存活
    q->testA();
}
//main()x线程
int main()
{
    A* p = new A();
    
    //开启一个线程
    thread t1(handler01, p);

    delete p;

    //等待线程结束
    t1.join();

    return 0;
}

打印结果:

A()
~A()
非常好用的方法!

这个是非常不合理的结果,因为对象A已经被析构掉了,结果子线程还能调用A对象的方法。

借助shared_ptr和weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。

解决方法代码如下:

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() {cout << "~A()" << endl;}
    void testA() { cout << "非常好用的方法!" << endl; }

private:
};

//子线程
void handler01(weak_ptr<A> pw)
{
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // q访问A对象的时候,需要侦测下A对象是否存活。
    shared_ptr<A> sp = pw.lock();
    if(sp != nullptr)
    {
        sp->testA();
    }
    else{
        cout << "A对象已经析构,不能够再访问!"<< endl;
    }
}
//main()x线程
int main()
{
    {
        shared_ptr<A> p(new A());
        
        //开启一个线程
        thread t1(handler01, weak_ptr<A>(p) );

        t1.detach();
    }
    
    std::this_thread::sleep_for(std::chrono::seconds(10));

    return 0;
}

打印结果:

A()
~A()
A对象已经析构,不能够再访问!

通过使用shared_ptr来指向对象A,然后子线程参数通过weak_ptr来接收对象A。这样当main()线程已经析构完对象A后,子线程中就肯定无法使用到对象A。保证了安全。如果一定要子线程中使用到对象A的方法,那么可以将子线程参数改成shared_ptr。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值