C++:智能指针

一、智能指针的使用及原理

1.1 为什么需要智能指针

在学习异常的时候,我们知道了由于异常的反复横跳可能会导致内存泄露的问题,但是对于一些自定类类型来说他在栈帧销毁的时候会去调用对应的析构函数,但是以下这种必须手动释放的场景,一旦抛出异常就会造成内存泄露的结果。

cpp 复制代码
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}

在学习异常的时候我们提出乐了一种解决方案,就是内部先接受一下异常,将内存给释放了,然后再重新将异常抛给外层处理。但是这种方法是治标不治本,导致复杂。因此为了解决抛异常之后可能存在的内存泄露的问题,C++提出了智能指针来解决这个问题,他的思想是RAII。下面我们将RAII来理解什么是智能指针。

1.2 RAII

一些指针必须手动去释放内存,但是如果我们将这个指针变成自定义类型,他会在栈帧销毁的时候去调用对应的析构函数。RAII就是大致的这种思想。

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

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

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

1.3 智能指针的基本框架

既然是指针,还得像指针一样去使用,因此在模版中我们必须重载一下*和->的运算符。

cpp 复制代码
	template<class T>
	class SmartPtr
	{
		SmartPtr(T* ptr=nullptr)
			:_ptr(ptr)
		{}

		~SmartPtr()
		{
			if(_ptr) //如果不为空
			delete[] _ptr;
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//
	private:
		T* _ptr;
	};

总结一下智能指针的原理:
1. RAII特性(将资源交给对象去管理)
2. 重载operator*和opertaor->,具有像指针一样的行为。

1.4 auto_ptr

智能指针有自己的发展历史,各个版本产生的结果根本原因就是由于拷贝构造和赋值重载的实现思想不同。

C++98版本的库中就提供了auto_ptr的智能指针。
auto_ptr的实现原理:管理权转移的思想

cpp 复制代码
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	//拷贝构造
	auto_ptr(auto_ptr<T>& sp)  //管理权转移、
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;//被拷贝对象悬空
	}
	//赋值重载
	auto_ptr<T>& operator=(auto_ptr<T>& sp)
    {
		if (this != &sp) //防止自己给自己赋值,自己给自己赋值会导致内存被释放掉。
		{
			if (_ptr) delete _ptr;//释放当前对象中的资源
			//转移sp对象中的资源给自己
			_ptr = sp._ptr;
			sp._ptr = nullptr;//自己悬空
		}
	}

	~auto_ptr()
	{
		if (_ptr) //如果不为空
			delete[] _ptr;
	}
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//
private:
	T* _ptr;
};

附上测试用例:

cpp 复制代码
void test_auto_ptr()
{
 auto_ptr<int> sp1(new int);
 auto_ptr<int> sp2(sp1); // 管理权转移

 // sp1悬空
 *sp2 = 10;
 cout << *sp2 << endl;
 cout << *sp1 << endl;
}

但是auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。

1.5 unique_ptr

C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝(不让你拷贝)

此时有两种思路:

(1)只声明不定义:使得默认构造无法生成并且没有函数定义。但是这种做可能存在一个问题就是有的人会在类外去定义!!!

(2)放在私有:可以解决问题。

(3)C++11引入的delete关键字:这个关键字可以强制默认拷贝构造和赋值重载无法被生成!!

cpp 复制代码
template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	//拷贝构造
	unique_ptr(const unique_ptr<T>& sp) = delete;
	//赋值重载
	unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

	~unique_ptr()
	{
		if (_ptr) //如果不为空
			delete[] _ptr;
	}
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//
private:
	T* _ptr;
};

附上测试代码:

cpp 复制代码
void test_unique_ptr()
{
 unique_ptr<int> sp1(new int);
 unique_ptr<int> sp2(sp1);
}

1.6 shared_ptr

但是unique_ptr本身不支持拷贝,所以C++11中又提供更靠谱的并且支持拷贝的shared_ptr

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

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。

  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

易错点1:引用计数应该如何表示???

(1)int类型的成员变量或者静态成员变量(错误)

(2)设成int*成员变量并指向堆区的一块空间(正确)

易错点2:拷贝构造的注意事项

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}
	//拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);//计数+1
	}
	//赋值重载
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
	  //有两种可能性 一种是自己给自己赋值 但是别人给自己赋值的话有可能指向的也是同一块空间
		if (_ptr != sp._ptr)
		{
			release();//清理一下原来的空间
		    //转移
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);//计数+1
		}
		return *this;
	}

	void release()
	{
		if (_ptr&&--(*_pcount) == 0) //为空就不进去了
		{
			//cout << "delete->" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

	~shared_ptr()
	{
		release();
	}
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//
	int use_count() const
	{
		return *_pcount;
	}
private:
	T* _ptr;
	int* _pcount;
};

附上测试代码

cpp 复制代码
void test_shared_ptr()
{
 shared_ptr<int> sp1(new int);
 shared_ptr<int> sp2(sp1);
 shared_ptr<int> sp3(sp1);

 shared_ptr<int> sp4(new int);
 shared_ptr<int> sp5(sp4);

 sp1 = sp1;
 sp1 = sp2;

 sp1 = sp4;
 sp2 = sp4;
 sp3 = sp4;

 *sp1 = 2;
 *sp2 = 3;
}

1.7 shared_ptr的循环引用问题

cpp 复制代码
struct ListNode
{
	int _data;
    shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};

void test_shared_ptr2()
{
	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;
}

循环引用的分析:

1、node1和node2析构,计数减到1,但是_next还指向下一个节点。但是_prev还指向上

一个节点。

2、所以_next析构了,那么node2就释放了。

3、而_prev析构了,那么node1就释放了。

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

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

cpp 复制代码
struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};

1.8 wake_ptr

wake_ptr的原理:为了解决shared_ptr的循环引用问题,不参与计数引用。

cpp 复制代码
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;
	};

也就是说循环引用的场景需要我们程序员自己去辨别出来,然后再使用wake_ptr来解决这个场景

1.9 shared_ptr的定制删除器

为什么需要定制删除器呢??原因是有些时候对象并不一定是new出来的。

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

    //定制删除器版本
	template<class D>    //库的底层是再包一层
	shared_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{}

	//拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);//计数+1
	}
	//赋值重载
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
	  //有两种可能性 一种是自己给自己赋值 但是别人给自己赋值的话有可能指向的也是同一块空间
		if (_ptr != sp._ptr)
		{
			release();//清理一下原来的空间
		    //转移
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);//计数+1
		}
		return *this;
	}

	void release()
	{
		if (_ptr&&--(*_pcount) == 0) //为空就不进去了
		{
			_del(_ptr);//利用删除器
			delete _pcount;
		}
	}

	~shared_ptr()
	{
		release();
	}
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//
	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcount;
	function<void(T*)> _del = [](T* ptr) {delete ptr; };//利用lambda表达式  以及包装器接受对象
};

附上测试用例:

cpp 复制代码
//定制删除器

template<class T>
struct DelArray 
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

void test_shared_ptr3()
{
	shared_ptr<ListNode> sp1(new ListNode[10], DelArray<ListNode>());//仿函数
	shared_ptr<ListNode> sp2(new ListNode[10], [](ListNode* ptr) {delete[] ptr; });//指向数组
	shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });//文件
	shared_ptr<int> sp4((int*)malloc(4), [](int* ptr) {free(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中的实现的。

可以把boost库理解成体验服

三、内存泄露

3.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:**内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。**内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

常见的内存泄露场景:

(1)内存申请忘记释放

(2)异常安全问题导致释放过程被跳过

cpp 复制代码
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;
}

3.2 内存泄漏分类

1、堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一

块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

2、系统资源泄漏

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

3.3 如何检测内存泄漏

在linux下内存泄漏检测:linux下几款内存泄漏检测工具

在windows下使用第三方工具:VLD工具说明

其他工具:内存泄漏工具比较

3.4 如何避免内存泄漏

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

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

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

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

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

相关推荐
学习前端的小z2 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹10 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE11 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
alphaTao13 分钟前
LeetCode 每日一题 2024/11/18-2024/11/24
算法·leetcode
我们的五年20 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
kitesxian22 分钟前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
zwjapple27 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five29 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
前端每日三省31 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
凡人的AI工具箱44 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang