C++智能指针

在这里插入图片描述

内存泄漏

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

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

void MemoryLeaks()
 {
 // 1.内存申请了忘记释放
 int* p1 = (int*)malloc(sizeof(int));
 int* p2 = new int;
 
 // 2.异常安全问题
 int* p3 = new int[10];
 
 Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
 
 delete[] p3;
 }

内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak),堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

  • 系统资源泄漏,指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定

如何检测内存泄漏

在linux下内存泄漏检测:linux下几款内存泄漏检测工具
在windows下使用第三方工具: VLD工具说明
其他工具: 内存泄漏比较
使用vc++库函数 快速查找内存泄漏

演示vc++库函数_CrtSetDbgFlag函数如如快速查找内存泄漏

#include <iostream>
#include <crtdbg.h> //c语言库中_CrtSetDbgFlag的头文件
using namespace std;

inline void EnableMemLeakCheck()
{
	_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
int main()
{
	EnableMemLeakCheck();
	int* leak = new int[10];
	return 0;
}

申请40字节时,在使用完毕后,直接return 了,并没有来得及delete leak, 那么就会导致40字节泄漏了,来看看EnableMemLeakCheck()函数的效果:
在这里插入图片描述

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

  2. 采用RAII思想或者智能指针来管理资源

  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

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

智能指针的使用及原理

RAII

  • RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
  • 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
    1.不需要显式地释放资源。
    2、采用这种方式,对象所需的资源在其生命期内始终保持有效

模拟智能指针的过程

#include <iostream>
#include <crtdbg.h>
#include <crtdbg.h> //c语言库中_CrtSetDbgFlag的头文件
using namespace std;

inline void EnableMemLeakCheck()
{
	_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}

template <class T>
class SmartPtr 
{
public:
	SmartPtr(T* ptr) 
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr) delete _ptr;
		_ptr = nullptr;
	}

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

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

private:
	T* _ptr;
};

int main()
{
	int* pi = new int[10];
	for (int i = 0; i < 10; i++) *(pi + i) = i;
	for (int i = 0; i < 10; i++) cout << pi[i] << " ";

	cout << "\n---------------------" << endl;
	SmartPtr<int> sp(pi);
	for (int i = 0; i < 10; i++) sp[i] = i + 10;
	for (int i = 0; i < 10; i++) cout << pi[i] << " ";
	

	EnableMemLeakCheck();
	return 0;
}

效果:对申请的40字节空间做两次修改
在这里插入图片描述

查看是否有内存泄漏:没有, 安全退出
在这里插入图片描述

总结一下智能指针的原理:
1. RAII特性, 通过对象的生命周期管理,内存资源
2. 重载operator*和opertaor->,具有像指针一样的行为

C++98 std::auto_ptr

std::auto_ptr

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

#include <iostream>
#include <crtdbg.h> //c语言库中_CrtSetDbgFlag的头文件
using namespace std;

inline void EnableMemLeakCheck()
{
	_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}

template <class T>
class auto_Ptr 
{
public:
	auto_Ptr(T* ptr)
		:_ptr(ptr)
	{}
	
	auto_Ptr(auto_Ptr<T>& ptr)
		:_ptr(ptr._ptr)
	{
		//源对象的值赋值给目标对象后, 源对象自动放弃对资源的占用权
		ptr._ptr = nullptr;
	}
	
	auto_Ptr<T>& operator=(auto_Ptr<T>& ap)
	{
		if (this == &ap) return *this;
		//如果之前申请过空间,就需要先释放
		if (this != &ap && _ptr) {
			delete _ptr;
			_ptr = nullptr;
		}

		//交接资源, ap自动放弃原有资源
		_ptr = ap._ptr;
		ap._ptr = nullptr;
		
		return *this;
	}
	
	~auto_Ptr()
	{
		if (_ptr)  delete _ptr;
		_ptr = nullptr;
		
	}

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

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

private:
	T* _ptr;
};

int main()
{
	int* ptr = new int();
	auto_Ptr<int> ap(ptr);
	*ap = 10;
	cout << *ap << endl;
	
	//使用auto_Ptr指针拷贝一个新的指针
	auto_Ptr<int> newap(ap);
	*newap = 20;
	//*ap = 10;  //不能再次使用,ap._ptr的值已经指向NULL了,解引用会空指针错误
	cout << *newap << endl;


	EnableMemLeakCheck();
	return 0;
}

运行结果:
在这里插入图片描述
查看是否会有内存泄漏:没有内存泄漏的提示
在这里插入图片描述

** auto_ptr的缺陷之一: 源对象资源交接到目标对象后最好不要继续使用源对象否则回引发程序崩溃的问题。**

请看以下:

int main()
{
	int* ptr = new int();
	auto_Ptr<int> ap(ptr);
	*ap = 10;
	cout << *ap << endl;
	
	//使用auto_Ptr指针拷贝一个新的指针
	auto_Ptr<int> newap(ap);
	*newap = 20;
	*ap = 10;  //err, ap的资源已经时nullptr了,此时访问会空指针错误
	cout << *newap << endl;


	EnableMemLeakCheck();
	return 0;
}

在这里插入图片描述

auto_ptr的拷贝赋值也是如此,当拷贝赋值给一个目标对象后,源对象自动放弃对已有资源的管理

auto_Ptr<T>& operator=(auto_Ptr<T>& ap)
{
	if (this == &ap) return *this;
	//如果之前申请过空间,就需要先释放
	if (this != &ap && _ptr) {
		delete _ptr;
		_ptr = nullptr;
	}

	//交接资源, ap自动放弃原有资源
	_ptr = ap._ptr;
	ap._ptr = nullptr;
	
	return *this;
}

测试:

int main()
{
	int* ptr = new int();
	auto_Ptr<int> ap1(ptr);
	*ap1 = 10;
	cout << *ap1 << endl;

	int* ptr1 = new int();
	auto_Ptr<int> ap2(ptr1);
	*ap2 = 20;
	ap1 = ap2;
	cout << *ap1 << endl;


	EnableMemLeakCheck();
	return 0;
}

在这里插入图片描述
并不会存在内存泄漏:
在这里插入图片描述

C++11 unique_ptr

他为了解决auto_Ptr的问题:
拷贝或者拷贝赋值后,再去使用源对象就会导致空指针的问题

在unique_ptr中是直接将拷贝赋值和拷贝构造给禁用了的,只保留auto_ptr中没有问题的功能接口

template <class T>
class unique_Ptr
{
public:
	unique_Ptr(T* ptr)
		:_ptr(ptr)
	{}
	unique_Ptr(unique_Ptr<T>& ptr) = delete;
	unique_Ptr<T>& operator=(unique_Ptr<T>& ap) = delete;
	~unique_Ptr()
	{
		if (_ptr)  delete _ptr;
		_ptr = nullptr;

	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

void test_unique_Ptr()
{
	int* ptr = new int();
	unique_Ptr<int> up(ptr);
	*up = 10;
	cout << *up << endl;
}

C++11 shared_ptr


template <class T>
class shared_Ptr
{
public:
	shared_Ptr(T* ptr)
		:_ptr(ptr)
		, _count(new size_t(1))
		, mtx(new mutex())
	{
	}

	//拷贝构造是为了让拷贝出来的智能指针管理同一个指针变量,
	//并共享同一个引用计数
	shared_Ptr(const shared_Ptr<T>& sp)
		:_ptr(sp._ptr)
		, _count(sp._count)
		, mtx(sp.mtx)
	{
		
		AddRefer();
	}
	//由于在多线程环境下,需要访问临界资源是原子的所以单独封装
	void AddRefer()
	{
		mtx->lock();
		++(*_count);
		mtx->unlock();
	}
	//由于在多线程环境下,需要访问临界资源是原子的所以单独封装
	void Release() 
	{
		bool flag = true; //标识锁是否需要释放
		mtx->lock();
		
		//当shared_ptr对象所关联的指针引用计数减至0时,就需要释放该指针
		if (!(--(*_count))) {  			
		    delete _count;
			delete _ptr;
			_count = nullptr;
			_ptr = nullptr;

			//如果_count已经为0了,那么_ptr需要释放,因此this对象
			//也就没有意义了,锁也就需要释放了
			flag = false; 		
		}
			
		mtx->unlock();
		if(!flag) delete mtx;
	}
	
	//拷贝赋值后,源对象的值会被覆盖,所以源对象所关联
	//的指针的引用计数需要--,为了保证他的线程安全,交由Release()函数管理
	
	//当值被覆盖之后被用来赋值覆盖该对象的对象引用计数多一个,因为此时两个智能指针都指向同一指针
	//因此就需要对sp._count 跟this->_count的引用计数 ++,当然也要保证他们的原子性,
	//所以交由AddRefer()函数 管理
	shared_Ptr<T>& operator=(const shared_Ptr<T>& sp)
	{
		if (this._ptr != sp._ptr) {
			
			Release();

			_count = sp._count;
			_ptr = sp._ptr;
			mtx = sp.mtx;

			AddRefer();
		}
		
		return *this;
	}
	~shared_Ptr()
	{
		Release(); 
	}
	size_t Getcount()
	{
		return *_count;
	}
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

private:
	T* _ptr;
	size_t* _count;
	mutex* mtx;
};

测试模拟shared_Ptr是不是线程安全的, 主要看引用计数最终是否为1,当shared_Ptr对象出了作用域会再次析构一次, 此时_count就会等于0了。

void func(const shared_Ptr<Date>& sp, size_t n)
{
	for (size_t i = 0; i < n; i++) {
		shared_Ptr<Date> p(sp);
	}
}
void test_shared_Ptr() 
{
	const int n = 10000000;
	shared_Ptr<Date> sp(new Date());
	thread t1(func, sp, n);
	thread t2(func, sp, n);

	t1.join();
	t2.join();

	cout << sp.Getcount() << endl;
	cout << sp->_year << " " << sp->_month << " " << sp->_day << endl;
}

int main()
{
	
	test_shared_Ptr();

	EnableMemLeakCheck();
	return 0;
}

程序运行结果:
在这里插入图片描述

如果只是这样处理Release函数, 是否会存在内存泄漏问题?

void Release() 
{
	mtx->lock();
	
	if (!(--(*_count))) {
		delete _count;
		delete _ptr;
		_count = nullptr;
		_ptr = nullptr;
	}
	mtx->unlock();
}

~shared_Ptr()
{
	Release();
	
}

答案是:是的,因为锁资源并未被释放。

在这里插入图片描述

注意:博主上面演示的时锁没有释放的时候导致的内存泄漏问题,请读者记得将锁释放,以下才是正确的处理代码

void Release() 
{
	bool flag = true;
	mtx->lock();
	
	if (!(--(*_count))) {
		delete _count;
		delete _ptr;
		_count = nullptr;
		_ptr = nullptr;
		flag = false;
	}
	mtx->unlock();
	if (!flag) delete mtx;
}

~shared_Ptr()
{
	Release();
	
}

在这里插入图片描述

shared_ptr循环引用问题

struct ListNode 
{
	ListNode() {}
	
	ListNode *_next;
	ListNode* _prev;
	int val;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};

void testcircular()
{
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;
	node1->_next = node2;
	node2->_prev = node1;
	delete node1;
	delete node2;
}

如果是对当前ListNode对象进行释放,那么在该对象被释放后偶,会自动调用他的析构函数。
在这里插入图片描述

可是如果我们想要用RAII机制呢? 就是不去手动释放这个指针,让智能指针去管理这个内存资源,让智能指针对象出了生命周期就会自动释放。那么只需要让智能指针管理这个裸指针。

struct ListNode 
{
	ListNode() {}
	shared_Ptr<ListNode> _next;
	shared_Ptr<ListNode> _prev;

	int val;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};

void testcircular()
{
	shared_Ptr<ListNode> node1 (new ListNode());
	shared_Ptr<ListNode> node2 (new ListNode());
	node1->_next = node2;
	node2->_prev = node1;
}

使用智能指针之后,当程序执行完了,shared_ptr对象被释放之后, ListNode资源并没有被释放。

在这里插入图片描述

问题的本质还是智能指针的问题,因为node1一开始绑定他自己的内存资源,由于node2的prev需要指向node1, 所以node1和node2->prev此时指向的都是node1中保存的内存资源。所以node1的内存资源的引用计数是2

而node2一开始和他自己的内存资源绑定了,之后node1->next又指向了node2, 所以node2中的内存资源此时的引用计数是2,被node2指向和被node1->next指向。
在这里插入图片描述
当需要释放node1、node2两个智能指针的内存资源时,首先需要对这两个内存资源的引用计数减到0时才能释放这两个内存资源,但是引用计数的释放顺序又导致新的问题,node1指向的内存,需要让node1和node2->prev都被释放了才能去释放node1的内存资源, node2指向的内存,需要让node2和node1->next都被释放了才能去释放node1的内存资源,但是到底先释放哪个呢?可以看出这是一个互相依赖的关系,最终导致都没能释放,也就导致引用计数没办法剪到0, 最终资源不释放, 导致内存泄漏。

循环引用问题, 解决方案使用weak_ptr。

C++11 weak_ptr

template<class T>
class weak_Ptr
{
public:
	weak_Ptr()
		:_ptr(nullptr)
	{}

	weak_Ptr(const shared_Ptr<T>& sp)
		:_ptr(sp.get())
	{}

	weak_Ptr<T>& operator=(const shared_Ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

	// 可以像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
struct ListNode
{
	ListNode() {}
	weak_Ptr<ListNode> _next;
	weak_Ptr<ListNode> _prev;

	int val;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};


void testcircular()
{
	shared_Ptr<ListNode> node1(new ListNode());
	shared_Ptr<ListNode> node2(new ListNode());
	//由于ListNode中的成员都是weak_ptr对象,即使指向了内存资源
	//也并不对该内存资源的引用计数++操作,也就不会导致循环引用的问题了。
	node1->_next = node2;
	node2->_prev = node1;
}

int main()
{
	
	testcircular();

	EnableMemLeakCheck();
	return 0;
}

可以看出使用weak_ptr去指向node1和node2的内存资源,并不会对引用计数++, 所以weak_ptr是不会去参数资源的管理的,他存在的作用只是去解决shared_ptr循环引用的问题。

在这里插入图片描述

智能指针自定义删除器

在这里插入图片描述

template <class U, class D> shared_ptr (U* p, D del);

class D: 其实是自定义删除器的类型,也就是如果我们想释放一个自定义类型的对象的话,需要指定该对象的自定义删除器

//指定自定义类型的删除器
template<class D>
struct destroy    
{
	void operator()(D* del)
	{
		cout << "delete" << endl;
		delete[] del;
	}
};

int main()
{
	std::shared_ptr <ListNode> sp1(new ListNode[10], destroy<ListNode>());
	EnableMemLeakCheck();
	return 0;
}

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中的实现的

RAII扩展学习

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

  • 设计守卫锁
  • 1、创建锁对象就加锁,析构锁资源就解锁
#include <thread>
#include <mutex>
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理
template<class Mutex>
class LockGuard
{
public:
	 LockGuard(Mutex& mtx)
	 	:_mutex(mtx)
	 {
	 	_mutex.lock();
	 }
	 
	 ~LockGuard()
	 {
	 	_mutex.unlock();
	 }
	 
	 LockGuard(const LockGuard<Mutex>&) = delete;
private:
	 // 注意这里必须使用引用,否则锁的就不是一个互斥量对象
	 Mutex& _mutex;
};

mutex mtx;

void Func(int n)
{
	 for (size_t i = 0; i < n; ++i)
	 {
		 LockGuard<mutex> lock(mtx);
		 ++n;
	 }
}
int main()
{
	 int n = 10000;
	 int begin = clock();
	 thread t1(Func, n);
	 thread t2(Func, n);
	 
	 t1.join();
	 t2.join();
	 
	 int end = clock();
	 cout << n << endl;
	 cout <<"cost time:" <<end - begin << endl;
 
 	return 0; 
}

在这里插入图片描述

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱生活,爱代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值