C++第四十八弹---深入理解智能指针:自动内存管理的艺术

✨个人主页:熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】

目录

[1. 为什么需要智能指针?](#1. 为什么需要智能指针?)

[2. 内存泄漏](#2. 内存泄漏)

[2.1 什么是内存泄漏](#2.1 什么是内存泄漏)

[2.2 内存泄漏的危害](#2.2 内存泄漏的危害)

[2.3 内存泄漏分类](#2.3 内存泄漏分类)

[2.4 如何检测内存泄漏](#2.4 如何检测内存泄漏)

[2.4 如何避免内存泄漏](#2.4 如何避免内存泄漏)

3.智能指针的使用及原理

[3.1 RAII](#3.1 RAII)

[3.2 智能指针的原理](#3.2 智能指针的原理)

[3.3 std::auto_ptr](#3.3 std::auto_ptr)

[3.4 std::unique_ptr](#3.4 std::unique_ptr)

[3.5 std::shared_ptr](#3.5 std::shared_ptr)

[3.6 lin::weak_ptr](#3.6 lin::weak_ptr)

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


1. 为什么需要智能指针?

下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析Func函数中的问题。

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;
}

1、如果p1这里new 抛异常会如何?

如果p1这里抛异常,我们不需要进行管理,因为空间没有开辟成功,不需要释放空间,也就没有问题

2、如果p2这里new 抛异常会如何?

如果p2这里抛异常,会直接跳到main函数中的catch中,导致p1没有被释放,不再使用的空间不释放会造成内存泄漏问题。

3、如果div调用这里又会抛异常会如何?

如果div调用这里抛异常,会直接会直接跳到main函数中的catch中,导致p1 和 p2都没有被释放,同样造成内存泄漏问题。

2. 内存泄漏

2.1 什么是内存泄漏

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

2.2 内存泄漏的危害

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

内存泄漏举例

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;
}

2.3 内存泄漏分类

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

  • 堆内存泄漏(Heap leak)

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

  • 系统资源泄漏

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

2.4 如何检测内存泄漏

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

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

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

2.4 如何避免内存泄漏

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

总结:

内存泄漏非常常见,解决方案分为两种:

1、事前预防型 。如智能指针等。2、事后查错型。如泄漏检测工具。

3.智能指针的使用及原理

3.1 RAII

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

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

代码演示

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
        if(_ptr)
        {
	    	cout << "delete:" << _ptr << endl;
		    delete[] _ptr;
        }
	}
private:
	T* _ptr;
};

// 智能指针解决抛异常问题
void func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

3.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用。

代码演示

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
private:
	T* _ptr;
};
struct Date
{
	int _year;
	int _month;
	int _day;
};
int main()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 10;
	cout << *sp1 << endl;
	SmartPtr<Date> sp2(new Date);
	// 需要注意的是这里应该是sp2.operator->()->_year = 2018;
	// 本来应该是sp2->->_year这里语法上为了可读性,省略了一个->
	sp2->_year = 2018;
	sp2->_month = 1;
	sp2->_day = 1;
}

运行结果

总结一下智能指针的原理:

    1. RAII特性
    1. 重载operator*和opertaor->,具有像指针一样的行为。

3.3 std::auto_ptr

std::auto_ptr文档
C++98版本的库中就提供了auto_ptr的智能指针。 下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理管理权转移的思想,使用标准库的auto_ptr。

代码演示

int main()
{
	auto_ptr<int> sp1(new int(1));
	// 拷贝构造 C++98 转移控制权,sp1被置空
	auto_ptr<int> sp2(sp1);

	// 修改值崩溃
	//*sp1+=1;
	return 0;
}

运行结果

下面简化模拟实现了一份lin::auto_ptr来了解它的原理。

// C++98 管理权转移  auto_ptr
namespace lin
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(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)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

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

3.4 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
unique_ptr文档
unique_ptr的实现原理 :**简单粗暴的防拷贝。**使用标准库的unique_ptr。

代码演示

class A
{
public:
	A(int a1 = 0,int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1;
	int _a2;
};
int main()
{
	unique_ptr<int> sp1(new int(1));
	// 不允许拷贝构造和赋值
	//unique_ptr<int> sp2(sp1);
	unique_ptr<int> sp2(new int(10));
	//sp2 = sp1;

	unique_ptr<A> sp3(new A(1, 1));
	A* p = sp3.get();
	cout << p << endl;

	// 成员变量公有
	sp3->_a1++;
	sp3->_a2++;

	unique_ptr<A[]> sp4(new A[10]);
	sp4[0]._a1++;

	return 0;
}

运行结果

使用拷贝或者赋值报错

下面简化模拟实现了一份lin::unique_ptr来了解它的原理。

// 原理:简单粗暴 -- 防拷贝
namespace lin
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		unique_ptr(const unique_ptr<T>&sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
	private:
		T* _ptr;
	};
}

3.5 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
std::shared_ptr文档
shared_ptr的原理 :是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

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

代码演示

class A
{
public:
	A(int a1 = 0,int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1;
	int _a2;
};

int main()
{
	shared_ptr<A> sp1(new A);
	// 支持拷贝构造和赋值,使用引用计数
	shared_ptr<A> sp2(sp1);

	shared_ptr<A> sp3 = make_shared<A>(1,1);

	cout << sp1.use_count() << endl;
	{
		shared_ptr<A> sp3(sp1);
		cout << sp1.use_count() << endl;
	}

	cout << sp1.use_count() << endl;

	return 0;
}

运行结果

shared_ptr.h

#include<atomic>
#include<functional>

// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace lin
{
	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);
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				// 最后一个管理的对象释放资源
				//delete _ptr;
				_del(_ptr);
				delete _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;
		}

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

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

		int use_count()
		{
			return *_pcount;
		}

		~shared_ptr()
		{
			release();
		}
	private:
		T* _ptr;
		atomic<int>* _pcount;
		// 包装器
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
}

main函数

#include "shared_ptr.h"

int main()
{
	lin::shared_ptr<A> sp1(new A);
	lin::shared_ptr<A> sp2(sp1);
	cout << sp1.use_count() << endl;

	lin::shared_ptr<A> sp3(new A(2,2));
	lin::shared_ptr<A> sp4(sp3);
	lin::shared_ptr<A> sp5(sp4);
	cout << sp3.use_count() << endl;

	// sp1指向的空间被覆盖
	sp1 = sp3;
	sp2 = sp3;

	// 自己给自己的资源赋值
	lin::shared_ptr<A> sp6(new A(3, 3));
	sp6 = sp6;

	// 实际也是自己给自己资源赋值,且两个地址不相同
	sp3 = sp4;
	return 0;
}

运行结果

std::shared_ptr的线程安全问题

通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

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

代码演示

mutex mtx;
// 智能指针对象本身拷贝析构是线程安全的
// 底层引用计数加减是线程安全的
// 指向的资源访问不是线程安全的

void func(lin::shared_ptr<list<int>> sp,int n)
{
	cout << sp.use_count() << endl;
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		lin::shared_ptr<list<int>> copy1(sp);
		lin::shared_ptr<list<int>> copy2(sp);
		lin::shared_ptr<list<int>> copy3(sp);

		sp->emplace_back(i);
	}
	mtx.unlock();
}
// 参数使用原子则是线程安全的

int main()
{
	lin::shared_ptr<list<int>> sp1(new list<int>);
	cout << sp1.use_count() << endl;

	thread t1(func, sp1, 1000000);
	thread t2(func, sp1, 2000000);

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

	cout << sp1->size() << endl;
	cout << sp1.use_count() << endl;
	return 0;
}

运行结果

std::shared_ptr的循环引用

代码演示

// 循环引用
struct Node
{
	std::shared_ptr<Node> _next;
	std::shared_ptr<Node> _prev;
	int _val;

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


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

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

	// 两个同时连接会导致循环引用,解决办法使用weak_ptr
	p1->_next = p2;// p2有两份
	p2->_prev = p1;// p1有两份

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

	return 0;
}

运行结果

循环引用分析:

    1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
    1. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
    1. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
    1. 也就是说_next析构了,node2就释放了。
    1. 也就是说_prev析构了,node1就释放了。
    1. 但是_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 Node
{
	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;
	int _val;

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


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

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

	// 两个同时连接会导致循环引用,解决办法使用weak_ptr
	p1->_next = p2;// p2有两份
	p2->_prev = p1;// p1有两份

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

	return 0;
}

运行结果

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这

个问题.

代码演示

// 仿函数的删除器
template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template<class T>
struct DeleteArrayFunc {
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	FreeFunc<int> freeFunc;
	std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
	DeleteArrayFunc<int> deleteArrayFunc;
	std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);


	std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
	std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
		{fclose(p); });
	return 0;
}

运行结果

注意:shared_ptr模拟实现也是添加删除器的版本。

3.6 lin::weak_ptr

// 简化版本的weak_ptr实现
namespace lin
{
	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;
	};
}

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

相关推荐
记录成长java23 分钟前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山23 分钟前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn26 分钟前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
青花瓷26 分钟前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~27 分钟前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程28 分钟前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错29 分钟前
C语言扫雷小游戏
c语言·开发语言·算法
小屁孩大帅-杨一凡1 小时前
java后端请求想接收多个对象入参的数据
java·开发语言
m0_656974741 小时前
C#中的集合类及其使用
开发语言·c#
java1234_小锋1 小时前
使用 RabbitMQ 有什么好处?
java·开发语言