IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++异常机制和智能指针机制的杂谈 -> 正文阅读

[C++知识库]C++异常机制和智能指针机制的杂谈

异常

认识异常

C语言中的处理异常的方式

  1. 错误码,或者全局的异常变量
  2. assert或者exit()等其他的方式去终止整个程序,但是这个问题一般比较严重

C++通过异常机制来解决问题
C++提供了一个标准的类去进行处理异常-exception
在这里插入图片描述

在编写C++代码时,除了语法错误,连接错误,可以在变成可执行程序之前发现,但是还有一些逻辑错误,可能会导致越界,程序崩溃等等。
异常 Exception 是程序可能检测到的 运行时刻不正常的情况 如被 0 除 数组越界 访问或空闲存储内存耗尽等等 这样的异常存在于程序的正常函数之外 而且要求程序立即处理。

这里面向对象语言C++提供的异常处理机制。
面对可能会出现异常的代码,可以检测出(try),然后被捕捉(catch),然后抛出(throw),这样就不会导致程序的逻辑问题,不会崩溃之类的。

当发生异常时,不能简单地结束任务,而是要退回到任务发生的起点,然后又由用户自定义执行下一步错误,即引发崩溃的就不能再运行这个指令。
在即将引发崩溃的条件下,进行抛出,然后捕捉,阻止程序崩溃。并不是等程序崩溃后再捕捉。

捕捉catch会捕捉throw抛出的特定类型的的变量,然后跳转到catch中,执行用户定义的代码

对于抛出的类型,如果没有进行特定类型的捕捉,则这个异常依旧会造成报错或者崩溃

异常造成的内存泄漏的问题

使用C++的异常机制会导致一个问题:内存泄漏。
很多人会感到疑惑和不解,C++异常机制就是用来解决这些问题的,怎么还会引发这些问题?
C++的异常机制的组成是在try块域中进行异常抛出throw的检测,然后在catch进行异常的捕获。但是这样是会打乱程序的执行流。强制程序执行其他步骤。这也是由于人的问题引发的缺陷。
如以下代码

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
void fuc1()
{
	int* arr = (int*)malloc(sizeof(int)*0x7ffffff);
	FILE* f = fopen("test.txt", "r");
	if (arr == nullptr)
	{
		throw string("开辟空间失败");
	}
	if (f == nullptr)
	{
		throw string("文件打开失败");
	}

	free(arr);
	fclose(f);
}


int main(void)
{
	
	try
	{
		fuc1();
	}
	catch (string& err)
	{
		cout <<err<< endl;
	}
	return 0;
}

看似没什么问题,在函数中既malloc了空间,也有free;对于文件操作,有open也有close。看起来没有引发内存泄漏的可能。
但是,关键就在于异常抛出上。
arr开辟空间与文件指针f打开文件后,如果malloc开辟的空间太大,导致开辟失败,或者当前目录下没有这个文件,导致打开失败,那么符合判断条件,然后就会被抛出,随即被检测到,然后被catch捕获。出现这个出现执行流的构过程。如果出现以上的某一个问题,那么函数中后面的代码就不会执行,直接跳出。比如文件打开失败,而文件指针是nullptr,这个没什么影响,但是malloc,这个没有出问题,确实在堆中开辟了一定的空间,而异常抛出导致后面代码没法执行,且malloc出来的空间也没有回收,这样就会导致内存泄漏的问题。
如果这样改进以下,就可以避免内存泄漏的问题

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
void fuc1()
{
	int* arr = (int*)malloc(sizeof(int)*0x7fffffff);
	FILE* f = fopen("test.txt", "r");
	if (arr == nullptr)
	{
		if(f!=nullptr)//判断一定要加,万一有多个变量G了怎么办
			fclose(f);
		throw string("开辟空间失败");
	}
	if (f == nullptr)
	{
		if(arr!=nullptr)
			free(arr);
		throw string("文件打开失败");
	}

	free(arr);
	fclose(f);
}


int main(void)
{
	
	try
	{
		fuc1();
	}
	catch (string& err)
	{
		cout <<err<< endl;
	}
	return 0;
}

这个解决方案虽然比较不太方便、高效,但是能用。
不过,这只是针对这些情况,如果想达到C++的简洁、高效的效果,需要定义一个类去操作。
可以选择创建一个基类,存放基本的异常信息:错误识别码,错误信息

class cz_Exception
{
public:
	cz_Exception(size_t errid,string errmess)
		:_errid(errid)
		,_errmess(errmess)
	{}
	size_t GetID(void)
	{
		return _errid;
	}
	string GetMessage(void)
	{
		return _errmess;
	}
protected:
	size_t _errid;
	string _errmess;
	//其他信息,时间,栈序列,等等
};

在其他地方使用还可以继承这个基类,使用派生类在不同地方使用不同的异常处理

异常的抛出和捕获规则

  1. 抛异常,可以抛出任意类型,但是对于捕获异常,是由抛出的异常的类型来决定被哪个catch捕获块来捕获
  2. 异常被抛出后,会一层层作用域的去与所在try块匹配的catch捕获快进行类型匹配,只要匹配到一次就会结束,最后一个作用域是main函数中的,如果都没有匹配的catch块,那么就会给系统,进而直接报错。
  3. catch(…){},可以捕获任意类型的异常,所有异常。但是关键问题是我们并不清楚捕获的异常到底是什么,所以,一般选择处理未知异常,防止因为某些疏忽造成未知异常,导致程序崩溃。
  4. 实际上的抛出和捕获并不是要求完全类型匹配,对于基类也能捕获派生类的异常,这个在项目中非常实用,所以C++利用多态的特性来使用一个类来封装异常。
  5. 对于throw抛出的异常,实际上是对异常信息的临时拷贝,因为普通的对象出了作用域会被销毁。且这个临时对象的异常信息,被捕获后也会被销毁。捕获就非常像传参调用

auto小知识

auto类型是不能用来作为参数,返回值的类型,因为编译器在编译器时无法推到到底该生成哪一个。
平常使用auto去接受某些对象时,都是能提前确定好了的,编译器在编译时会去自动推导,生成一样的类型的对象。
所以想用catch(auto e){}去替代catch(…){},是不现实的,IDE会直接报错的。

项目中的异常使用

在一个项目中,需要不同的功能模块。这个需要不同的开发者去实现。所以定义一个最基本的基本类,让开发者在不同的项目中去继承这个基类,这样在catch时可以直接使用一个catch(基类){}就能完成所有异常的捕获。比如说这个样子

class cz_Exception
{
public:
	cz_Exception(size_t errid,string errmess)
		:_errid(errid)
		,_errmess(errmess)
	{}
	//虚函数重写,让派生类方便添加其他信息进行重写
	virtual size_t GetID(void)
	{
		return _errid;
	}
	virtual string GetMessage(void)
	{
		return _errmess;
	}
protected://使用protected来封装成员,保证继承类能够得到
	size_t _errid;
	string _errmess;
	//其他信息,时间,栈序列,等等
};
//可以在派生类里面添加一些其他的protected信息,比如说:来自哪个功能..
class cz_Move_Exception :public cz_Exception
{

};
class cz_OpenCV_Exception :public cz_Exception
{

};
class cz_Vision_Exception :public cz_Exception
{

};

异常安全规范

在这里插入图片描述

异常的优缺点

优点

  1. 可以返回错误码以及更具体的错误信息
  2. 不用层层处理捕获的异常,只需要抛出-捕获即可
  3. 大项目和框架都会使用异常进行测试处理
  4. 像一些重载函数和返回值不好处理的函数,选择异常比选择断言要更好处理,可以简化错误和查找错误原因

缺点

  1. 异常会打断执行流的进度,发生错误时,会造成执行流乱跳,调试和分析时,会造成一些问题
  2. 对硬件需要一些开销,尤其是小性能的硬件.
  3. 会引发一些异常安全的问题,比如内存泄漏之类的,不过这可以靠RALL智能指针来解决
  4. 不同厂商对异常由自己的定义,有些标准混乱的问题。

异常的多次抛出

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <fstream>
using namespace std;

class cz_Exception
{
public:
	cz_Exception(size_t errid,string errmess)
		:_errid(errid)
		,_errmess(errmess)
	{}
	size_t GetID(void)
	{
		return _errid;
	}
	string GetMessage(void)
	{
		return _errmess;
	}
protected://使用protected来封装成员,保证继承类能够得到
	size_t _errid;
	string _errmess;
	//其他信息,时间,栈序列,等等
};
void fuc1()
{
	int* arr;
	FILE* f;
	try
	{
		arr = (int*)malloc(sizeof(int) * 0x7ffffff);
		f = fopen("test.txt", "r");
		if (arr == nullptr)
		{
			throw cz_Exception(1, "malloc开辟失败");
		}
		if (f == nullptr)
		{
			throw cz_Exception(2, "文件打开失败");
		}
	}
	catch (cz_Exception& err)//捕获是为了处理安全问题
	{
		if (err.GetID() == 1)
		{
			if(f != nullptr)
			{
				cout << "关闭文件" << endl;
				fclose(f);
			}
			throw err;//再次抛出,处理异常问题
		}
		if (err.GetID() == 2)
		{
			if (arr != nullptr)
			{
				cout << "回收资源" << endl;
				free(arr);
			}
			throw err;
		}
	}
	free(arr);
	fclose(f);
}
 

int main(void)
{
	
	try
	{
		fuc1();
	}
	catch (cz_Exception& err)
	{
		cout <<err.GetID()<<"  " << err.GetMessage() << endl;
	}
	return 0;
}

但是这样的操作依旧非常累赘,所以简便的解决方案就是智能指针。

智能指针

智能指针其实就是类封装。

使用一个类来维护指针,维护指针开辟的资源。由于类只能在作用域内有效,所以,使用一个类来去维护开辟的资源。
且在抛异常时,异常也算一个临时变量的拷贝,然后异常会不断进入下一个作用域栈帧,在执行流离开一个作用域,系统会清理资源

对于对象而言,离开了作用域,系统会调用析构函数来清理当前作用域栈中的对象。随着异常的抛出离开当前作用域,则就自动调用析构函数来清理资源,所以我们可以用智能指针一个类来维护用指针开辟的资源,然后再析构函数中来delete指针的资源

一个简单的智能指针

#pragma once
template <class T>
class SmartPtr
{
public:
	//RAII特性
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		std::cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
	//由于智能指针要满足一个指针的基本功能-访问,所以需要一些重载
	T* operator->()
	{
		return _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
private:
	T* _ptr;
};

由于智能指针的需求,需要想像普通指针一样,正常使用,需要能够访问指针的内容,修改指针的内容。

所以,必须需要->*的重载。

智能指针解决异常中内存泄漏的问题

#include <iostream>
#include <string>
using namespace std;
#include "SmartPtr.h"
class Exct
{
public:
	Exct(int id,string str)
		:_id(id)
		,_str(str)
	{}
	const int& GetID(void)
	{
		return _id;
	}
	const string& what(void)
	{
		return _str;
	}
private:
	int _id;
	string _str;
};
void fuc(void) throw(Exct)
{
	SmartPtr<pair<int,int>> pa(new pair<int,int>);
	SmartPtr<int> pb(new int);
	pa->first = 10;
	pa->second = 10;
	*pb = 0;
	if (*pb == 0)//如果出现除零错误,就会导致指针a,b无法delete,造成内存泄漏,选择多次 捕获抛出 或者 智能指针
		throw Exct(1, "除零错误");
	cout << "a / b = " << pa->second / *pb << endl;
}
int main()
{
	try
	{
		fuc();
	}
	catch (Exct& err)
	{
		cout <<"错误码:"<<err.GetID() << "   错误描述:" << err.what() << endl;
	}

}

效果展示
在这里插入图片描述
使用智能指针后,代码中并没有显示的专门去处理malloc之类的资源回收,但是依旧不会造成内存泄漏,这就是RAII机制,利用系统对于资源的管理,进行垃圾回收。

RAII

这种技术也叫RAII。可以利用对象的生命周期来控制程序资源(内
存,文件句柄,网络连接,互斥量等等)。
利用类的构造函数类自动获取资源,利用类的析构函数来自动释放、
清理资源,就是我们把创建的资源托管给了系统,系统会来帮我们进
行创建、删除资源的问题
好处:

  1. 不需要显式的释放资源
  2. 采用这种方式,对象再生命周期内始终有效,且不会泄漏丢失。
    像智能指针、递归互斥锁的锁首位等待,都是使用这种技术

智能指针的拷贝问题

智能指针想要达到和普通指针一样的使用效果,又希望具有RAII的特性,且指针需要解决一个关键的问题,就是指针拷贝的问题。指针的拷贝并不是深拷贝,而是浅拷贝问题。但是仅仅是普通的浅拷贝又会因为RAII特性,而造成多次析构的问题。

auto_ptr

从C++98标准开始,C++标准委员会就由auto_ptr开始解决智能指针拷贝的问题。

auto_ptr选择的是管理权转移
多个智能指针指向一个空间资源时,资源的管理权在最新的一个指针,其他智能指针为空。

unique_ptr

然后又有一个解决方案unique_ptr
解决不了问题就解决问题的源头

直接禁止智能指针拷贝和复制

unique_ptr(const unique_ptr<T>& up) = delete; unique_ptr<T> operator=(const unique_ptr<T>& up) = delete;
但是依旧达不到需求。

shared_ptr

最优秀的解决方案,来自boost库,最后被引入C++11标准
shared_ptr,运用了引用计数的方法完成普通指针的效果
在这里插入图片描述

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _ptrCount(new int(1))
	{}
	//直接禁止拷贝和赋值
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _ptrCount(sp._ptrCount)
	{
		*_ptrCount += 1;
	}
	const shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)//防止指向同一处的指针赋值
		{
			if (*_ptrCount == 1)
			{
				//可以选择-删除开辟的空间,也可以选择报错
				//cout << "该赋值会造成内存泄漏" << endl;
				//assert(false);

					//删除空间,防止内存泄漏
				cout << "operator=:删除一个空间" << endl;

				delete _ptr;
				delete _ptrCount;

			}
			//解决bug-指向某个空间的智能指针指向另外一个空间
			if (*_ptrCount > 1)//对于*_ptrCount==1的情况,本身就会造成泄漏,所以不是代码该考虑的问题
			{
				*_ptrCount -= 1;
			}
			_ptr = sp._ptr;
			_ptrCount = sp._ptrCount;
			*_ptrCount += 1;
		}
		return *this;
	}
	~shared_ptr()
	{
		cout << "~shared_ptr:";
		if (*_ptrCount == 1)
		{
			cout << "删除一个智能指针指向的空间" << endl;

			std::cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _ptrCount;
			_ptr = nullptr;

		}
		else
		{
			*_ptrCount -= 1;
			cout << "删除一个智能指针" << endl;
		}
	}

	T* operator->()
	{
		return _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
private:
	T* _ptr;
	int* _ptrCount;
	//在构造时创建一个变量,并赋值1
	//在拷贝时,指针指向拷贝过来的变量,并加1
	//在析构的时候,--,当值只有1时,就清理资源
};

为了满足线程安全的需求,使用了mutex指针来维护引用计数

template <class T>
	class shared_ptr
	{
		//对于引用计数的加减操作,使用函数来是为了更好的在多线程中操作
		void AddRef(void)
		{
			_ptrMutex->lock();
			++(*_ptrCount);
			_ptrMutex->unlock();
		}
		void ReleaseRef(void)
		{
			_ptrMutex->lock();
			bool flag = false;
			if (--(*_ptrCount) == 0)
			{
				delete _ptr;
				delete _ptrCount;
				flag = true;
			}
			_ptrMutex->unlock();
			if (flag == true)//要保证解锁后才能删除锁,否则会死锁
			{
				delete _ptrMutex;
			}
		}
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_ptrCount(new int(1))
			,_ptrMutex(new mutex)
		{}
		//直接禁止拷贝和赋值
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_ptrCount(sp._ptrCount)
			,_ptrMutex(sp._ptrMutex)//一块资源,用一个锁来维护
		{
			AddRef();
		}
		const shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//防止指向同一处的指针赋值
			{
				ReleaseRef();

	 			_ptr = sp._ptr;
				_ptrCount = sp._ptrCount;
				AddRef();
			}
			return *this;
		}
		~shared_ptr()
		{
			cout << "~shared_ptr" << endl;
			ReleaseRef();
		}

		T* operator->()
		{
			return _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
	private:
		T* _ptr;
		int* _ptrCount;
		mutex* _ptrMutex;
		//在构造时创建一个变量,并赋值1
		//在拷贝时,指针指向拷贝过来的变量,并加1
		//在析构的时候,--,当值只有1时,就清理资源
	};

使用了mutex指针来维护资源的引用计数,使用指针是为了保证一个资源有且只有一个锁。

weak_ptr

解决了智能指针循环引用的问题

什么是循环引用

在这里插入图片描述

当需要清理这两个资源时,由于都只剩下另外一块资源的指针,所以引用计数都是1
当清理这个资源1时,需要先清理内部的自定义类型的成员,清理sp1,就需要清理其指向的资源2,
清理资源2,又需要先清理sp2,清理sp2,就需要先析构资源1。

就这样,析构资源1,就需要析构sp1,析构sp1又需要清理资源2,清理资源2,有需要先析构sp2,
清理sp2,又需要先清理资源1.
这就是智能指针的循环引用问题,陷入死循环了
所以,出现循环引用的问题,就是资源的最后管理权变成了对方的,导致互相管理权交换,死循环

关键是,不要增加资源内部的智能指针造成的引用计数的增加,官方提供了weak_ptr来解决
这个可以像指针一样访问资源,但是不会增加引用计数,weak_ptr不会接受原生指针进行构造,
而是可以接受智能指针进行资源操作

weak_ptr对于资源只读不写,不会参与资源的管理,这样就不会该改变资源的引用计数。

template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const cz::shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr<T> operator=(const cz::shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		~weak_ptr()
		{
			//不参与资源的管理,只读不写
			cout << "weak_ptr" << endl;
		}

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

删除器

在这里插入图片描述

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:02:42  更:2022-07-17 16:05:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:30:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码