目录

异常的概念

异常的用法

异常的基础用法

异常与多态

异常重新抛出

异常的执行过程

异常的规范使用

函数的异常抛出声明

规范的异常体系

要点梳理

异常的概念

在C语言中有两种传统的错误处理机制:

强制终止程序,比如assert等,当发生诸如内存错误,除0错误时就会终止程序。缺陷:比较暴力,用户难以接受。返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。

也就是说,在C语言中基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

而在C++中通常采用“抛异常”的方式来处理错误。C++异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

异常的用法

异常的基础用法

C++异常的基本用法就是抛出异常(throw)和尝试捕获异常(try - catch),即一个异常的基础写法通常包含throw、try、catch这三个部分。其中,throw用于抛出异常;try,用于尝试捕获代码块中的异常;catch,用于匹配捕获到的不同类型的异常,并作出不同的反馈。

写法格式如下:

try

{

// try a throw

}

catch (ExceptionName e1)

{

// catch - e1

}

catch (ExceptionName e2)

{

// catch - e2

}

catch (...)

{

// catch - others

}

其中,catch(...)表示捕获其它任意类型的异常,通常用于捕获未知或者未设置的异常,以预防抛出异常而没捕获的情况发生。

而抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。

用法示例如下:

void test()

{

// ...

throw "const char*型的异常"; // 常量字符串是const char*型的

}

int main()

{

try

{

// 其中,这里也可以直接throw抛出一个异常

test();

}

catch (const int errint)

{

cout << "int型的异常" << endl; // 抛出的是const char* 的异常,所以不会匹配到这里

}

catch (const char* errmsg)

{

cout << errmsg << endl; // 类型匹配,所以最终打印结果就是:const char*型的异常

}

catch (...)

{

cout << "未知异常" << endl; // 用于防止抛出异常而没捕获的情况发生。

}

return 0;

}

异常与多态

当我们的异常体系很小时,catch的类型与抛出的类型完全匹配是完全行得通的,但如果我们的异常体系变得庞大时,如果还是有一个throw的类型就写一个catch语句就会显得十分冗杂。

所以我们可以利用C++的多态机制来灵活地处理这种情况。catch一个父类引用,这样就可以仅用一个catch语句块就可以处理多种相同体系的异常问题了。例如:

// 基类异常

class baseException

{

public:

virtual string what()

{

return "baseException.";

}

};

// student子类异常

class studentException : public baseException

{

public:

virtual string what()

{

return "student error!";

}

};

// teacher子类异常

class teacherException : public baseException

{

public:

virtual string what()

{

return "teacher error!";

}

};

// 主函数,测试异常

int main()

{

try

{

throw studentException(); // try中抛出子类异常

}

catch (baseException& e) // 父类引用遇到子类对象形成多态

{

cout << e.what() << endl;

}

return 0;

}

异常重新抛出

C++中还支持异常的重新抛出,即在catch语句中继续抛出异常。那么为什么要重新抛出异常呢?例如考虑如下场景:(内容参考:C++中异常处理中的异常重新抛出的一种用法)

假设存在一个第三方库,我们需要使用自己的函数进行调用,有如下代码:

#include

#include

using namespace std;

/*第三方库中函数 void func(int i)

异常代码 -1:运行时错误

-2:数据超界异常

*/

void func(int i)

{

if(i<0)

throw -1;

if(i>100)

throw -2;

}

int main()

{

try

{

func(199);

}

catch (int i)

{

cout<<"Error Code: "<

}

return 0;

}

//运行结果为 Error Code: -2

此时我们根本不知道-2代表什么意思,只能去查找函数的手册,不仅麻烦,而且不直观。所以我们可以考虑对异常重新抛出,那么改进后的代码如下:

#include

#include

using namespace std;

/*第三方库中函数 void func(int i)

异常代码 -1:运行时错误

-2:数据超界异常

*/

void func(int i)

{

if(i<0)

throw -1;

if(i>100)

throw -2;

}

//这是我们自己的库。调用第三方库的函数void func(int i)

void myFunc(int i)

{

try

{

func(i);

}

catch(int i)

{

switch(i)

{

case -1:

throw "Runtime Error";

break;

case -2:

throw "Data Error";

break;

}

}

}

int main()

{

try

{

myFunc(199);

}

catch (const char *s)

{

cout<<"Error Code: "<

}

return 0;

}

//结果输出为 Error Code: Data Error

异常的执行过程

当执行到throw语句时,首先会检查当前的throw语句是否在try块内部,如果是,就在当前函数栈中查找匹配的catch语句。如果匹配到了则直接跳到catch的地方执行。如果没有相匹配的catch块,则退出当前函数栈,在上层函数栈帧中继续查找尝试匹配。如果到达main函数的栈,都没有匹配的catch,就会终止程序,有些编译器还会报错。例如:

图片出处:

C++异常详细介绍-CSDN博客

上述沿着调用链查找匹配的catch块的过程叫栈展开或者栈解旋。也就是说异常被抛出后,从进入try块起,到异常被抛掷前(遇到throw之前),这期间在栈上构造的所有对象,都会被自动析构,其中析构的顺序与构造的顺序相反。其原因自然就与函数栈帧的开辟与释放分不开了,其具体细节就不再过多的阐述了。

需要注意的是,throw在一个函数中的效果和return有些类似。当执行到throw语句之后就会立即去寻找匹配的try语句块并跳到对应的catch语句块中,就不会再执行throw后续的代码了。

异常的规范使用

函数的异常抛出声明

一般来说,为了代码的可读性与规范型,抛出异常的函数通常需要在函数声明部分,以throw(...)的形式声明抛出异常的类型(声明和定义分开时,两个都可以写声明throw部分)示例如下:

// 这里表示这个函数会抛出A、B、C、D中的某种类型的异常

void fun() throw(A,B,C,D);

// 这里表示这个函数只会抛出bad_alloc的异常

void* operator new (std::size_t size) throw (std::bad_alloc);

// 这里表示这个函数不会抛出异常

void* operator new (std::size_t size, void* ptr) throw();

void* operator new (std::size_t size, void* ptr) noexcept; // C++11支持

其中,早期用throw()来表示没有异常抛出,C++11之后可以用noexcept代替。

函数的异常抛出声明并不影响函数的任何功能,即它并没有实质性地影响函数的运行,只是一个便于代码阅读的声明。也就是说,如果实际抛出的异常和声明部分的类型不对应,并不会导致任何运行和编译错误。不过还是要保持统一的,因为如果不保持统一,那么这个异常抛出声明不但没有提高效率,反而还可能会造成很多麻烦。

规范的异常体系

实际中,并不是我们想抛什么异常就抛什么异常,这样会导致捕捉的时候不好捕捉。而是会建立一个异常体系,结合多态的性质,在抛出异常时,只需要用基类进行捕捉即可。

其中,在C++库中也建立了一个异常体系。也给我们提供了一些异常类。我们可以在程序中使用这些标准异常,它们就是以父子类的层次结构组织起来的(图片摘自:C++ 异常处理 | 菜鸟教程)

说明如下:

要点梳理

try和catch语句块不能省略后面的大括号,且try和catch之间不能有其它语句。try和catch必须匹配使用,即如果只有try没有catch就会报错。捕获列表catch是按照抛出异常的类型进行捕获的。catch(...)表示捕获其它任意类型的异常,通常用于捕获未知或者未设置的异常,以预防抛出异常而没捕获的情况发生。抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。被选中的处理代码的调用链是,找到类型匹配且离抛出异常位置最近的catch语句块。捕获是根据抛出的类型进行捕获的,捕获之后可以继续抛出新的异常。如果抛出了异常但没捕获,程序会异常终止。而如果没有捕获到异常则会跳过整个异常捕获部分,包括catch(...)语句块throw与return类似,异常抛出后会立即结束try块与原函数,所以throw后面部分的代码不会被执行。实际中抛出和捕获的类型不一定要类型完全匹配,可以抛出派生类对象,使用基类引用来捕获,这个在实际生活中很实用。

文章链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。