C++小工进阶之路(智能指针上)XXIII

C++将内存的管理(使用new来申请空间)交给了用户自己来管理,所以很有可能会申请空间后忘记释放,引起内存泄漏。
举例: 没有智能指针的话需要考虑内存泄露的地方太多太多,代码太臃肿了

#include<iostream>
using namespace std;
bool Test1()
{
	//成功返回true,失败返回false
	return false;
}
void Test2()
{
	//....

	throw 1; //抛出异常
}
void TestFunc()
{
	int *p = new int[10];
	FILE* fd = fopen("aaa.txt", "rb");
	if (fd == nullptr)
	{
		delete[] p;
		return;
	}
	if (!Test1())
	{
		delete[] p;
		fclose(fd);
		return;
	}
	try
	{
		Test2();
	}
	catch (...)
	{
		delete[] p;
		fclose(fd);
		throw;
	}
	//...
	
	//假如上面执行顺利,来到代码的末端
	delete[] p;
	fclose(fd);
}
int main()
{
	TestFunc();
	return 0;
}

原生态的指针:T*
如果采用原生态的指针来管理资源,那么程序存在资源泄漏的风险比较大
使用原生态指针容易产生内存泄漏,能否让程序自我感知,让对象在销毁的时候自己去释放空间。

RAII(资源获取及初始化)
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源的简单技术
巧妙地利用了类中的构造函数和析构函数。

//RAII --- 资源获取及初始化
//在构造函数中放资源
//在析构函数中释放资源

//智能指针的简单模拟实现
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
	:_ptr(ptr)
	{
		cout << "SmartPtr" << endl;
	}
	~SmartPtr()
	{
		cout << "~SmartPtr" << endl;
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;  //加不加这句都可以,因为析构结束,对象已经销毁,成员变量也就不存在了
		}
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
struct A
{
	int a = 100;
	int b = 200;
	int c = 300;
};
int main()
{
	SmartPtr<int> sp(new int);
	*sp = 10;
	cout << *sp << endl;
	SmartPtr<A> sp1(new A);
	cout << sp1->a << endl;;
	return 0;
}

RAII:用户不用考虑什么时候该释放资源,把释放资源的时机,交给了编译器
上面代码存在的缺陷:
前面string引出浅拷贝的问题—》我们是通过深拷贝来解决的
但是在智能指针中我们却不能通过深拷贝的方式来解决。
原因:
因为只能指针的出现只是为了解决用户对空间的忘记释放问题,用户在使用智能指针的时候并不知道智能指针内部的实现,所以实现智能指针的时候不能采用深拷贝,因为在实在用户并不知情的情况下开辟的空间,加入用户空间本来就不多的情况下,智能指针内部申请空间会使用户感到迷茫,一句话就是,在智能指针内部可以使用深拷贝来解决浅拷贝问题但是因为智能指针只是管理空间,所以不能够开辟空间。
所有不同类型的智能指针:(都必须解决的问题)

  • RAII:资源可以自动释放
  • 类对象具有指针类似的行为:operator* operator->
  • 浅拷贝的解决方式

C++98中想到了一个方式:auto_ptr
实现:

namespace vs
{
	template<class T>
	class auto_ptr
	{
	public:
		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<T>& ap)  //这也就是auto_ptr解决浅拷贝的方式也就是资源的转移
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;   //让原对象指向空
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ps)
		{
			if (this != &ps)
			{
				if (!this)   //如果当前对象已经管理资源了,就要先把当前对象的资源先释放
					delete _ptr;
				_ptr = ps._ptr;
				ps._ptr = nullptr;
			}
			return *this;
		}
	private:
		T* _ptr;
	};
}
void TestAuto_ptr()
{
	vs::auto_ptr<int> ps1(new int);
	vs::auto_ptr<int> ps2(ps1);  //因为ps2是最后创建的,auto_ptr就会猜测之前的ps1你可能不用了,然后在智能指针内部,让指针指向空
	//*ps1 = 10;   指针指向空之后,这个语句报错,此时ps1已经是个空对象
	vs::auto_ptr<int> ps3;
	ps3 = ps2;
}
int main()
{
	TestAuto_ptr();
	return 0;
}

在这里插入图片描述
为什么要使用智能指针来管理申请的空间,直接申请的空间要等到程序结束,或者用户手动释放,资源才会从内存中被清理掉,但是用户会忘记释放,再加上因为异常的缘故,往往让程序推出的原因很对,有时候会释放资源的地点考虑的不够完全,这样的话写出的代码都是有可能引起资源泄漏的,所以才有了智能指针。
在这里插入图片描述

那么又该如何解决资源的转移带来的问题就是,之前的对象将不再对这块空间可以使用的权力,假如不让之前的对象指向空的话 ,那么再释放的时候又有多个指针指向一块空间,释放的时候就会引起对此释放代码崩溃。

所以就引入了一个标记位
增加一个成员变量bool _owner;
但一个对象发生资源转移之后那么这块对象,将不再具备对这块空间的释放的权力,此时_owner变为 false,当一个对象具有对共享的资源有释放的权利的时候,此时_owner才是true.

那么auto_ptr中的代码就要这样写:

对上面的auto_ptr的改进:
//析构函数
~auto_ptr()
{
	if(_ptr && _owner)   //资源存在,切拥有释放权
	{
		delete _ptr;
		_ptr = nullptr;
	}
}
// 拷贝构造函数
auto_ptr<T>& operator=(auto_ptr<T>& ps)
	:_ptr(ap._ptr)
	,_owner(true)
{
	ap._owner = false;
}
//赋值运算符重载:
auto_ptr<T>& operator=(auto_ptr<T>& ps)
{
	if (this != &ps)
	{
		if (!this && _owner == true)   //如果当前对象已经管理资源了,就要先把当前对象的资源先释放
			delete _ptr;
		_ptr = ps._ptr;
		_owner = true;
		ps._owner = false;
	}
	return *this;
}

改进后的auto_ptr的实现原理:
RAII + operator*() + operator->() + _owner

但是_owner版本的auto_ptr缺陷:可能会造成野指针–而且还有可能造成代码崩溃

为什么说容易产生野指针呢?就是因为假如有多个对象指向一个资源,虽然现在只有一个对象对这个资源有释放的权力,假如这个资源被释放了,此时继续引用这个资源的其他对象在不知道的情况下,就无法在使用这个内存资源,此时这些对象里面的指针就变成了野指针。

第二个版本虽然解决了第一个版本的不能引用多个对象的问题,但是又引入了新的更严重的问题,就是野指针,所以标准的auto_ptr使用的就是资源的转移
所以给出的建议是,什么情况下都不要使用auto_ptr

由平时普通的指针来理解智能指针:
在这里插入图片描述
无论智能指针是以什么方式实现的:

智能指针的原理:RAII (自动释放资源) + operator* + operator-> (保持原生态的功能) + 解决浅拷贝

这里说说的解决浅拷贝,为的就是防止在离开作用域的时候,智能指针多次释放资源而引起的崩溃。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//C++11:auto_ptr---->保留:资源转移实现
//就是为了防止之前其他人写的代码中使用了auto_ptr编译不能通过
//C++11提供了更靠谱的智能指针
//unique_ptr 唯一的指针,
//浅拷贝发生的原因就是拷贝构造函数和赋值运算符引起的,所以unique_ptr就是不提供这两个函数,就没法进行浅拷贝
template<class T>
class DFDef
{
public:
	void operator()(T*& p)
	{
		if (p)
		{
			delete p;
			p = nullptr;
		}
	}
};
template<class T>
class Free
{
public:
	void operator()(T* &p)
	{
		if (p)
		{
			free(p);
			p = nullptr;
		}
	}
};
class FClose
{
public:
	void operator()(FILE*& p)
	{
		if (p)
		{
			fclose(p);
		}
	}
};
namespace vs
{
	template<class T, class DF = DFDef<T>>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				//delete _ptr;  //释放资源的方式固定死了,不能处理任意类型的资源,只能处理new类型的空间
				//DF()(_ptr);
				//上面这句代码 分成两句也就是
				DF df;
				df(_ptr);
				_ptr = nullptr;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//解决浅拷贝:禁止调用拷贝构造函数,禁止调用拷贝构造函数
		unique_ptr(const unique_ptr<T>&) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
	private:
		T* _ptr;
	};
}
void TestUniquePtr()
{
	vs::unique_ptr<int> up1(new int);
	vs::unique_ptr<int, Free<int>> up2((int*)malloc(sizeof(int)));
	vs::unique_ptr<FILE, FClose> up3(fopen("1.txt","w"));

}
int main()
{
	TestUniquePtr();
	return 0;
}
发布了230 篇原创文章 · 获赞 28 · 访问量 9337

猜你喜欢

转载自blog.csdn.net/weixin_43767691/article/details/103159812