【C++】 ——C++11新特性之智能指针

一、引入智能指针的重要性

关于智能指针,其实是C++11的一种新的特性,记住一句话:它主要是解决内存泄漏问题,因为智能指针是一个类,当超出了类的作用域之后,类就会自动调用析构函数,析构函数就会自动释放资源。那这里可能会问,当我们申请一块内存空间的时候,当使用完,记得释放掉不就可以了吗?其实也没错,但是如果在new之后,delete之前,抛出了异常,如以下fun2代码所示:

void fun1()
{
	throw int(11);  //抛出异常
}
void fun2()
{
	int* p = new int[10000];
	fun1();
	delete[] p;
}
int main()
{
	try
	{
		fun2();
	}
	catch (int& e)
	{
		cout << "捕获" << endl;
	}
	system("pause");
	return 0;
}

这样写是会造成内存泄漏问题,那这里其实有一种办法可以解决,就是如果发现delete之前发现某个函数抛出了异常,就在delete之前捕获这个异常,并且在catch语句里面进行资源的释放,并且可以再将这个异常重新抛出。如以下代码所示:

void fun2()
{
	int *p = new int[10000];
	try
	{
		fun1();
	}
	catch(int& e)
	{
		delete[] p;
		cout << "重新抛出" << endl;
		throw;
	}
	delete[] p;
}

有没有发现很繁琐?如果代码很多的话,我们很容易混淆,不知道究竟是哪个抛出了异常。所以引出智能指针是很有必要的

二、智能指针的原理

2.1 RAII

(1)RAII(Resource Acquisition Is Initialization -> 资源获得即初始化),是一种利用生命周期来控制程序资源的简单技术。也是一种避免内存泄漏的方法
(2)在对象构造的时候获取资源,接着控制对资源的访问使之在对象的生命周期内有效,最后在对象析构的时候释放资源
(3)我们实际上把管理一份资源的责任交给了一个对象,这样做不仅可以不需要显示的去释放资源,也可以使得对象所需的资源在生命周期内始终有效。

2.2 智能指针的原理

为了使智能指针能够像平常的指针一样,我们对“->” , " * "进行了重载

template<class T>
class SmartPtr
{
private:
	T* _ptr;
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{
	}
	T& operator*()  //重载* ,方便解引用
	{
		return *_ptr;
	}
	T* operator->()  //重载 ->,方便访问结构体成员
	{
		return _ptr;
	}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
};
int main()
{
	int *p = new int;
	SmartPtr<int> sp(p);
	*sp = 10;
	cout << *sp << endl;
	system("pause");
	return 0;
}

三、C++库中的智能指针

3.1 auto_ptr

头文件:memory
auto_ptr可以说是最原始的智能指针,但是他也会存在一个问题,如以下:

int main()
{
	int* p = new int;
	auto_ptr<int> ap(p);
	auto_ptr<int> copy(ap);
	*ap = 10;
	system("pause");
	return 0;
}

这里程序就会崩溃,这么说吧,当我们将ap拷贝之后,他自己就会成为一个空指针,那再解引用空指针,就会出问题。我们可以模拟实现一下auto_ptr,你就会明白为啥了

3.1.2 auto_ptr的模拟实现

template<class T>
class MyAuto_ptr
{
private:
	T* _ptr;
public:
	MyAuto_ptr(T* ptr) 
		:_ptr(ptr)
	{}
	MyAuto_ptr(MyAuto_ptr<T>& p)
		:_ptr(p._ptr) //拷贝构造
	{
		p._ptr = nullptr;  //发生拷贝构造之后自己就置空了,所以解引用空指针就发生了问题
	}
	MyAuto_ptr<T>& operator=(MyAuto_ptr<T> &p)  //重载赋值
	{
		if (this != &p)  //防止自己给自己赋值
		{
			if (_ptr)
				_ptr = nullptr;
			 
			_ptr = p._ptr; 
			p._ptr = nullptr;  //将自己置空
		}
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T& operator->()
	{
		return _ptr;
	}
	~MyAuto_ptr()
	{
		if (_ptr)
			delete _ptr;
	}
};

3.2 unique_ptr

C++ 库里面的unique_ptr,但从字面意思来说unique就是表示特殊的,唯一的意思,它的确很粗暴,因为auto_ptr拷贝构造的缺陷,它直接将拷贝构造和赋值都delete掉.做了防拷贝,如下所示:

template<class T>
class MyUnique_ptr
{
private:
	T* _ptr;
public:
	MyUnique_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}
	MyAuto_ptr(MyAuto_ptr<T>& p) = delete;  //拷贝构造
	MyAuto_ptr<T>& operator=(MyAuto_ptr<T> &p)=delete  //重载赋值
	T& operator*()
	{
		return *_ptr;
	}
	T& operator->()
	{
		return _ptr;
	}
	~MyUnique_ptr()
	{
		if (_ptr)
			delete _ptr;
	}
};

我们是不是还没看出它的缺点,那再来看一个代码:

unique_ptr<int> p1(new string("hello world"));
unique_ptr<int> p2;
p2 = p1;  // #1、 not allowed

unique_ptr<int> p3;
p3 = unique_ptr<int>(new string("hello world")); //#2、 allowed

其中#1留下悬挂的unique_ptr(p1),这可能导致危害(我觉得…unique_ptr连赋值和拷贝构造函数都没有,应该就出错了)。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 p3 后就会被销毁

那其实真正的问题还是没有解决,它是将指针悬空了,那拷贝构造的问题还是没有解决。

3.3 shared_ptr

shared_ptr从字面来说就是“共享式拥有概念”,多个智能指针可以指向相同的内存空间,该空间和其相关资源会在“最后一个引用被销毁”的时候释放。使用计数来表明资源被几个指针共享。(我老感觉这里有点写时拷贝的感觉…)

class MyShared_ptr
{
private:
	T* _ptr;
	int* _count;  //标记内存被几个智能指针指向的计数
};

3.3.1 shared_ptr 的模拟实现:

template<class T>
class MyShared_ptr
{
public:
	MyShared_ptr(MyShared_ptr<T>& p)//拷贝构造函数
		:_pr(p._ptr)
		,_count(p._count)
	{
		++(*_count);   //增加计数
	}
	MyShared_ptr<T>& operator=(MyShared_ptr<T>& p)
	{
		if (_ptr != p._ptr) //防止自己给自己赋值
		{
			if (--(*count) == 0)  //如果是最后一个指向该空间的指针,可以直接删除
			{
				delete _ptr;
				delete _count;
			}
			_ptr = p._ptr;
			_count = p._count;
			++(*_count);

		}
	}
	~MyShared_ptr()//析构函数,若当前计数为0,直接释放空间
	{
		if (--(*_count) == 0)
		{
			delete _ptr;
			delete _count;
		}
	}

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

这里看到share_ptr好像已经接近完美了,已经把拷贝构造和赋值的问题解决了
关于shared_ptr得使用:

shared_ptr<int> sp1(new int(20));
shared_ptr<int> sp2(sp1);  //调用拷贝构造
cout<<*sp2<<endl;

3.3.2 shared_ptr存在的问题

但是啊,其实也是有点问题的,原因如下:

智能指针对象中引用计数是多个智能指针对象共享的,如果有几个线程中智能指针的引用计数同时操作,比如 一个++,一个 - -,会导致资源未释放或者程序崩溃的问题。简而言之就是这个操作不是原子的,会引发线程安全问题。我们能解决的就是上锁来保证线程安全,直接贴上代码吧!

template<class T>
class Shared_Ptr {
public:
	Shared_Ptr(T* ptr = nullptr)
		: _ptr(ptr)
		, _count(new int(1))
		, _pmtx(new mutex)
	{}

	Shared_Ptr(Shared_Ptr<T>& ap)
		:_ptr(ap._ptr)
		, _count(ap._count)
		, _pmtx(ap._pmtx)
	{
		Add();
	}

	void Add()//加锁的++
	{
		_pmtx->lock();
		++(*_count);
		_pmtx->unlock();
	}

	void Release()//加锁的--
	{
		bool flag = false;
		_pmtx->lock();
		if (--(*_count) == 0)
		{
			delete _ptr;
			delete _count;
			flag = true;//判断是否释放锁
		}
		_pmtx->unlock();
		if (flag == true)
			delete _pmtx;
	}

	Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap)
	{
		if (_ptr != ap._ptr)
		{
			Release();
			_ptr = ap._ptr;
			_count = ap._count;
			Add();
		}
		return *this;
	}

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

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

	~Shared_Ptr()
	{
		Release();
	}
private:
	T* _ptr;
	int* _count;
	mutex* _pmtx;
};

3.3.3 shared_ptr存在的循环引用问题
(别急,这是shared_ptr的最后一点问题)

struct ListNode 
{    
	int _data;    
	shared_ptr<ListNode> _prev;    
	shared_ptr<ListNode> _next;
 
	~ListNode()
	{ 
		cout << "~ListNode()" << endl; 
	} 
};
 
int main()
{
 	shared_ptr<ListNode> node1(new ListNode);    
 	shared_ptr<ListNode> node2(new ListNode);    
 	cout << node1.use_count() << endl;    
 	cout << node2.use_count() << endl;
 
    node1->_next = node2;    
    node2->_prev = node1;
 
    cout << node1.use_count() << endl;    
    cout << node2.use_count() << endl;
 
    return 0; 
}

写这个是希望cur和prev被智能指针管理,但是报错了。原因如下:
(1)node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
(2)node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
(3)node1和node2析构,引用计数减到1,但是_next还指向下一个节点。并且_prev还指向上一个节点。
(4)也就是说_next析构了,node2就释放了,_prev析构了,node1就释放了。
(5)但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2 成员,这就叫循环引用,谁也不会释放。

解决方法:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加 node1和node2的引用计数。 
struct ListNode 
{    
	int _data;    
	weak_ptr<ListNode> _prev;    
	weak_ptr<ListNode> _next; 
  
	~ListNode()
	{ 
		cout << "~ListNode()" << endl; 
	} 
};
 
int main() 
{    
	shared_ptr<ListNode> node1(new ListNode);    
	shared_ptr<ListNode> node2(new ListNode);    
	cout << node1.use_count() << endl;    
	cout << node2.use_count() << endl;
 
    node1->_next = node2;    
    node2->_prev = node1;
 
    cout << node1.use_count() << endl;    
    cout << node2.use_count() << endl;
 
    return 0; 
}

四、总结

  • auto_ptr :管理权限转移,在拷贝构造的时候,会将自己的_ptr置为nullptr,解引用的时候会崩溃,有缺陷,一般严禁使用
  • unique_ptr:直接将拷贝构造和赋值delete掉,防拷贝,一般鼓励使用
  • shared_ptr:设置引用计数,同时要考虑线程安全和循环引用,线程安全问题通过加锁解决,而循环引用问题用weak_ptr解决
原创文章 78 获赞 21 访问量 3522

猜你喜欢

转载自blog.csdn.net/Vicky_Cr/article/details/105808281