详解C++智能指针及交叉引用问题

为什么要使用智能指针

在简单的程序中,我们不大可能忘记释放 new 出来的指针,但是随着程序规模的增大,我们忘了 delete 的概率也随之增大。在 C++ 中 new 出来的指针,赋值意味着引用的传递,当赋值运算符同时展现出“值拷贝”和“引用传递”两种截然不同的语义时,就很容易导致“内存泄漏”。

手动管理内存带来的更严重的问题是,内存究竟要由谁来分配和释放呢?指针的赋值将同一对象的引用散播到程序各处,但是该对象的释放却只能发生一次。当在代码中用完了一个资源指针,该不该释放 delete 掉它?这个资源极有可能同时被多个对象拥有着,而这些对象中的任何一个都有可能在之后使用该资源,其余指向这个对象的指针就变成了“野指针”;那如果不 delete 呢?也许你就是这个资源指针的唯一使用者,如果你用完不 delete,内存就泄漏了。

资源的拥有者是系统,当我们需要时便向系统申请资源,当我们不需要时就让系统自己收回去(Garbage Collection)。当我们自己处理的时候,就容易出现各种各样的问题。

此时我们的需求就是用户可以不关注资源的释放,一个智能指针管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异常,资源在到期的情况下,一定会进行释放。

智能指针定义及原理

定义:所谓智能指针就是智能/自动化的管理指针所指向的动态资源的释放。它是一个类,有类似指针的功能。 无论如何保证资源一定会释放(用户不用自己来释放资源)

原理:因为我们可以平时访问到的内存资源是数据段 (.data) 堆(heap)  栈(stack),智能指针的原理就是利用栈上的对象出作用域自动析构的特点,把资源释放的代码 放在智能指针的析构函数里面。

C++11库里面,提供了带引用计数的智能指针和不带引用计数的智能指针,这篇文章主要介绍它们的原理和应用场景,包括auto_ptr,scoped_ptr,unique_ptr,shared_ptr,weak_ptr

自定义智能指针

本质上就是将一个指针封装成一个类

template<typename T>
class CSmartPtr
{
public:
    CSmartPtr(T *ptr = nullptr)
		:mptr(ptr){}

	~CSmartPtr()
	{
		delete mptr;
	}
private:
    T *mptr;
};
int main()
{
   CSmartPtr<int>ptr;
    /*
    \其他代码
    */

    return 0;
}

上面一段代码就是将一个指针封装成一个类,然后利用类的构造函数对资源进行初始化,当指针的生命周期完成后,利用类的析构函数对资源进行释放,因此智能指针都是在栈上进行定义的。

如果我们在堆上定义一个只能指针会怎么样?比如:CSmartPtr *p = new CSmartPtr(new int)

因为我们是在堆上进行定义智能指针的,因此我们需要手动delete才能够调用其析构函数对资源进行释放,这就回到了使用裸指针的问题上了,相当于这个智能指针还是一个裸指针。

自定义完整功能的一个智能指针

template<typename T>
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;

};

但是这里有一个问题当我们去使用智能指针时候下面情况会崩掉

CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);

为什么会这样?因为我们调用的是默认的拷贝构造函数做的是一个浅拷贝,使得ptr1与ptr2都指向了这一块外部资源,最后当ptr2析构完成后,这一块资源已经被释放,ptr1已经变成一个野指针了,最后delete野指针程序就会崩掉

此时我们就需要解决这样的问题:

1.解决智能指针的浅拷贝问题

2.当多个指针使用一块资源的时候我们需要只释放一次资源

C++11在这里提供了两类指针:不在引用参数的智能指针(一个指针指向一块资源),带引用计数的智能指针(多个指针指向一块资源)

不带引用计数的智能指针auto_ptr,scoped_ptr ,unique_ptr 

1.auto_ptr

// 自定义的智能指针
template<typename T>
class CSmartPtr
{
public:
	// 构造函数
	CSmartPtr(T *ptr = nullptr):mptr(ptr){}
	~CSmartPtr() { delete mptr; }

	// auto_ptr的实现方式
	CSmartPtr(CSmartPtr &src)
	{
	    mptr = src.release();
	}

	T* release()
	{
	    T *ptmp = mptr;
	    mptr = nullptr;
	    return ptmp;
	}

	// 指针常用运算符重载函数
	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }
	T* operator->() { return mptr; }

private:
	T *mptr;
};

这里的拷贝构造函数直接调用了release函数。release函数定义一个ptmp指针也指向mptr指向的资源,然后将mptr置为nullptr,因此拷贝构造函数就是让最后一个auto_ptr智能指针持有资源,原来的auto_ptr都被赋nullptr了

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

因此当我们对智能指针进行拷贝构造的时候,只能使用最后一个智能指针,原来的指针都不能使用。

int main()
{
	vector<auto_ptr<int>> vec;
	vec.push_back(auto_ptr<int>(new int(10)));
	vec.push_back(auto_ptr<int>(new int(20)));
	vec.push_back(auto_ptr<int>(new int(30)));
	// 这里可以打印出10
	cout << *vec[0] << endl;
	vector<auto_ptr<int>> vec2 = vec;
	/* 这里由于上面做了vector容器的拷贝,相当于容器中
	的每一个元素都进行了拷贝构造,原来vec中的智能指针
	全部为nullptr了,再次访问就成访问空指针了,程序崩溃
	*/
	cout << *vec[0] << endl;
	return 0;
}

所以不要在容器中使用auto_ptr,C++建议最好不要使用auto_ptr,除非应用场景非常简单。

总而言之,auto_ptr智能指针不带引用计数,那么它处理浅拷贝的问题,是直接把前面的auto_ptr都置为nullptr,只让最后一个auto_ptr持有资源。

2.scoped_ptr

scoped_ptr的实现原理是防止对象间的拷贝与赋值。具体实现是将拷贝构造函数和赋值运算符重载函数设置为保护或私有,并且只声明不实现,并将标志设置为保护或私有,防止他人在类外拷贝,简单粗暴,但是也提高了代码的安全性。

template<typename T>
class  ScopedPtr
{
public:
    ScopedPtr(T* ptr = NULL) 
        :_ptr(ptr)
    {}
 
    T& operator*()
    {
        return *_ptr;
    }
 
    T* operator->()
    {
        return _ptr;
    }
 
    T* GetStr()
    {
        return _ptr;
    }
    //析构函数
    ~ScopedPtr()
    {
        if (_ptr!=NULL)
        {
            delete _ptr;
        }
    }
protected:
    //防拷贝
    ScopedPtr(ScopedPtr<T>& ap);
    ScopedPtr& operator=(ScopedPtr<T>& ap);
 
    T* _ptr;
};

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

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

3.unique_ptr

unique_ptr 独占所指向的对象, 同一时刻只能有一个 unique_ptr 指向给定对象(通过禁止拷贝语义, 只有移动语义来实现),unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

在这里直接附上unique_ptr源码

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
		}
	
	/*提供了右值引用的operator=赋值重载函数*/
	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);
		}

	/*
	交换两个unique_ptr智能指针对象的底层指针
	和删除器
	*/
	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());
			}
		}
	
	/*unique_ptr提供->运算符的重载函数*/
	_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());
		}

	/*提供bool类型的重载,使unique_ptr对象可以
	直接使用在逻辑语句当中,比如if,for,while等*/
	explicit operator bool() const noexcept
		{	// test for non-null pointer
		return (get() != pointer());
		}
    
    /*功能和auto_ptr的release函数功能相同,最终也是只有一个unique_ptr指针指向资源*/
	pointer release() noexcept
		{	// yield ownership of pointer
		pointer _Ans = get();
		this->_Myptr() = pointer();
		return (_Ans);
		}

	/*把unique_ptr原来的旧资源释放,重置新的资源_Ptr*/
	void reset(pointer _Ptr = pointer()) noexcept
		{	// establish new pointer
		pointer _Old = get();
		this->_Myptr() = _Ptr;
		if (_Old != pointer())
			{
			this->get_deleter()(_Old);
			}
		}
	/*
	删除了unique_ptr的拷贝构造和operator=赋值函数,
	因此不能做unique_ptr智能指针对象的拷贝构造和
	赋值,防止浅拷贝的发生
	*/
	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<int> ptr(new int);
unique_ptr<int> ptr2 = move(ptr); // 使用了右值引用的拷贝构造
ptr2 = 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;
}

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

总结三者之间的区别

scoped_ptr 与 auto_ptr不同的是:auto_ptr可以转让资源所有权,在转让的时候,上一个指针失去使用权。而scoped_ptr则是不允许转让使用权。

unique_ptr 与 auto_ptr 同样都可以改变资源所有权,通过release函数实现,因此只有最后一个智能指针拥有资源。

unique_ptr 与 scoped_ptr 同样都是禁止了拷贝构造和赋值操作,防止了浅拷贝的发生。

unique_ptr提供了带右值引用参数的拷贝构造和赋值,因此unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作

在产生unique_ptr临时对象的地方,如把unique_ptr作为函数的返回值时,我们可以通过编译器隐式调用带右值引用参数的拷贝构造和赋值的方式,生成新对象。

总而言之,我们可以看出unique_str的功能比auto_ptr更为强大,它支持托管堆上分配的数组,支持定制deleter,并且可以通过move语意使unique_ptr对象与容器兼容,但仍然有一些不足,比如重复释放,使用move语意之后源对象失去了对指针的管理权,再次使用会出现undefine行为。要避免这些情况,除了使用时要注意之外,最好的办法还是使用带有引用计数功能的智能指针。
 

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

什么是带引用计数的智能指针

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

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

简单实现一个带引用计数的智能指针

class RefCnt
{
public:
	// 给资源添加引用计数
	void add(void *ptr)
	{
		auto it = mrefCntMap.find(ptr);
		if (it != mrefCntMap.end())
		{
			it->second++;
		}
		else
		{
			// make_pair(ptr,1);
			mrefCntMap.insert({ ptr, 1 });
		}
	}
	// 给资源减少引用计数
	void del(void *ptr)
	{
		auto it = mrefCntMap.find(ptr);
		if (it != mrefCntMap.end())
		{
			if (--(it->second) == 0)
			{
				mrefCntMap.erase(it);
			}
		}
	}
	// 返回指定资源的引用计数
	int get(void *ptr)
	{
		auto it = mrefCntMap.find(ptr);
		if (it != mrefCntMap.end())
		{
			return it->second;
		}
		return -1;
	}
private:
	// 一个资源void* 《=》 计数器 int
	unordered_map<void*, int> mrefCntMap;
};

// 自定义的智能指针
template<typename T>
class CSmartPtr
{
public:
	// 构造函数
	CSmartPtr(T *ptr = nullptr)
		:mptr(ptr)
	{
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}
	}
	~CSmartPtr()
	{
		mrefCnt.del(mptr);
		if (0 == mrefCnt.get(mptr))
			delete mptr;
	}
	CSmartPtr(const CSmartPtr<T> &src)
		:mptr(src.mptr)
	{
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}
	}
	CSmartPtr<T>& operator=(const CSmartPtr &src)
	{
		if (this == &src)
			return *this;

		mrefCnt.del(mptr);
		if (0 == mrefCnt.get(mptr))
			delete mptr;

		mptr = src.mptr;
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}

		return *this;
	}


	// 指针常用运算符重载函数
	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }
	T* operator->() { return mptr; }

private:
	T *mptr;
	static RefCnt mrefCnt;
};
template<typename T>
RefCnt CSmartPtr<T>::mrefCnt;

int main()
{
	CSmartPtr<int> ptr1(new int);
	CSmartPtr<int> ptr2 = ptr1;

	*ptr2 = 20;
	*ptr1 = 30;

	CSmartPtr<int> ptr3(new int);
	ptr2 = ptr3;

	auto_ptr<int> p;

	vec.push_back(auto_ptr<int>(new int));

	vector<auto_ptr<int>> vec1 = vec;
	*/
	return 0;
}

引用计数器是在哪里存放的?

private:
	/*
	下面这两个是shared_ptr的成员变量,_Ptr是指向内存资源的指针,_Rep是
	指向new出来的计数器对象的指针,该计数器对象包含了资源的一个引用计数器count
	*/
	element_type * _Ptr{nullptr};
	_Ref_count_base * _Rep{nullptr};

shared_ptr智能指针的资源引用计数器在内存的heap堆上。shared_ptr一般被称作强智能指针,weak_ptr被称作弱智能指针

shared_ptr  引起资源引用计数的改变 , weak_ptr 不会引起资源引用计数的改变。

这两个指针的应用场景是什么?

智能指针的交叉引用问题

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr<B> ptrb;
	void test() { cout << "test" << endl; }
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr<A> ptra;
};
int main()
{
	shared_ptr<A>pa(new A());
	shared_ptr<B>pb(new B());
	pa->ptrb = pb;
	pb->ptra = pa;
	getchar();
	return 0;
}

由于都是强智能指针,因此当每次进行对资源进行一次指向都会使得引用计数器++操作,对于A来说此时的引用计数为2,对于B来说也是2,因此当程序结束时,引用计数会进行--操作,此时引用计数为1,不符合析构条件(引用计数为0),不能够析构对象释放资源,因此会导致内存泄漏。

因此请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr

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

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

修改代码解决以上问题

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;
}

因此,解决智能指针的交叉引用问题,我们可以定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr

上面代码还有一种改法就是将其中一个改为弱指针,另外一个改成强指针也可以解决该问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值