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

C++智能指针

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

C++智能指针

C++智能指针
    • 内存泄漏
    • 智能指针的使用及原理
      • RAII原理
      • 智能指针的使用
    • std::auto_ptr
    • std::unique_ptr
    • ★std::shared_ptr
      • std::shared_ptr的线程安全问题
      • std::shared_ptr的循环引用问题
    • std::weak_ptr
    • 特化删除器
      • delete和delete[]的区别
    • RAII扩展学习
    • 总结

由于C++语言没有自动内存回收机制,程序员每次new出来的内存需手动调用delete才能进行释放,如果因为种种原因如:忘记delete释放资源或程序抛异常而造成delete未被执行,那么会导致内存泄漏产生危害,使用智能指针可以方便程序员编程同时避免上述特殊意外的发生;

内存泄漏

什么是内存泄漏:

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

如何避免内存泄漏:

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状 态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保 证。
  2. 采用RAII思想或者智能指针来管理资源
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

智能指针的使用及原理 RAII原理

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句 柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源(出作用域会自动析构)。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效,意外出了生命周期,也会自动析构,不会内存泄漏。
智能指针的使用

定义简单的智能指针类:

template
class SmartPtr
{
public:
    //智能指针:
	// 1、RAII原理回收
	// 2、重载operator* 和 operator->  用起来像指针一样;
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		delete _ptr;	//析构的时候delete掉内部存的原生ptr
		cout << "delete:"<<_ptr << endl;    //打印delete信息;
	}
	//
	T& operator*()
	{
		return *_ptr;
	}

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

异常导致内存泄漏:

int main()
{
	int a = 0;
	try
	{
		int* array = new int[10];
		if (a == 0)
		{
			throw "Division by zero condition!";//模拟抛出异常;则后续的array delete不会执行,内存泄漏;
		}

		cout << "delete []" << array << endl;
		delete[] array;					//正常情况下array delete位置
	}


	catch (const char* errmsg)//异常捕获位置;
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "unkown exception" << endl;
	}
	return 0;
}

运行结果

使用上面定义的SmartPtr智能指针类管理int* array;

int main()
{
	int a = 0;
	try
	{
		SmartPtrarray(new int[10]);
		if (a == 0)
		{
			throw "Division by zero condition!";//模拟抛出异常;跳出SmartPtr所在的作用域,其生命周期结束,自动调用析构函数释放资源,不会造成内存泄漏'
		}
	}


	catch (const char* errmsg)//异常捕获位置;
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "unkown exception" << endl;
	}
	return 0;

}


运行结果

这就是RAII原理,利用SmartPtr对象的生命周期管理某个指针变量;同时重载载operator*和opertaor->,使其具有指针一样的行为 这便是智能指针;

上述SmartPtr存在问题如下:

int main()
{
	//智能指针a,b管理同一块空间的时候
	SmartPtra = new int;
	SmartPtrb = a;

	a.~SmartPtr();//假设(a生命周期结束),自动调用了析构,释放了该内存
	b.~SmartPtr();//假设(b生命周期结束),自动调用了析构,再次释放该内存的时候出问题,其已经被a释放了,相当于一块内存delete了两次;

	return 0;
}

程序结束,自动调用a,b的析构函数;

则a析构时 释放了该内存

b析构时 再次释放该内存的时候出问题,其已经被a释放了,相当于一块内存delete了两次,报错;

std::auto_ptr

针对上述问题,C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

std::auto_ptr官方文档

namespace bit//bit为自己定义的命名空间,为了和标准库std区分;
{
	// 解决方法: C++98 auto_ptr-> 拷贝时,管理权转移
	//问题:    被拷贝的对象悬空,是一种不好的设计
	// 这种方式是一种失败的设计,被质疑很久,一般公司都要求不要使用它。
	template
	class auto_ptr
	{
	public:
		// 1、RAII
		// 2、重载operator* 和 operator->  用起来像指针一样
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		// sp2(sp1) 管理权转移
		auto_ptr(auto_ptr& sp)
			:_ptr(sp._ptr)//原生指针管理权转移入this->_ptr,sp对象里的_ptr被置空,失去管理能力;
		{
			sp._ptr = nullptr;
		}

		// ap2 = ap3;
		auto_ptr& operator=(auto_ptr& ap)
		{
			if (this != &ap)//优化自己对自己赋值;
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)//这里当_ptr为nullptr,代表原生ptr管理者已经转移,析构啥也不做,真正的管理者调用
			{		//析构才会进入,delete掉原生指针;
				delete _ptr;
				cout << _ptr << endl;
			}	
		}

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

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

	private:
		T* _ptr;
	};
int main()
{
	//智能指针a,b管理同一块空间的时候
	bit::auto_ptra = new int;
	bit::auto_ptrb = a;//这里拷贝构造,a的原生ptr置为空,管理权给b,则a析构的时候啥也不做了;

	return 0;
}

程序结束,自动调用a,b的析构函数;

则a析构时 因内部ptr已经置空了,什么也不干;

b析构时 delete掉new出来转移过来的真正的ptr;

上述auto_ptr存在问题如下:

	int main()
	{
		//智能指针a,b管理同一块空间的时候
		bit::auto_ptra = new int;
		bit::auto_ptrb = a;

		b.~auto_ptr(); //现在再从实现原理层来分析会发现,这里拷贝后把a对象的指针赋空了,导致a对象悬空
 					// 通过a对象访问资源时就会出现问题。
		*a = 10;
		return 0;
	}
std::unique_ptr

C++98 – 2011年中间这段,其实大家在使用boost库的智能指针
boost – scoped_ptr shared_ptr weak_ptr
C++11 吸收了boost库中有价值的一些设计
C++11 – unique_ptr shared_ptr weak_ptr

C++11中开始提供更靠谱的unique_ptr

std::unique_ptr官方文档)

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

// 模拟实现一份简答的UniquePtr,了解原理;
template
class UniquePtr
{
public:
	UniquePtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~UniquePtr()
	{
		if (_ptr)
			delete _ptr;
	}
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }

private:
	// C++98防拷贝的方式:只声明不实现+声明成私有
	UniquePtr(UniquePtr const&);
	UniquePtr& operator=(UniquePtr const&);

	// C++11防拷贝的方式:delete
	UniquePtr(UniquePtr const&) = delete;
	UniquePtr& operator=(UniquePtr const&) = delete;

private:
	T* _ptr;
};

这个设计相当于直接将拷贝功能移除了,还是有缺陷的,某些场景还是需要使用拷贝构造,赋值拷贝等;

★std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

[std::shared_ptr官方文档](shared_ptr - C++ Reference (cplusplus.com))

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

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

模拟实现简化的SharedPtr:

#include 
#include 
template 
class SharedPtr
{
public:
	SharedPtr(T* ptr = nullptr)
		: _ptr(ptr)
		, _pRefCount(new int(1))//一开始构造管理对象以后,有一个SharedPtr在管理原生ptr
		, _pMutex(new mutex)
	{}
    
    // 拷贝构造
	SharedPtr(const SharedPtr& sp)
		: _ptr(sp._ptr)
		, _pRefCount(sp._pRefCount)
		, _pMutex(sp._pMutex)
	{
		// 加锁或者使用加1的原子操作
		_pMutex->lock();
		++(*_pRefCount);
		_pMutex->unlock();
	}
    
	// 拷贝赋值
	SharedPtr& operator=(const SharedPtr& sp)
	{
        //防止自己给自己赋值,如果ptr就一个管理对象的话,它自己给自己复制就会先delete ptr再把空ptr给自己 出现问题												
		if (_ptr != sp._ptr)
		{
            	//加锁释放管理的旧资源!!!别忘记
            	_pMutex->lock();//旧锁
				if (--(*_pCount) == 0)
				{
					delete _pCount;
					delete _ptr;
				}
           		_pMutex->unlock();
            	//(释放旧资源之后*this跟旧资源就没关系了,内部成员全部更新成新对象sp的,引用计数再+1)
				// 共享管理新对象的资源,并增加引用计数
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
            	 _pMutex = sp._pMutex;
            
            	_pMutex->lock();//新锁
				++(*_pRefCount);
            	_pMutex->unlock();//新锁
		 }	
			return *this;
	}
    //借助swap的现代写法
     //shared_ptr& operator=(shared_ptr sp)
	//{
	//	swap(_ptr, sp._ptr);
	//	swap(_pCount, sp._pCount);
    //  swap(_pMutex, sp._pMutex);
    
	//	_pMutex->lock();//新锁
	//	++(*_pRefCount);
     //  _pMutex->unlock();//新锁
    
	//	return *this;
	//}
    
	T& operator*() { return *_ptr; }
    
	T* operator->() { return _ptr; }
    
	int UseCount() { return *_pRefCount; }
    
	T* Get() { return _ptr; }
    
    ~SharedPtr()
    {
       bool deleteflag = false;
		// 引用计数减1,如果减到0,则释放资源;这个引用计数_pRefCount是临界资源,多线程需要注意上锁;
		_pMutex->lock();
		if (--(*_pRefCount) == 0)
		{
			delete _ptr;
			delete _pRefCount;
			deleteflag = true;//最后一个管理者将ptr释放后,这个mutex终于可以释放了;
		}
		_pMutex->unlock();

		if (deleteflag == true)
			delete _pMutex;
    }
	
private:
	int* _pRefCount; // 引用计数; 初始构造时new int 给到这个int*,之后赋值,拷贝将这个int*赋值给其他管理者就好;
	T* _ptr; // 指向管理资源的指针 
	mutex* _pMutex; // 互斥锁
};

这里引用计数 _pRefCount 不可以使用static,因为需要管理的原生ptr不止一个,不能所有ptr共用一个count;

用int* pRefCount初始创建在堆上开空间的引用计数 方法要求掌握,实用;

int main()
	{
		SharedPtr sp1(new int(10));
		SharedPtr sp2(sp1);
		*sp2 = 20;
		cout << sp1.UseCount() << endl;
		cout << sp2.UseCount() << endl;
		SharedPtr sp3(new int(10));
		sp2 = sp3;
		cout << sp1.UseCount() << endl;
		cout << sp2.UseCount() << endl;
		cout << sp3.UseCount() << endl;
		sp1 = sp3;
		cout << sp1.UseCount() << endl;
		cout << sp2.UseCount() << endl;
		cout << sp3.UseCount() << endl;

		return 0;
	}

运行结果:

std::shared_ptr的线程安全问题
  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这 个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了。会导致资源未 释放或者程序崩溃的问题。所以智能指针中引用计数++、–是需要加锁的,这样引用计数的操作就是线程安全的。(这个问题在设计中,对引用计数的–or++中上锁解决)
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题;(这个问题程序员在使用智能指针时上锁解决,类似于stl库,它们都是非线程安全的);
std::shared_ptr的循环引用问题
struct ListNode
{
	int _data;
	shared_ptr _prev;
	shared_ptr _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr node1(new ListNode);
	shared_ptr 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;
}

可以看到两个New的两个ListNode都没释放!

循环引用分析:

  1. node1和node2两个智能指针对象指向两个节点,他俩的引用计数变成1,我们不需要手动delete(智能指针对象生命周期delete)。

  2. node1的_next指向node2,node2的_prev指向node1,他俩的引用计数变成2。

  3. 程序结束,调用node1和node2析构,他俩的引用计数减到1,但是node1的_next还指向下一个节点。但是node2的_prev还指向上一个节点,此时只是引用计数-1,还没到0,他们两个node还没释放。

  4. 也就是说Node1的_next析构了,node2就释放了。

  5. 也就是说Node2的_prev析构了,node1就释放了。

  6. 但是_next属于node1的成员,node1释放了,_next才会析构,node2才能释放,而node1由node2的_prev管理,_prev又属于node2 成员,释放了node2,_prev才能释放,进而node1才能释放,所以这就叫循环引用,谁也不会释放。

解决方案:

在引用计数的场景下,把节点中的_prev和_next改成weak_ptr

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// !原理!就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

struct ListNode
{
	int _data;
	weak_ptr _prev;
	weak_ptr _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr node1(new ListNode);
	shared_ptr 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;
}

std::weak_ptr

std::weak_ptr用法如上,解决shared_ptr的循环引用问题;

特化删除器

如果new出来的不是对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题 (ps:删除器这个问题我们了解一下)

new T[10]; 和 new T;不同,前面是个array连续对象空间,有时delete和delete[]需要正确使用;

delete和delete[]的区别

或者FILE*这种指针就不能delete释放,而是需要f.close(ptr);

指针有很多种类,他们的释放方式有时需要我们给出;

template>//缺省值,可以搞个针对大多数指针类型delete方式的默认释放;
class shared_ptr
{
    //...
    ~SharedPtr()
    {
       bool deleteflag = false;
		// 引用计数减1,如果减到0,则释放资源;这个引用计数_pRefCount是临界资源,多线程需要注意上锁;
		_pMutex->lock();
		if (--(*_pRefCount) == 0)
		{
   			//delete _ptr;
            
             D del;//使用特定删除器正确释放;
             del(_ptr);
			delete _pRefCount;
			deleteflag = true;//最后一个管理者将ptr释放后,这个mutex终于可以释放了;
		}
		_pMutex->unlock();

		if (deleteflag == true)
			delete _pMutex;
    }
};
// 仿函数删除器
//特化对应释放方式;
template
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template
struct DeleteArrayFunc {
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	FreeFunc freeFunc;
	shared_ptr sp1((int*)malloc(4), freeFunc);//free释放原生ptr

	DeleteArrayFunc deleteArrayFunc;
	shared_ptr sp2((int*)malloc(4), deleteArrayFunc);//delete[]释放原生ptr

	return 0;
}

RAII扩展学习

RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。

#include 
#include 
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理
template
class LockGuard
{
public:
	LockGuard(Mutex* mtx)
		:_mutex(mtx)
	{
		_mutex->lock();
	}
	~LockGuard()
	{
		_mutex->unlock();
	}
	LockGuard(const LockGuard&) = delete;
private:
	Mutex* _mutex;
};


mutex mtx;//对于多线程可见的全局的一把锁;
static int n = 0;
void Func()
{
	for (size_t i = 0; i < 1000000; ++i)
	{
        //这里利用RAII对mutex进行管理,之后不用显示调用unlock,因为出了生周期作用域,会自动析构进而解		
		LockGuard lock(mtx);/
		++n;
	}
}
int main()
{
	int begin = clock();
	thread t1(Func);
	thread t2(Func);
	t1.join();
	t2.join();
	int end = clock();
    
	cout << n << endl;
	cout << "cost time:" << end - begin << endl;

	return 0;
}

总结
  1. 不要使用auto_ptr,他是早期的设计缺陷。
  2. 不拷贝的场景建议使用unique_ptr
  3. 如果要拷贝或者要定制删除器,建议使用shared_ptr。不过要注意循环引用的问题

关于智能指针常见问题

  • 什么是RAIl
  • 了解智能指针的发展历史----auto_ptr–boost库/社区/unique,shared
  • 模拟实现一个简洁智能指针----建议写unique_ptr;非得shared就省去线程安全和删除器;
  • 什么是循环引用?如果解决?解决的原理是什么?----weak_ptr
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/932003.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

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

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