【C++】异常

目录

C语言处理错误的方式

传统的错误处理机制

C++异常概念

三个关键字

throw

try

catch

语法

示例一

示例二

示例三

示例四

异常的抛出与匹配规则

异常的重新抛出

异常安全问题

异常的接口声明

自定义异常体系


C语言处理错误的方式

传统的错误处理机制

  • 方式一: 终止程序,例如assert,assert用于检测程序中的逻辑错误,若某个整型表达式的值为false,则程序会抛出一个Assertion failed的错误并且终止程序运行;assert语法如下:
//c语言
#include <assert.h>
void assert(int expression);
//expression为待检测的表达式,若expression的值为false,则assert宏会输出错误信息并终止程序

 缺陷:用户难以接受,当发生内存错误,除0错误时便终止程序;

示例:

#include<stdio.h>
#include <assert.h>
int main()
{
	int a = 10, b = 20;
	assert(a > b);//此处抛出Assertion failed错误
	printf("a is greater than b\n");
	return 0;
}

运行结果:

  •  方式二:返回错误码,C语言中常见的错误码一般是通过函数返回值来表示的,通常情况下,0表示函数执行成功,而其他值则表示函数执行失败或出现异常,常见的错误码包括但不限于以下几种:

    1.  errno:表示系统调用出现错误,此时errno变量会被赋值为一个非零值,具体的错误信息可以通过perror函数来输出;

    2.  返回负数:表示函数执行出现错误,具体的错误码可以通过查看函数文档或者头文件中的宏定义来了解;

    3.  NULL指针:表示指针为空,0~255之间的内存编号是系统占用的内存,不允许访问的内存地址;

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main() 
{
	FILE * fp = fopen("data.txt", "r");
	if (fp == NULL) 
	{
		printf("Failed to open file: %d\n", errno);
		exit(-1);
	}

	//...

	fclose(fp);
	fp=NULL;
	return 0;
}

运行结果:

缺陷:需要程序员自己去查找对应的错误;系统的很多库的接口函数都是通过把错误码放到errno中,表示错误,由于函数的返回值只有1个,无法判断返回值表示错误代码或正常结果,可能产生二义性

C++针对C语言错误处理机制的不足,引入了异常,不会终止程序并且会详细介绍错误信息;

int Division()
{
	int a = 0;
	int b = 0;
	cin >> a >> b;
	if (b == 0)
	{
		throw "除0错误";//抛出异常
	}
	return a / b;
}

int main()
{
	try
	{
		cout << Division() << endl;//可能会出现异常代码
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;//捕获异常
	}
        cout<<"=============="<<endl;
	return 0;
}

运行结果:

C++异常概念

异常是面向对象语言处理错误的一种方式,当一个函数出现自己无法处理的错误时,可以抛出异常,然后由函数的直接或间接的调用者处理这个错误;

三个关键字

throw

  • throw: 发生错误时程序会通过使用throw关键字抛出一个异常;throw语句本质为跳转,即命令程序跳转到另一条语句,throw关键字表示引发异常,紧随其后的值指出异常的特征;
  • throw 内置类型数据,表示抛出内置类型数据的异常;如throw "unknow exception",表示抛出const char*类型异常;
  • throw 自定义类型的匿名对象,表示抛出自定义类型的异常;如throw string(),表示抛出string类型的异常;

try

  • try: try块包含可能会引发异常的代码当try块内的代码执行过程中遇到异常时,程序不会继续执行try块之后的代码,而是跳转到相应的异常处理代码块即catch块,catch块中可以对异常进行处理或者继续抛出,以保证程序的稳定运行;
  • try块后面通常跟一个或多个catch块,若try块代码执行期间未产生异常,则try块后的所有catch子句均不会执行

catch

  • catch: catch块用于捕获try块中抛出的异常并进行相应的处理,catch语句必须紧跟在try语句之后,并且可以有多个catch块,分别用于捕获不同类型的异常
  • catch(内置类型数据) { }捕获抛出的内置类型数据的异常,如catch(int)可捕获整型数据的异常;
  • catch(exception e){ }   可捕获自定义类型对象的异常,如捕获自定义类的匿名对象;
  • catch(...){ } 可以捕获任何类型的异常,通常用于最后一个catch语句,表示捕获除其它catch块异常类型之外的任何类型异常;
  • 若不存在与异常类型相匹配的catch语句,则系统会自动调用terminate函数,该函数内部调用abort()函数,使程序终止/中断;

语法

try
{
	//保护代码(可能抛出异常的代码)
}
catch (ExceptionName1 e1)
{
	//处理内置类型ExceptionName1异常的代码
}
catch (ExceptionName2 e2)
{
	//处理自定义类型ExceptionName2异常的代码
}
catch (...)		//省略号...表示捕获任何类型的异常
{
	//处理任何异常的代码
}

// 若程序运行期间出现异常且未被捕获,则系统会自动调用terminate函数,使程序终止/中断 

示例一

int Division(int a,int b)
{
	if (b == 0)
	{
		throw 1;//表示抛出int类型异常
	}
	return a / b;
}

int main()
{
	try
	{
		cout << Division(10,0) << endl;//可能会出现异常代码
	}
	catch (int errmsg)
	{
		cout << "捕获int类型异常:errmsg=" << errmsg << endl;//捕获异常
	}
	catch (...)
	{
		//捕获任意类型的异常(发挥兜底作用)
		//处理异常
		//...
	}
	cout << "==========================" << endl;
	return 0;
}

运行结果:

示例二

//自定义类型异常
class Exception 
{
public:
	void what() 
	{
		cout << "自定义类型异常" << endl;
	}
};

int Division(int a,int b)
{
	if (b == 0)
	{
		throw Exception();//使用匿名对象,表示抛出自定义类型的异常
	}
	return a / b;
}

int main()
{
	try
	{
		cout << Division(10,0) << endl;
	}
	catch (int errmsg)
	{
		cout << "捕获int类型异常:errmsg=" << errmsg << endl;//捕获异常(未捕获)
	}
	catch (...)
	{
		//捕获任意类型的异常(此处可捕获Exception类型异常)
		cout << "处理Exception类型异常" << endl;		
	}
	cout << "==========================" << endl;
	return 0;
}

运行结果:

示例三

程序运行期间出现异常且未被捕获,则系统会自动调用terminate函数,使程序终止/中断;所以异常一定要被捕获

int Division(int a,int b)
{
	if (b == 0)
	{
		throw string();//使用匿名对象,表示抛出string类型的异常
	}
	return a / b;
}

int main()
{
	try
	{
		cout << Division(10,0) << endl;
	}
	catch (int errmsg)
	{
		cout << "捕获int类型异常:errmsg=" << errmsg << endl;//捕获异常(未捕获)
	}

	//程序运行期间出现异常(string类型异常)并且未被捕获,则系统会自动调用terminate函数,使程序会终止/中断 

	return 0;
}

运行结果:

示例四

try块代码执行期间未产生异常,则try块后的所有catch子句均不会执行(F11调试观看)

int Division(int a,int b)
{
	if (b == 0)
	{
		throw string();//使用匿名对象,表示抛出string类型的异常
	}
	return a / b;
}
int main()
{
	try
	{
		cout << Division(10,5) << endl;
	}
	catch (int errmsg)
	{
		cout << "捕获int类型异常:errmsg=" << errmsg << endl;//捕获异常(未捕获)
	}
	catch (...)
	{
		//捕获任意类型的异常
		//处理异常
		//...
	}
	return 0;
}

异常的抛出与匹配规则

  • 异常是通过抛出对象而引发的,抛出对象的类型决定了应该激活哪个catch的处理代码

示例:

  • 当抛出异常时,被选中的处理代码是函数调用链中与抛出对象类型匹配且离抛出异常位置最近的catch块;

示例:

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	else
		return ((double)a / (double)b);
}

void Func()
{
	try
	{
		Division(10, 0);
	}
	catch (int errmsg2)
	{
		cout << "catch (int errmsg3)" << endl;
	}
	catch (const char* errmsg1)
	{
		cout << "catch (const char* errmsg1)" << endl;
	}
}

int main()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg3)
	{
		cout << "catch (const char* errmsg3)"<< endl;
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}
	cout << "==============================" << endl;

	return 0;
}

  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁

异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些矫正处理后,需要交给更外层的调用链函数来处理catch可以做完矫正操作,再将异常重新抛出,交给上一层或者最外层的函数进行处理

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	else
		return ((double)a / (double)b);
}
void Func()
{
	int *ptr = new int[10];
	try
	{
		int a, b = 0;
		cin >> a >> b;
		Division(a, b);
	}
	catch (const char* errmsg)
	{
		//将开辟的空间释放
		delete[] ptr;
        //重新抛出异常errmsg
		throw errmsg;
	}
	//若没有发生异常,不会执行catch子句
	//此处还需要释放空间
	delete[] ptr;
	cout << "void Func()" << endl;
}
int main()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << "exception errmsg:" << errmsg << endl;
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}
	cout << "===============================================" << endl;

	return 0;
}

运行结果:

异常安全问题

由于抛异常时只要找到匹配的catch就直接跳到catch块执行,没有找到对应catch的函数就不会继续执行,这样导致函数的执行流乱跳,可能会导致一些问题;

  • 构造函数完成对象的构造和初始化,最好不要再构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化;
double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	else
		return ((double)a / (double)b);
}
class A
{
public:
	A()
	{
		cout << "调用构造函数" << endl;
		_ptr1 = new int[10];
		int x, y = 0;
		cin >> x >> y;
		Division(x, y);
		_ptr2 = new int[10];
	}
	~A()
	{
		cout << "调用析构函数" << endl;
		delete[] _ptr1;
		delete[] _ptr2;
	}
private:
	int* _ptr1;
	int* _ptr2;
};

类中成员变量为2个指针,外部定义对象时会调用构造函数,_ptr1初始化之后抛出异常,导致_ptr2没有完成初始化,仍然为随机值,当对象销毁时便会释放随机值指向的空间,导致程序崩溃; 

  • 析构函数主要完成资源的清理,最好不要在析构函数中抛异常,否则可能导致某些释放空间的语句未执行从而导致内存泄漏;
  • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏;

//new操作符分配内存时,若内存不足,new会抛出std::bad_alloc异常
int* p = new int[1000000000000];

 示例(不安全的程序)

void Func()
{
	int* p1 = new int[10];
	//若开辟p1指向的空间失败,抛出异常,p2与p3所指向的空间也没有开辟自然也就不需要释放空间
	//跳转到main()函数函数栈帧中捕获异常,直接结束;

	int* p2 = new int[20];
	//若开辟p2指向的空间失败,抛出异常,被迫在当前函数栈帧中捕获异常,因为必须要释放p1所指向的空间
	//否则跳转到main()函数函数栈帧中捕获异常,导致p1所指向的空间无法释放

	int* p3 = new int[30];
	//若开辟p3指向的空间失败,抛出异常,被迫在当前函数栈帧中捕获异常,因为必须要释放p1、p2所指向的空间
	//否则跳转到main()函数函数栈帧中捕获异常,导致p1、p2所指向的空间无法释放


	//...

	delete[] p1;
	delete[] p2;
	delete[] p3;
}

int main()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << "exception errmsg:" << errmsg << endl;
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}
	cout << "===============================================" << endl;
	return 0;
}

解决方案(C++98)

void Func()
{
	int* p1 = new int[10];
	int* p2 = nullptr;
	int* p3 = nullptr;
	try
	{
		p2 = new int[20];
		try
		{
			p3 = new int[30];
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			//捕获什么抛出什么
			throw;
		}

	}
	catch (...)
	{
		delete[] p1;
	}
	//...

	delete[] p1;
	delete[] p2;
	delete[] p3;
}

int main()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << "exception errmsg:" << errmsg << endl;
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}
	cout << "===============================================" << endl;
	return 0;
}

异常的接口声明

  • 异常规格说明,是使函数调用者直到函数可能会抛出哪些异常。可以在函数后面接throw(异常类型),列出这个函数可能抛出的所有异常类型;
  • 在函数后面加 throw() 或者 noexcept(c++11)表示不抛异常;
  • 若没有接口声明表示,此函数可能会抛出任意类型的异常;
// 此处表示func()函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);

// 此处表示operator new()函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);

// 此处表示operator delete()函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
void* operator delete (std::size_t size, void* ptr) noexcept;

自定义异常体系

/* 异常的基类 */
class BaseException 
{
public:
	//基类虚函数
	virtual void what()
	{}
};

//异常的子类1:空指针异常
class NullPointerException : public BaseException
{
public:
	//子类重写父类虚函数
	virtual void what()
	{
		cout << "空指针异常" << endl;
	}
};

//异常的子类2:索引越界
class IndexOutOfRangeException : public BaseException 
{
public:
	//子类重写父类虚函数
	virtual void what()
	{
		cout << "索引越界异常" << endl;
	}
};

void Test()
{
	//throw NullPointerException();//抛出空指针类的匿名对象(子类对象)

	throw IndexOutOfRangeException();//抛出索引越界类的匿名对象(子类对象)
}

int main() 
{
	try 
	{
		Test();
	}
	catch (BaseException& e)
	{	
		//多态使用:基类的引用类型捕获子类异常对象
		e.what();
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}
	cout << "================================" << endl;
	return 0;
}

运行结果:

实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,建立一个继承体系,首先建立一个异常类作为基类,然后各个派生类继承基类,来定义出不同的异常,基类相当于一个框架,派生类是具体的异常,各个派生类具体实现各自异常的内容,然后抛异常只需要抛派生类,捕捉异常时只需要捕捉基类即可;

//父类: 总公司
class Exception
{
public:

	Exception(const char* str = nullptr, int id = 0)
		:_errmsg(str)
		, _id(id)
	{}
	virtual void what()const = 0;
protected:
	string _errmsg;//错误描述
	int _id;//错误编号
};

//子类:数据库部门
class SqlException :public Exception
{
public:
	SqlException(const char *str = nullptr, int id = 1)
		:Exception(str, id)
	{}

	virtual void what()const
	{
		cout << "error msg:" << _errmsg << endl;
		cout << "error id:" << _id << endl;
	}
};
//子类:网络部门
class HttpException :public Exception
{
public:
	HttpException(const char *str = nullptr, int id = 2)
		:Exception(str, id)
	{}

	virtual void what()const
	{
		cout << "error msg:" << _errmsg << endl;
		cout << "error id:" << _id << endl;
	}
};
//子类:缓存部门
class CacheException :public Exception
{
public:
	CacheException(const char *str = nullptr, int id = 3)
		:Exception(str, id)
	{}

	virtual void what()const{
		cout << "error msg:" << _errmsg << endl;
		cout << "error id:" << _id << endl;
	}
};

void test()
{
	//当网络连接失败,抛出网络异常
	//throw HttpException("Http Failed", 2);

	//当缓存失败时,抛出缓存异常
	//throw CacheException("Cache Failed", 3);

	//当数据库存储数据失败,抛出数据库异常
	throw SqlException("Sql Failed", 4);
}

int main()
{
	try
	{
		test();
	}
	//基类的引用类型或者指针类型捕获子类异常对象实现多态调用
	catch (const Exception& e)
	{
		e.what();
	}
	catch (...)
	{
		cout << "unknow exception" << endl;
	}
	return 0;
}

运行结果:


欢迎大家批评指正,博主会持续输出优质内容,谢谢各位观众老爷观看,码字画图不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小呆瓜历险记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值