栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【C++】深入掌握智能指针

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【C++】深入掌握智能指针

智能指针介绍

学习C语言的人一直在接触裸指针,裸指针有很多不好用的地方(对于本菜鸡来讲),比如有以下原因:

  1. 资源忘记释放,导致资源泄露
  2. 同一资源释放多次,程序崩溃

智能指针的出现就很好的解决了上面的问题。
C++11库里面,提供了带引用计数的智能指针 和 不带引用计数的智能指针,这篇文章主要介绍他们的原理和应用和场景,包括auto_ptr, scoped_ptr, unique_ptr, shared_ptr, weak_ptr。

自己实现智能指针

为了更好理解智能指针的原理,我们最好自己实现一个简单地智能指针窥探一下基本原理。利用栈上对象出作用于会自动析构这一特点,把资源释放的代码放到析构函数中执行。


template 
class CSmartPtr
{
public:
    CSmartPtr(T *ptr = nullptr) : mptr(ptr) {}
    ~CSmartPtr() { delete mptr; }

private:
    T *mptr;
};

int main()
{
    CSmartPtr ptr(new int);
    
    return 0;
}

1. 智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化地址,析构函数中负责释放资源。
2. 利用栈上对象出作用于自动析构这一特点,保证了资源的释放。

有个面试问题问“能不能在对象定义智能指针?”,比如*CSmartPtr p = new CSmartPtr(new int);
p虽然是智能指针类型,但是实质上还是一个裸指针,因此p还是需要进行手动delete,又回到了裸指针面临的问题。

当然要做到智能指针和裸指针使用相似,需要重载 ** * 和 -> **函数

#include
using namespace std;

template 
class CSmartPtr
{
public:
    CSmartPtr(T *ptr = nullptr) : mptr(ptr) {}
    ~CSmartPtr() { delete mptr; }

    T &operator*() { return *mptr; }
    const T &operator*() const { return *mptr; }

    T &operator->() { return mptr; }
    const T &operator->() const { return mptr; }

private:
    T *mptr;
};

int main()
{
    CSmartPtr ptr(new int);
    *ptr = 20;
    cout << *ptr << endl;
    return 0;
}

上面的使用方法已经和普通的智能指针很想死了,但还是有着很大的问题,像下面:

int main()
{
    CSmartPtr ptr1(new int);
    CSmartPtr ptr2(ptr1);
    return 0;
}

这个一运行直接崩溃,问题出在默认的的构造函数做的是浅拷贝,两个指针都持有一个new int资源,ptr2先释放,ptr1再重复释放资源,因此导致崩溃。

现在看看C++库中提供的智能指针的怎么解决的。

不带引用计数的智能指针
  1. auto_ptr
    先看一下主要源码
template
	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智能指针持有资源,原来的auto_ptr都被赋nullptr了

int main()
{
    auto_ptr p1(new int);
    auto_ptr p2 = p1;
    *p1 = 10;
    return 0;
}

这里可以注意到p1已经是个空指针了,访问空指针的指向是一个很危险的操作。

上面的程序,如果不了解auto_ptr很容易出现严重的问题。
现在考虑一个问题,auto_ptr能不能使用在容器当中?

int main()
{
    vector> vec;
    vec.push_back(auto_ptr(new int(10)));
    vec.push_back(auto_ptr(new int(20)));
    vec.push_back(auto_ptr(new int(30)));
    //打印出10
    cout << *vec[0] << endl;
    vector> vec2 = vec;
    
}

因此,不要再容易中荣勇auto_ptr,C++最好不用auto_ptr,除非应用场景非常简单!
【总结】:auto_ptr处理了浅拷贝的问题,把前面的指针都置为nullptr

  1. scoped_ptr
    浏览下scoped_ptr源码
template class scoped_ptr // noncopyable
{
private:
    T * px;
	
	
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);
 
    typedef scoped_ptr 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 ) // never throws
    {
#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 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的智能指针对象就不支持这两种操作,从根本上杜绝浅拷贝的发生。因此,scoped_ptr也不能用在容器中,容器相互拷贝或者复制,就会引起scoped_ptr的拷贝构造和赋值,编译错误。
【总结】auto_ptr可以任意转移资源的所有权,而scoped_ptr不会转移所有权(拷贝构造和赋值被禁止)

  1. unique_ptr
template	// = 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);
		}

	
	void swap(unique_ptr& _Right) noexcept
		{	// swap elements
		_Swap_adl(this->_Myptr(), _Right._Myptr());
		_Swap_adl(this->get_deleter(), _Right.get_deleter());
		}

	
	~unique_ptr() noexcept
		{	// destroy the object
		if (get() != pointer())
			{
			this->get_deleter()(get());
			}
		}
	
	
	_NODISCARD pointer operator->() const noexcept
		{	// return pointer to class object
		return (this->_Myptr());
		}

	
	_NODISCARD pointer get() const noexcept
		{	// return pointer to object
		return (this->_Myptr());
		}

	
	explicit operator bool() const noexcept
		{	// test for non-null pointer
		return (get() != pointer());
		}
    
    
	pointer release() noexcept
		{	// yield ownership of pointer
		pointer _Ans = get();
		this->_Myptr() = pointer();
		return (_Ans);
		}

	
	void reset(pointer _Ptr = pointer()) noexcept
		{	// establish new pointer
		pointer _Old = get();
		this->_Myptr() = _Ptr;
		if (_Old != pointer())
			{
			this->get_deleter()(_Old);
			}
		}
	
	unique_ptr(const unique_ptr&) = delete;
	unique_ptr& operator=(const unique_ptr&) = delete;
	};

从上面看到,unique_ptr有一点和scoped_ptr做的一样,就是去掉了拷贝构造函数和operator=赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生。

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

//示例1
unique_ptr ptr(new int);
unique_ptr ptr2 = move(ptr);//使用了右值引用的拷贝构造
ptr2 = move(ptr);//使用了右值引用的operator=重载函数
}
//示例2
unique_ptr test_uniqueptr()
{
    unique_ptr ptr1(new int);
    return ptr1;
}
int main()
{
    //调用上面函数,return ptr1,此时调用右值引用的拷贝构造
    unique_ptr ptr = test_uniqueptr();//延长了ptr1的生命周期
    return 0;
}

unique_ptr还提供了reset重置资源,swap交换资源等函数。
可以看到,unique_ptr从名字就可以看出来,最终也是只能有一个该智能指针引用资源,因此建议在使用不带引用计数的智能指针时,可以优先选择unique_ptr智能指针。

带引用计数的智能指针shared_ptr 和 weak_ptr

当允许多个智能指针指向同一个资源的时候,每一个智能指针都会给资源的引用计数+1,析构-1,最后一个智能指针把资源的引用计数从1减到0,才真正释放了这个资源。

对整数的++或者–操作并不是线程安全的,因此share_ptr和weak_ptr底层的引用计数已经通过CAS操作。

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

因此,shared_ptr智能指针的资源引用计数器在内存的heap堆上。shared_ptr一般被称作强智能指针,weak_ptr被称作弱智能指针,它们有下边两个非常重要的应用场景需要注意。

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

看下面示例

#include 
#include 
using namespace std;

class B; // 前置声明类B
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr _ptrb; // 指向B对象的智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr _ptra; // 指向A对象的智能指针
};
int main()
{
	shared_ptr ptra(new A());// ptra指向A对象,A的引用计数为1
	shared_ptr 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;
}

可以看到,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 
#include 
using namespace std;

class B; // 前置声明类B
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	weak_ptr _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
    // 定义对象时,用强智能指针
	shared_ptr ptra(new A());// ptra指向A对象,A的引用计数为1
	shared_ptr 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

	
	return 0;
}
自定义删除器
~unique_ptr() noexcept
{	// destroy the object
if (get() != pointer())
	{
	this->get_deleter()(get()); // 这里获取底层的删除器,进行函数对象的调用
	}
}

从unique_ptr的析构函数可以看到,如果要实现一个自定义的删除器,实际上就是定义一个函数对象而已,示例代码如下:

class FileDeleter
{
public:
	// 删除器负责删除资源的函数
	void operator()(FILE *pf)
	{
		fclose(pf);
	}
};
int main()
{
    // 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
	unique_ptr filePtr(fopen("data.txt", "w"));
	return 0;
}

当然这种方式需要定义额外的函数对象类型,不推荐,可以用C++11提供的函数对象function和lambda表达式更好的处理自定义删除器,代码如下:

int main()
{
	// 自定义智能指针删除器,关闭文件资源
	unique_ptr> 
		filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});

	// 自定义智能指针删除器,释放数组资源
	unique_ptr>
		arrayPtr(new int[100], [](int *ptr)->void {delete[]ptr; });

	return 0;
}

https://blog.csdn.net/QIANGWEIYUAN/article/details/88562935?spm=1001.2014.3001.5501

我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号