[C++] 共享智能指针探究

 

概念引入

        在C++应用中,野指针是一件非常令人头痛的事情。它的发生往往是因为引用了已经被删除的指针。也就是像这样:

int* a = new int(1);
delete a;

cout << *a << endl;

        当然,上例的错误非常明显,一般除了笔误,我们很少遇到这样的问题。更为常见的是,某个类A需要以指针的形式引用某个对象b,而该对象b是在其它地方分配和管理的:

class A
{
private:
    B* b = nullptr;
public:
    void SetB(B* _b) { b = _b; }
};

        当我们写下这样的代码时,我们实际上已经陷入了野指针的风险——在代码运行的过程中,类A无法确保它访问到的b是否是一个有效的对象。在复杂的程序运行环境中,它随时可能在别处delete,此时继续访问b,崩溃就发生了。

        对于有较好编程习惯的人而言,在别处执行delete之后,会将指针指向nullptr——但即便如此,类A中的b对象依然指向原来已经被释放的位置,因为我们只置空了别处的指针,而没有置空指针b。所以我们无法通过if(b)之类的操作来判断指针b的有效性。

 

        但是,我们的需求却让我们不得不写出如上的代码,因为我们确实需要在类A运行的这段时间内,使用对象b来完成一些操作。在不同类之间共享对象,是一种非常常见的操作。而另外一处在对对象b执行delete操作时,却很难知道还有谁依赖这一对象。

       即便它得以知道此时有一处正在引用这一对象,不能执行delete操作,我们依然面临着一个新的问题,也就是在A不再需要对象b时,它需要完成之前未能完成的工作——也就是把之前本应delete而没有delete的对象b释放。

        为了很好地解决以上问题,我们考虑为每个指针引入引用计数这一概念。它的思想非常简单,即记录了当前这一指针被引用的次数,在引用次数为0时,说明没有人再需要这个对象了,它会自动销毁。

        例如,在我们之前的例子中:

        (1) 对象b初始化,b的引用计数为1

        (2) 类A的实例调用setB(),b的引用计数为2

        (3) 类A外某处释放b,b的引用计数为1(此时b尚未销毁,类A的实例依然可以访问到b)

        (4) 类A处释放b,b的引用计数为0,b自动销毁

      此时,可能存在的“崩溃”就不会再发生了,我们解决了我们一开始遇到的问题。

实现细节

        “智能”指针并不意味着这个指针已经非常“聪明”,导致我们可以随时所欲地使用指针,而无需在意内存的使用情况。相反,我们在使用智能指针的时候,反而需要非常清楚我们当前这一操作会对引用计数带来什么影响;并且,在我们不再需要使用某个指针时,我们依然可能需要做相关的销毁工作。也就是说,智能指针的引入并不是为了让我们用的“爽”,而更多地是为了避免野指针满天飞等情况。

       那么,我们就很必要了解智能指针的一些实现细节。这些细节包括了,如何完成引用计数的增加、减少以及在为0时自动销毁,在何种操作下需要增加引用计数,何种操作下又需要减少引用计数。

      (1) 引用计数的设计

       首先,根据前面的描述,引用计数应该是实际对象(即b)的属性,所以,从直观的角度,我们最好将引用计数设计为b的一个成员变量。但是,这会造成一些实现上的麻烦,我们要么强制要求每个由智能指针(实际上是一个包含一个指针的管理类)的对象必须有refCount成员变量,要么要求它从一个特殊的包含refCount的基类继承。为了避免这一麻烦,我们在练习中将其设计为智能指针类的成员变量,而非实际指针的属性。

       由于指针的“共享”属性,那么可能会有多个智能指针管理类在引用这一指针,为了确保它们对同一指针的引用计数记录保持一致,我们把refCount这一变量也设计为int指针,在共享一个指针的智能指针类之间也共享这一refCount指针,那么,我们的智能指针目前就包含了两个成员变量,如下:

template<typename T>
class SmartPtr
{
private:
        T* ptr = nullptr;
        int* refCount = nullptr;
};

        (2) 引用计数的增加引用与减少引用

        首先,我们讨论了很久的引用计数的增加与减少,有必要对这两个行为做一下定义。对于增加,则比较简单,直接将引用计数加1即可;而对于减少引用计数而言,我们需要在减少后判断引用计数是否已经减为0,并在减为0时,完成指针的释放内存操作:

template<typename T>
class SmartPtr
{
public:
    // ...
	void AddRef()
	{
		assert(refCount);
		(*refCount)++;
	}

	void Release()
	{
                if(!ptr) return;
		assert(refCount && (*refCount) != 0);

		(*refCount)--;
		if (refCount && (*refCount) == 0)
		{
			delete refCount;
			delete ptr;
			refCount = ptr = nullptr;
		}
	}
    // ...
};

        (3) 初始化

       现在,我们开始探究在不同的操作下,引用计数应该发生什么样的变化。为了构造智能指针,我们需要提供一个初始化的方法,传入原始的裸指针,此时由于对象刚刚被构造,只有一个对象,所以引用计数将被初始化为1。

template<typename T>
class SmartPtr
{
// ...
public:
	SmartPtr(T* p) : refCount(new int(1)), ptr(p) {  }
// ...
};

       (4) 拷贝构造

        使用一个已有的智能指针对一个新的智能指针做拷贝构造,这意味着这个指针有了一个新的引用对象,此时,引用计数应该加1:

template<typename T>
class SmartPtr
{
// ...
public:
	SmartPtr(SmartPtr& q)
	{
		if (q)
		{
			ptr = q.get();
			refCount = q.GetRefCount();

			AddRef();
		}
	}
// ...
};

        (5) 赋值运算符

        对于赋值运算而言,和拷贝类似,将智能指针对象q赋值给p后,它们所指向的那个新的指针的引用计数将会加1。不过需要注意的是,在此之前p可能还引用着另一对象,所以在赋值之前,需要将其可能引用的对象的引用计数减1:

template<typename T>
class SmartPtr
{
// ...
public:
	SmartPtr& operator =(const SmartPtr &q)
	{
		if (this != &q)
		{
			Release();
			
			if (q)
			{
				ptr = q.get();
				refCount = q.GetRefCount();

				AddRef();
			}
		}
		return *this;
	}
// ...
};

       (6) 一些必要的运算符

        为了让我们的智能指针管理类"表现"得更像一个指针,我们还需要引入一些函数,如下:

	operator bool() const
	{
		return ptr;
	}

	T* operator->()
	{
		return ptr;
	}

	T& operator*()
	{
		assert(ptr != nullptr);
		return *ptr;
	}

	T* get()
	{
		return ptr;
	}

        (7) 结果与验证  

       最终,我们简单的做了一个共享智能指针的demo,只是大致描述了引用计数的变化过程,具体实现细节不一定准确:

#pragma once
#include <assert.h>
template<typename T>
class SmartPtr
{
private:
	T* ptr = nullptr;
	int* refCount = nullptr;

public:
	SmartPtr(T* p)
		:refCount(new int(1)), ptr(p)
	{ 

	}

	~SmartPtr()
	{
		Release();
	}

	SmartPtr(SmartPtr& q)
	{
		if (q)
		{
			ptr = q.get();
			refCount = q.GetRefCount();

			AddRef();
		}
	}

	SmartPtr& operator =(const SmartPtr &q)
	{
		if (this != &q)
		{
			Release();
			
			if (q)
			{
				ptr = q.get();
				refCount = q.GetRefCount();

				AddRef();
			}
		}
		return *this;
	}

	operator bool() const
	{
		return ptr;
	}

	T* operator->()
	{
		return ptr;
	}

	T& operator*()
	{
		assert(ptr != nullptr);
		return *ptr;
	}

	T* get()
	{
		return ptr;
	}

	void AddRef()
	{
		assert(refCount);
		(*refCount)++;
	}

	void Release()
	{
		if(!ptr) return;
		assert(refCount && (*refCount) != 0);

		(*refCount)--;
		if (refCount && (*refCount) == 0)
		{
			delete refCount;
			delete ptr;
			refCount = ptr = nullptr;
		}
	}

	int Count()
	{
		if (refCount)
		{
			return *refCount;
		}
		return 0;
	}

	int* GetRefCount()
	{
		return refCount;
	}
};

       我们可以在这个智能指针类下做一些小测试。

       首先我们可以尝试将这个指针传入一个函数,这个函数将会临时使用一下指针。那么,按照我们的初衷,初始化时,引用计数应该为1,传入函数后,由于有了临时引用,引用计数变为2,函数退出后,不再引用,引用计数又变回为1。

void Run(SmartPtr<int>& t)
{
	SmartPtr<int> q(t);
	cout << q.Count() << endl; // refCount : 2
}

int main()
{
	SmartPtr<int> p(new int(20)); 
        cout << p.Count() << endl; // refCount : 1

	Run(p);

	cout << p.Count() << endl; // refCount : 1
}

        但是,我们也可以发现一点问题,如果用户使用裸指针来初始化多个智能指针,如在上例中的Run函数传入裸指针而非共智能指针,引用计数就不会像我们预想的那样增加,如下面的代码清单。实际上,在C++标准库中的智能指针也有类似的问题。

void Run(int* t)
{
	SmartPtr<int> q(t);
	cout << q.Count() << endl; // refCount : 1
}

int main()
{
	SmartPtr<int> p(new int(20)); 
        cout << p.Count() << endl; // refCount : 1

	Run(p.get());

	cout << p.Count() << endl; // refCount : 1
}

         我们再考虑这样的调用(可能需要我们补上移动语义的函数定义),最终打印出来的a将会是乱码,因为我们传入参数的智能指针是一个临时对象,它会在离开其作用域的时候(当前表达语句)释放对象a。

void Run(SmartPtr<int> t)
{
	SmartPtr<int> q(t);
	cout << q.Count() << endl;
}

int main()
{
	int* a = new int(20);

	Run( SmartPtr<int>(a) );

	cout << *a << endl;
}

其它

       C++11中已经包含了类似的智能指针,称为shared_ptr,包含在头文件<memory>中。关于它的具体使用与陷阱,可以参考《C++ Primer 5》以及https://en.cppreference.com/w/cpp/memory/shared_ptr

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值