C++异常机制和智能指针机制的杂谈

Posted Booksort

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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._ptrMutexC++智能指针详解:智能指针的引入

正确地使用智能指针

智能指针之atuo_ptr源码剖析

浅析C++智能指针和enable_shared_from_this 机制

使用TR1的智能指针

C++ 浅析智能指针