C++程序设计——智能指针

一、内存泄漏简介

1.什么是内存泄漏

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

2.内存泄漏的危害

        长期运行的程序出现内存泄漏,会造成较大的影响,比如操作系统、服务器等,出现内存泄漏会导致响应越来越慢,最终程序卡死等情况。

3.内存泄漏的分类

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

(1)堆内存泄漏(Heap Leak)

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

(2)系统资源泄漏:

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

4.如何避免内存泄漏

(1)工程前期良好的设计规范,养成良好的编码规范,申请了空间后就记得要匹配的去释放。

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

(3)出现问题了使用内存泄漏工具进行检测。

等等……

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

1. RAII

        RAII是一种利用对象生命周期来控制程序资源的简单技术。

        在对象构造时获取资源,然后控制对资源的访问,使之在对象的生命周期内始终保持有效;最后在对象析构的时候释放资源

相当于把管理一份资源的责任托管给了一个对象,这样做有两大好处:

(1)不需要显示的释放资源;

(2)对象所需的资源在其生命周期内始终保持有效。

//设计思想---->不完善
template
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr) 
	{}
	~SmartPtr() {
		if (_ptr) {
			delete _ptr;
			_ptr = nullptr;
		}
	}

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

private:
	T* _ptr;
};
//没有显示实现拷贝构造函数、赋值运算符重载,C++编译器会默认自动生成
//默认生成的拷贝构造函数、赋值运算符重载,采用的是浅拷贝
//通过SmartPtr对象构造另一个对象或赋值时,会导致程序运行崩溃

2.智能指针的原理

(1)RAII特性;

(2)重载operator*和operator->,使对象具有像指针一样的行为;

(3)解决浅拷贝方法。

三、标准库中的智能指针

1. std::auto_ptr

auto_ptr是C++98版本库中提供的智能指针。

1.1 初始版本

实现原理:

(1)RAII特性;

(2)重载operator*和operator->,使对象具有像指针一样的行为;

(3)资源转移的思想,解决浅拷贝。

资源转移思想:

        一旦发生拷贝,就将原对象管理的资源转移到新对象中,旧对象就失去了对资源联系,这样就解决了一块空间被多个对象使用而造成程序崩溃问题。

缺陷:

        不符合我们的使用习惯,对于不熟悉其特性的使用者,可能造成程序崩溃。

模拟实现:

namespace AutoPtr {
	template
	class auto_ptr {
	public:
		//RAII
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
		~auto_ptr() {
			if (_ptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//指针类似行为
		T& operator*() {
			return *_ptr;
		}
		T* operator->() {
			return _ptr;
		}
		//解决浅拷贝--》资源转移思想
		auto_ptr(auto_ptr& ap)
			: _ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		auto_ptr& operator=(auto_ptr& ap) {
			if (this != &ap) {
				//转移之前,若当前对象已经管理了资源,需要先将其释放,再接收新资源
				//否则会造成资源泄漏
				if (_ptr) {
					delete _ptr;
				}
				//转移资源
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		T* Get() {
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

1.2 改进版本

技术勘误报告:03标准对auto_ptr进行了改进

实现原理:

(1)RAII特性;

(2)重载operator*和operator->,使对象具有像指针一样的行为;

(3)资源管理权限转移思想,解决浅拷贝。

资源管理权限转移思想:

        发生拷贝时,进行资源拷贝,然后转移资源管理权限(对资源释放的权力)。这样虽然是浅拷贝,但是只有一个对象具有资源的管理权限(对资源释放的权力),就避免了多次释放造成程序崩溃。

缺陷:

(1)致命缺陷:可能导致野指针

        比如一个对象获得了资源管理权限,但是该对象的生命周期比其他共有该资源的对象短,那么该对象在生命周期结束时对资源进行了释放,其他对象内部就成了野指针。

C++程序设计——智能指针_第1张图片

(2)多个对象内指向的是同一个资源,一个对象对其进行了修改,其他对象内也会体现。

所以C++11又将资源管理权限转移的方式,跳回了资源转移的方式。

模拟实现:

namespace AutoPtr_II {
	template
	class auto_ptr {
	public:
		//RAII
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _owner(false)
		{
			if (_ptr) _owner = true;
		}
		~auto_ptr() {
			//管理了资源,并且用于管理权限,才释放资源
			if (_ptr && _owner) {
				delete _ptr;
				_ptr = nullptr;
				_owner = false;
			}
		}
		//指针类似行为
		T& operator*() {
			return *_ptr;
		}
		T* operator->() {
			return _ptr;
		}
		//解决浅拷贝--》资源管理权限转移思想
		auto_ptr(auto_ptr& ap)
			: _ptr(ap._ptr)
			, _owner(ap._owner)
		{
			//转移管理权限
			ap._owner = false;
		}
		auto_ptr& operator=(auto_ptr& ap) {
			if (this != &ap) {
				//转移之前,若当前对象已经管理了资源,需要先将其释放,再接收新资源
				//否则会造成资源泄漏
				if (_ptr && _owner) {//同理,有管理权限才释放
					delete _ptr;
				}
				//拷贝资源,转移权限
				_ptr = ap._ptr;
				_owner = ap._owner;
				ap._owner = false;
			}
			return *this;
		}

		T* Get() {
			return _ptr;
		}
	private:
		T* _ptr;
		//mutable修饰成员变量,
		//表明const类型对象,或const成员函数内部,可以对该变量进行修改
		mutable bool _owner;
	};
}

2. std::unique_ptr

unique_ptr是C++11版本提供的更靠谱的智能指针。

实现原理:

(1)RAII特性;

(2)重载operator*和operator->,使对象具有像指针一样的行为;

(3)防止拷贝的思想,解决浅拷贝。即资源只能被一个对象管理。

使用场景:

        资源确定不会被共享时。

防止拷贝方法:

(1)C++98:将拷贝构造函数和赋值运算符重载只声明不定义,且设置为private访问权限。

(2)C++11:在要禁止编译器默认生成成员函数的原型后跟上=delete,编译器就不会再自动生成对应的默认成员函数。

针对不同资源释放方法:

        定制删除器,资源释放方法由用户通过模板参数列表传入,默认为delete方式。

缺陷:资源不能共享

模拟实现:

#pragma once

//默认释放方式-->new的资源
template
class DFDef {
public:
	void operator()(T*& ptr) {
		if (ptr) {
			delete ptr;
			ptr = nullptr;
		}
	}
};
template
class Free {//处理malloc的资源
public:
	void operator()(T*& ptr) {
		if (ptr) {
			free(ptr);
			ptr = nullptr;
		}
	}
};

class FClose {//处理fopen打开的文件指针
public:
	void operator()(FILE*& pf) {
		if (pf) {
			fclose(pf);
			pf = nullptr;
		}
	}
};

namespace UniquePtr {
	template>
	class unique_ptr {
	public:
		//RAII
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
		~unique_ptr() {
			if (_ptr) {
				//delete _ptr;//不能直接delete,应该根据资源申请的方式,来进行对应的释放方式
				//解决方法:定制删除器,释放方法由用户传入
				DF()(_ptr);
				_ptr = nullptr;
			}
		}
		//指针类似行为
		T& operator*() {
			return *_ptr;
		}
		T* operator->() {
			return _ptr;
		}
		//解决浅拷贝--》防止拷贝-->资源独占
		// 方法1---C++11
		//在要禁止编译器默认生成成员函数的原型后跟上=delete
		//编译器就不会再自动生成对应的默认成员函数
		//unique_ptr(const unique_ptr&) = delete;
		//unique_ptr& operator=(const unique_ptr&) = delete;

		//方法2---C++98
		//将拷贝构造函数和赋值运算符重载只声明不定义,且设置为private访问权限
	private:
		unique_ptr(const unique_ptr&);
		unique_ptr& operator=(const unique_ptr&);

	public:
		T* Get() {
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

3. std:shared_ptr

3.1 实现原理

shared_ptr是C++11版本提供的更靠谱的,并且支持拷贝的智能指针。

shared_ptr的原理:

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

实现原理:

(1)RAII特性;

(2)重载operator*和operator->,使对象具有像指针一样的行为;

(3)引用计数的思想,解决浅拷贝:

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

在对象被销毁时,说明该对象不再使用该资源,对象的引用计数减一

③如果引用计数为0,说明该对象是最后一个使用该资源的对象,在销毁时必须释放资源。

④如果引用计数不为0,说明除了自己外,还有其他对象在使用该资源,自己销毁时就不能释放资源,否则其他对象内部就成了野指针。

3.2 线程安全问题

        智能指针对象中的引用计数,是多个智能指针对象所共享的,而对引用计数的++、--操作不是原子操作,多线程环境下就可能对引用计数计数错乱,导致资源没有释放或程序崩溃问题。

        所以对智能指针对象中的引用计数的++、--操作需要加锁保护,确保对引用计数的操作安全性。

注意:shared_ptr保证多线程环境下,对引用计数的操作安全性;但是不保证用户对资源的操作安全性。

3.3 shared_ptr的循环引用

循环引用问题:

        比如:在一个双向链表中,对与每个节点资源通过shared_ptr进行管理,节点内部的next和prev指针域也更换为shared_ptr,那么两个相邻的节点的情况就应该是如下:

C++程序设计——智能指针_第2张图片

 释放链表节点sp2、sp1:

(1)释放sp2时:发现引用计数减一后为1,不为0,那么就不会释放资源。

(2)释放sp1时:发现引用计数减一后为1,不为0,那么就不会释放资源。

最终导致两个节点实际上都没有被释放,造成资源泄漏

解决方式:weak_ptr

注意:

(1)weak_ptr不能单独管理资源(也不能初始化为nullptr);

(2)weak_ptr也是通过引用计数的原理实现;

(3)weak_ptr存在的唯一作用,就是解决shared_ptr的循环引用问题。

解决原理:

(1)两个双向链表未连接时的初始状态(链表内部的next和prev指针采用的是weak_ptr):C++程序设计——智能指针_第3张图片

ps:图内右边的data给错了,应该是10,不影响原理理解

 (2)将两个节点通过next和prev指针域进行连接,使用weak_ptr连接时,引用计数增加的是weak:

C++程序设计——智能指针_第4张图片

 释放链表节点顺序sp2、sp1:

(1)释放sp2时:因为sp2是shared_ptr,所以对use引用计数减一,use变为0,那么说明该资源不能再被使用,会进行释放:先清理内部资源,若遇到weak_ptr且管理了资源,会对其指向的引用计数的weak减一,减一后若引用计数内的use、weak都是0,则会释放掉引用计数资源;随后在释放节点前,会对其引用计数内的weak-1,若weak-1后也为0,则会将引用计数资源也释放掉。

所以sp2在完成释放工作后,左边的引用计数use=1,weak-1=1,节点资源、引用计数资源还存在;右边的引用计数use为0,weak=1,节点被释放,引用计数资源还存在。

(2)释放sp1时:同理按照上述的释放方式。

最终两个节点资源以及引用计数资源都被释放。

3.4 shared_ptr模拟实现

#pragma once
#include 
#include 
//默认释放方式-->new的资源
template
class DFDef {
public:
	void operator()(T*& ptr) {
		if (ptr) {
			delete ptr;
			ptr = nullptr;
		}
	}
};
template
class Free {//处理malloc的资源
public:
	void operator()(T*& ptr) {
		if (ptr) {
			free(ptr);
			ptr = nullptr;
		}
	}
};

class FClose {//处理fopen打开的文件指针
public:
	void operator()(FILE*& pf) {
		if (pf) {
			fclose(pf);
			pf = nullptr;
		}
	}
};

namespace SharedPtr {
	template>
	class shared_ptr {
	public:
		//RAII
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(nullptr)
			, _mutex(nullptr)
		{
			if (_ptr) {
				_pCount = new int(1);
				_mutex = new std::mutex;
			}
		}
		~shared_ptr() {
			release();
		}

		//指针类型行为
		T& operator*() {
			return *_ptr;
		}
		T* operator->() {
			return _ptr;
		}

		//解决浅拷贝--->引用计数
		shared_ptr(const shared_ptr& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
			, _mutex(sp._mutex)
		{
			AddRef();
		}
		shared_ptr& operator=(const shared_ptr& sp) {
			if (this != &sp) {
				//若当前对象已经管理资源
				release();

				//共享sp资源
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_mutex = sp._mutex;
				AddRef();
			}
			return *this;
		}

		int use_count() const {//返回引用计数
			return *_pCount;
		}
		T* Get() {
			return _ptr;
		}
	private:
		void release() {//资源释放
			//资源存在,且计数减一后为0,说明已经没有对象在使用资源
			if (_ptr && 0 == SubRef()) {
				DF()(_ptr);
				_ptr = nullptr;
				delete _pCount;
				_pCount = nullptr;
				delete _mutex;
				_mutex = nullptr;
			}
		}

		//++、--操作不是原子操作,需要加锁保护
		void AddRef() {//引用计数加一
			if (_pCount) {
				_mutex->lock();
				++(*_pCount);
				_mutex->unlock();
			}
		}
		int& SubRef() {//引用计数减一
			if (_pCount) {
				_mutex->lock();
				--(*_pCount);
				_mutex->unlock();
			}
			return *_pCount;
		}
	private:
		T* _ptr;
		int* _pCount;
		std::mutex* _mutex;//保证多线程环境下,对引用计数的操作安全性
		//但是不保证用户对资源的操作安全性
	};
}

4. 应用场景

auto_ptr:建议什么情况下都不要使用。

unique_ptr:资源确定不需要被共享情况下,可以使用。

shared_ptr:资源共享类型的智能指针

weak_ptr:搭配shared_ptr使用,解决shared_ptr循环引用问题。

你可能感兴趣的:(C++程序设计,c++)