智能指针的提出背景

要讲智能指针,首先我们要知道为什么会有这种东西出现.
这里要提到异常部分说讲的异常的重新抛出部分代码:

void Func2()
{
	throw 1;
}

void Func1()
{
	auto y = new int;
	cout << "new int " << endl;
	Func2();
	delete y;
	cout << "delete int" << endl;
}
int main()
{
	try
	{
		Func1();
	}
	catch (...)
	{
		cout << "catch succeed!" << endl;
	}
	return 0;
}

这部分代码如果是这样写显然会出现内存泄漏,因此需要稍作修改Func1:

//仅修改Func1
void Func1()
{
	auto y = new int;
	cout << "new int " << endl;
	try 
	{
		Func2();
	}
	catch (...)
	{
		delete y;
		cout << "delete int" << endl;
		throw;
	}
	delete y;
	cout << "delete int" << endl;
}

OK,这部分已经告诉我们普通的指针在异常体系里面容易出现内存泄露的风险。当然你觉得仅是如此程度的稍作修改还能接受的话且看下面一串代码:

void Func1()
{
	auto y = new int;
	auto x = new int;
	cout << "new int " << endl;
	try
	{
		Func2();
	}
	catch (...)
	{
		delete y;
		delete x;
		cout << "delete int" << endl;
		throw;
	}
	//...
}

运行结果:

new int
delete int
catch succeed!

看起来是不是很完美,毫无破绽?那么我们回忆一下关键字new会不会抛异常呢?很显然,当空间不足的时候就会抛异常。那么如果当x申请时抛异常又会发生什么呢:

void Func1()
{
	auto y = new int;
	cout << "new int y" << endl;
	auto x = new int[1e20];
	cout << "new int x" << endl;
	try
	{
		Func2();
	}
	catch (...)
	{
		delete y;
		delete x;
		cout << "delete int" << endl;
		throw;
	}
	//...
}
new int y
catch succeed!

这意味着y空间没有释放,造成了内存泄漏。本质是因为new x是抛异常,并且被main的catch接受了。所以想解决这个问题就必须将new x放进try语块里:

void Func1()
{
	auto y = new int;
	cout << "new int y" << endl;
	try
	{
		auto x = new int[1e20];
		cout << "new int x" << endl;
		try
		{
			Func2();
		}
		catch (...)
		{
			delete y;
			cout << "delete int y" << endl;
			delete[] x;
			cout << "delete[] int y" << endl;
			throw;
		}
	}
	catch (...)
	{
		delete y;
		cout << "delete int y" << endl;
		throw;
	}
	//...
}

运行结果:

new int y
delete int y
catch succeed!

这下倒是成功了,但是越来越复杂了。如果每次new一个新指针都要嵌套一层try和catch语句,那么代码将会十分臃肿且可读性很差。为了解决这个问题就需要智能指针。

智能指针的简单设计

其实回忆一下,之所以会内存泄漏,主要是抛异常跳过了回收内存的语句。那如果我们设计一个自动回收内存的方案就不会发生这种状况了。考虑线程安全部分讲到的LockGuard的设计,我们只需要设计一个类用构造函数保存指针,析构函数来delete指针。

template<class T>
class simple_ptr
{
public:
	simple_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~simple_ptr()
	{
		delete _ptr;
		cout << "~simple_ptr()" << endl;
	}
private:
	T* _ptr;
};

如此,调用:

void Func2()
{
	throw 1;
}

void Func1()
{
	simple_ptr<int>x(new int);
	cout << "new int" << endl;
	Func2();
}

int main()
{
	try
	{
		Func1();
	}
	catch (...)
	{
		cout << "catch succeed!" << endl;
	}
	return 0;
}
new int
~simple_ptr()
catch succeed!

很好,事请得到了完美解决,但是智能指针还有部分细节值得处理。

智能指针

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

simple_ptr的问题

首先simple不能像指针一样解引用和->,这是一个问题。其次还面临着拷贝问题:

//simple_ptr(const simple_ptr<T>& sp)
//:_ptr(sp._ptr)
//{}
int main()
{
	simple_ptr<int>p1(new int);
	simple_ptr<int>p2(p1);
	return 0;
}

无需多言,聪明的读者应该意识到了问题。同一块空间会被多次delete。这才是亟待解决的重要问题。
我们来看看C和C++是怎么设计智能指针的。

auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想
来观测一下他的拷贝构造和赋值重载是如何实现的:

auto_ptr(auto_ptr<T>& sp)
	:_ptr(sp._ptr)
{
	// 管理权转移
	sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
	// 检测是否为自己给自己赋值
	if (this != &ap)
	{
		// 释放当前对象中资源
		if (_ptr)
			// 转移ap中资源到当前对象中
			_ptr = ap._ptr;
		ap._ptr = NULL;
		delete _ptr;
	}
	return *this;
}

很好,这是相当令人诟病的设计。拷贝竟然会把原先的指针置空,如果对于不熟悉auto_ptr机理的人使用更是毁灭性的。
实际上在C++11中已经弃用了auto_ptr,改用unique_ptr替代:
Note: This class template is deprecated as of C++11. unique_ptr is a new facility with a similar functionality, but with improved security (no fake copy assignments), added features (deleters) and support for arrays. See unique_ptr for additional information.

unique_ptr

C++11提供了更多的智能指针模板,其中包括unique_ptr作为auto_ptr的上位替代。
unique_ptr的实现原理:简单粗暴的防拷贝。
在这里插入图片描述
从第九条可以看到,unique_ptr直接将拷贝构造给禁止了。
实际上在大多数不需要拷贝指针的场景我们都可以直接使用unique_ptr,而一些需要拷贝的场景就需要用到shared_ptr

shared_ptr

shared_ptr允许拷贝构造,具体是依靠引用计数来实现的:

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

shared_ptr的简单模拟实现

实际上我们只需要知道用一个类成员来保存引用计数,别的实现也不是什么复杂的事,这里就直接放出代码:

template<class T>
class shared_ptr
{
public:

	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{
	}

	shared_ptr(const shared_ptr<T>sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		++(*_pcount);
	}

	shared_ptr<T>& operator=(const shared_ptr<T>sp)
	{
		//注意考虑自己赋值给自己的情况
		if (_ptr != sp._ptr)
		{
			this->release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;

			++(*_pcount);
		}
		return *this;
	}

	void release()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
	}

	~shared_ptr()
	{
		release();
	}

	int use_count()
	{
		return *_pcount;
	}

	T& operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
	int* _pcount;
};

Is so easy,isn’t it?

std::shared_ptr的线程安全问题

需要注意的是shared_ptr的线程安全分
为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

二问题需要在外面加锁,而一问题则需要给引用计数_pcount加锁。当然了,实际上我们也可以将其改为原子操作来解决:

//int* _pcount;
atomic<int>* _pcount;

循环引用(weak_ptr)

循环引用是shared_ptr的一个问题,首先我们简单声明一个类:

struct Node
{
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
	int _val;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};

简单调用下:

int main()
{
	shared_ptr<Node>p1(new Node);
	shared_ptr<Node>p2(new Node);

	return 0;
}
~Node()
~Node()

没问题,现在让他们循环引用:

int main()
{
	shared_ptr<Node>p1(new Node);
	shared_ptr<Node>p2(new Node);
	p1->_next = p2;
	p2->_prev = p1;
	cout<<"The end!" << endl;

	return 0;
}
The end!

很显然,p1、p2对应的空间没有被销毁,现在造成了内存泄漏。
简单分析一下原因,p1的next指向p2导致其对应的空间引用计数+1,同理p1指向的空间引用计数+1.

int main()
{
	shared_ptr<Node>p1(new Node);
	shared_ptr<Node>p2(new Node);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1;

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	return 0;
}
1
1
2
2

现在问题是main函数结束后会自动释放p1和p2,他们指向的空间引用计数都变成1但是里面空间的指针又互相指着,导致他们的引用计数不能归零。

因此解决方案有:令Node里的指针不会增加引用计数。这里可以使用weak_ptr
在这里插入图片描述
在这里插入图片描述
这意味着我们可以用shared_ptr构造weak_ptr,同时不增加引用计数.
简单修改下Node:

struct Node
{

	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;
	int _val;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};

调用main:

int main()
{
	shared_ptr<Node>p1(new Node);
	shared_ptr<Node>p2(new Node);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1;

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	return 0;
}
1
1
1
1
~Node()
~Node()

完美解决!

定制删除器

作为一个指针类,怎么能只放new出来的东西呢,是不是也要能放别的类型呢:

int main()
{
	myptr::shared_ptr<A> sp1(new A[10]);
	myptr::shared_ptr<int> sp2((int*)malloc(4));
	myptr::shared_ptr<FILE> sp3(fopen("test.txt", "w"));
	myptr::shared_ptr<A> sp4(new A);

	return 0;
}

现在问题是找不到对应的析构函数,相信根据前面实现容器的经验大家已经想到了解决方案。只需增加一个仿函数模板即可。
C++11库里的shared_ptr就增加了删除器这个参数:
在这里插入图片描述
当然,决定仿函数写的麻烦,我们也只需要写lambda表达式即可

template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};

int main()
{
	shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
	shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());
	shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });

	shared_ptr<A> sp4(new A);

	return 0;
}

没有问题,shared_ptr内部实现需要增加一个封装器成员变量.
完整代码:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new atomic<int>(1))
	{}

	template<class D>
	shared_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new atomic<int>(1))
		, _del(del)
	{}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		(*_pcount)++;
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//if (this != &sp)
		if (_ptr != sp._ptr)
		{
			this->release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;

			++(*_pcount);
		}

		return *this;
	}

	void release()
	{
		if (--(*_pcount) == 0)
		{
			// 最后一个管理的对象,释放资源
			//delete _ptr;
			_del(_ptr);

			delete _pcount;
		}
	}

	~shared_ptr()
	{
		release();
	}

	int use_count()
	{
		return *_pcount;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	atomic<int>* _pcount;

	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐