C++ STL——异常

[TOC]


<font color="red">注:原创不易,转载请务必注明原作者和出处,感谢支持!</font>

<font color="red">注:内容来自某培训课程,不一定完全正确!</font>

一 C++异常机制概述

什么是异常处理?一句话,异常处理就是处理程序中的错误。

为什么需要异常处理以及异常处理的基本思想?

C++之父Bjarne Stroustrup在《The C++ Programming Language》中讲到:一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以在用户的代码里处理了,不用留给库去发现)。

Bjane Stroustrup说:提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或间接)调用者能够处理这个问题。也就是说,C++将问题的检测与问题的处理相分离。

在异常处理机制出现之前的错误处理方式?在C语言中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单理解为一个全局整型变量)去记录错误。当然C++中仍然可以使用这两种方法。但是这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。还有一个缺点是一个函数的返回值只有一个,你通过函数的返回值标识错误代码,那么函数就不能返回其他的值。当然,你也可以通过指针或者C++的引用来返回另外的值,但这可能会令你的程序略微晦涩难懂。

异常的优点在哪里?

(1)函数的返回值可以忽略,但异常不可以忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少回促使程序员开发出来的程序更健壮一点。而如果使用C语言的errno宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其妙地终止或者出现错误的结果。 (2)整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。 (3)整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。 (4)异常处理可以在调用时跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需在一处进行处理就可以了,不需要每级函数都处理。

下面是一个异常的基本语法实例。如果除数y为0,则抛出y的值。在Test1()中尝试捕获异常。

1// 异常的基本语法 2int Divide(int x, int y) 3{ 4 if (y == 0) 5 { 6 // 抛出异常 7 throw y; 8 } 9 return x / y; 10} 11 12void Test1(void) 13{ 14 // 试着捕获异常 15 try 16 { 17 Divide(10, 0); 18 } 19 // 异常是根据类型进行匹配的 20 catch (int e) 21 { 22 cout << "Error : 除数为" << e << endl; 23 } 24}

异常可以跳级处理。比如在下面的这个例子中,函数的调用顺序为Test2() --> CallDivide() --> Divide()。异常在Divide()函数中被抛出,但异常的捕获却没有在CallDivide()中进行,而是放到了Test2()中进行。

1int Divide(int x, int y) 2{ 3 if (y == 0) 4 { 5 // 抛出异常 6 throw y; 7 } 8 return x / y; 9} 10 11// CallDivide()并未对异常进行处理 12void CallDivide(int x, int y) 13{ 14 Divide(x, y); 15} 16 17// Divide()所抛出的异常在函数调用顶层Test2()中被捕获 18// 如果Test2()中仍然没有捕获到Divide()所抛出的异常,则 19// 异常会被抛到函数调用的最顶层main()函数中,如果在main() 20// 中异常还没有被捕获并处理,则程序会终止执行! 21void Test2(void) 22{ 23 try 24 { 25 CallDivide(10, 0); 26 } 27 catch (int e) 28 { 29 cout << "错误:除数为" << e << endl; 30 } 31}

二 栈解旋(unwinding)

栈解旋是指当异常被抛出后,从进入try块起,到异常被抛出前,这期间在栈上构造的所有对象,都会被自动析构,析构的顺序与构造的顺序相反。这一过程被称为栈的解旋(unwinding)。

比如下面的例子,进入try语句块之后,先调用的是CallDivide(),在CallDivide()当中先是构建了Person对象p3,然后调用Divide(),在Divide()当中构建了对象p1和p2,直到异常的发生。

1class Person 2{ 3public: 4 Person(string nm) 5 { 6 name = nm; 7 cout << "Person对象" << name << "构建" << endl; 8 } 9 10 ~Person() 11 { 12 cout << "Person对象" << name << "析构" << endl; 13 } 14 15private: 16 string name; 17}; 18 19int Divide(int x, int y) 20{ 21 Person p1("p1"); 22 Person p2("p2"); 23 24 if (y == 0) 25 throw y; 26 27 return x / y; 28} 29 30void CallDivide() 31{ 32 Person p3("p3"); 33 Divide(10, 0); 34} 35 36void Test01() 37{ 38 try 39 { 40 CallDivide(); 41 } 42 catch (int e) 43 { 44 cout << "有异常发生!" << endl; 45 } 46}

上面程序的输出如下,可以看到构建的顺序和析构的顺序刚好相反。

1Person对象p3构建 2Person对象p1构建 3Person对象p2构建 4Person对象p2析构 5Person对象p1析构 6Person对象p3析构 7有异常发生! 8

三 异常接口的声明

(1)为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw (A, B, C),这个函数func能够且只能抛出类型A,B,C及其子类型的异常。 (2)如果在函数声明中没有包含异常接口声明,则此函数可以抛出任何类型的异常 (3)一个不抛出任何异常的函数可以声明为:void func() throw () (4)如果一个函数抛出了它的异常接口声明所不允许抛出的异常,则unexcepted函数会被调用,该函数默认行为调用terminate()函数中断程序。

下面是一个异常接口声明的实例。

1// 这个函数只能抛出int float char三种类型的异常,抛出其他异常就报错 2void func1() throw (int, float, char) 3{ 4 // 抛出char *会导致程序异常退出 5 throw string("abc"); 6} 7 8// 这个函数不能抛出任何异常 9void func2() throw () 10{ 11 // 抛出异常会导致程序异常退出 12 throw 1; 13} 14 15// 这个函数可以抛出任何类型异常 16void func3() 17{ 18 ; 19}

四 异常类型和异常变量的生命周期

throw的异常是有类型的,可以是数字、字符串、和类对象等。catch需要严格地匹配异常类型。

1void fun1() 2{ 3 throw 1; 4} 5 6void fun2() 7{ 8 throw "exception"; 9} 10 11class MyException 12{ 13public: 14 MyException(string msg) 15 { 16 error = msg; 17 } 18 19 void what() 20 { 21 cout << error << endl; 22 } 23 24private: 25 string error; 26}; 27 28void fun3() 29{ 30 // 抛出匿名对象 31 throw MyException("我刚写的异常!"); 32} 33 34void Test1() 35{ 36 try 37 { 38 fun1(); 39 } 40 catch (int e) 41 { 42 cout << "int型异常!" << endl; 43 } 44 45 try 46 { 47 fun2(); 48 } 49 catch (char *e) 50 { 51 cout << "char *型异常!" << endl; 52 } 53 54 try 55 { 56 fun3(); 57 } 58 catch (MyException e) 59 { 60 // 对象MyException里封装了异常的相关信息 61 e.what(); 62 } 63}

下面进行异常变量生命周期分析。首先是使用普通的匿名对象去接抛出异常的情况。

1class MyException 2{ 3public: 4 MyException() 5 { 6 cout << "MyException构造函数被调用" << endl; 7 } 8 9 MyException(const MyException & ex) 10 { 11 cout << "MyException拷贝构造函数被调用" << endl; 12 } 13 ~MyException() 14 { 15 cout << "MyException析构函数被调用" << endl; 16 } 17}; 18 19void fun() 20{ 21 // 抛出匿名异常对象 22 throw MyException(); 23} 24void Test() 25{ 26 try 27 { 28 fun(); 29 } 30 // 使用普通对象去接抛出的异常 31 catch (MyException e) 32 { 33 cout << "异常捕获!" << endl; 34 } 35}

此时,程序输出结果如下。程序首先是在fun()中调用了MyException的构造函数创建了一个匿名对象,然后将该匿名对象抛出。然后在使用普通元素e接收抛出的异常对象时,调用了拷贝构造函数将匿名对象拷贝至e当中。然后是捕获异常,捕获之后,在catch语句中进行处理。当catch中处理完了异常之后再将对象e和匿名对象析构,所以输出两次“MyException析构函数被调用”。

1MyException构造函数被调用 2MyException拷贝构造函数被调用 3异常捕获! 4MyException析构函数被调用 5MyException析构函数被调用

接下来是使用引用去接抛出的异常对象的情况。

1void Test() 2{ 3 try 4 { 5 fun(); 6 } 7 // 使用引用去接抛出的异常 8 catch (const MyException &e) 9 { 10 cout << "异常捕获!" << endl; 11 } 12}

此时程序输出如下。和使用普通元素e接收抛出异常对象相比,使用引用去接收抛出对象少了调用拷贝构造函数的步骤。因为,在catch时直接引用了匿名对象,所以从始至终,只要匿名对象被创建和析构。

<font color="red">我比较菜,我有疑问:参考下面的用指针去接收抛出的异常部分,为什么这里可以在catch语句块里去引用匿名对象?这个匿名对象不是在fun()里创建的吗?随着fun()调用完毕,退栈之后,匿名对象还存在吗?如果不存在,那你还怎么能够引用呢?如果存在,那说明这个匿名异常对象肯定不在栈内存中,不在栈内存中,那它在哪里?在堆区吗?</font>

1MyException构造函数被调用 2异常捕获! 3MyException析构函数被调用

下面是使用指针去接收抛出异常对象的<font color="red">错误</font>示范。

1void fun() 2{ 3 // 抛出匿名对象的地址 4 throw &(MyException()); 5} 6void Test() 7{ 8 try 9 { 10 fun(); 11 } 12 // 使用指针去接抛出的异常 13 catch (const MyException *e) 14 { 15 cout << "异常捕获!" << endl; 16 } 17}

此时,程序输出结果如下。<font color="red">可以看到,在catch块中进行异常处理之前,匿名对象就已经被析构了!此时指针e成了一个野指针,你再也无法通过e来获取e中封装的异常信息了!</font>

1MyException构造函数被调用 2MyException析构函数被调用 3异常捕获!

下面是使用指针去接收抛出异常对象的<font color="red">正确</font>示范。<font color="red">为了防止匿名对象在进行异常处理之前被析构,你需要在堆中创建匿名对象,这也就意味着你需要手动地管理内存!</font>

1void fun() 2{ 3 // 抛出匿名对象的地址,匿名对象在堆中创建 4 throw new MyException(); 5} 6void Test() 7{ 8 try 9 { 10 fun(); 11 } 12 // 使用指针去接抛出的异常 13 catch (const MyException *e) 14 { 15 cout << "异常捕获!" << endl; 16 // 千万别忘记手动释放匿名对象的内存! 17 delete e; 18 } 19}

此时,程序终于输出正确了!

1MyException构造函数被调用 2异常捕获! 3MyException析构函数被调用

五 C++标准异常库

C++标准异常库的成员

(1)在上述的继承体系中,每个类都提供了构造函数、拷贝构造函数和赋值操作符重载 (2)logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述 (3)所有的异常类都有一个what()方法,返回const char *类型(C风格字符串)的值,描述异常信息。

标准异常类的具体描述

异常名称

描述

exception

所有标准异常类的父类

bad_alloc

当operator new and operator new[]请求分配失败时

bad_exception

这是个特殊的异常类,如果函数的异常抛出列表里声明了bad_exception异常,当函数内部抛出了异常列表中没有的异常,这时调用的unexcepted函数中若抛出异常,不论什么类型,都会被替换为bad_exception类型

bad_type

使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常

bad_cast

使用dynamic_cast转换引用失败时

ios_base::failure

io操作过程中出现错误

logic_error

逻辑错误,可以在运行前检测的错误

runtime_error

运行时错误,仅在运行时才可以检测的错误

logic_error的子类:

异常名称

描述

length_error

试图生成一个超出该类型最大长度的对象时,例如vector的resize操作

domain_error

参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负值的函数

out_of_range

超出有效范围

invalid_argument

参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是'0'或者'1'的时候,抛出该异常

runtime_error的子类:

异常名称

描述

range_error

计算结果超出了有意义的值域范围

overflow_error

算数计算上溢

underflow_error

算数计算下溢

invalid_argument

参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是'0'或者'1'的时候,抛出该异常

编写自己的异常类 为什么要编写自己的异常类? (1)标准库中的异常类是有限的 (2)在自己的异常类中,可以添加自己的信息(标准库中的异常类值允许设置一个用来描述异常的字符串)

如何编写自己的异常类? (1)建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时 (2)当继承标准异常类时,应该重载父类的what()函数和虚析构函数 (3)因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数。

下面是一个C++标准异常库的应用实例

1class Person 2{ 3public: 4 Person() 5 { 6 mAge = 0; 7 } 8 9 void setAge(int age) 10 { 11 if (age < 0 || age > 100) 12 throw out_of_range("年龄应该在0到100之间"); 13 14 this->mAge = age; 15 } 16 17private: 18 int mAge; 19}; 20 21 22void Test() 23{ 24 Person p; 25 26 try 27 { 28 p.setAge(1000); 29 } 30 catch (const exception &e) 31 { 32 cout << e.what() << endl; 33 } 34}

下面是自己手写的异常类的应用实例。

1class MyOutOfRangeException : public exception 2{ 3public: 4 MyOutOfRangeException(char *error) 5 { 6 pError = new char[strlen(error) + 1]; 7 strcpy_s(pError, strlen(error) + 1, error); 8 } 9 ~MyOutOfRangeException() 10 { 11 if (pError != nullptr) 12 { 13 delete[] pError; 14 } 15 } 16 virtual const char *what() const 17 { 18 return pError; 19 } 20 21private: 22 char *pError; 23 24}; 25 26void Call(void) 27{ 28 throw MyOutOfRangeException("我自己的异常类!"); 29} 30 31void Test() 32{ 33 try 34 { 35 Call(); 36 } 37 catch (const exception& e) 38 { 39 cout << e.what() << endl; 40 } 41}

六 异常的继承

下面是一个自己手写异常继承的应用案例。

1// 异常基类 2class BaseMyException 3{ 4public: 5 virtual void what() = 0; 6 virtual ~BaseMyException() {} 7}; 8 9// 继承 10class TargetSpaceNullException : public BaseMyException 11{ 12public: 13 virtual void what() 14 { 15 cout << "目标空间为空!" << endl; 16 } 17 ~TargetSpaceNullException() {} 18}; 19 20// 继承 21class SourceSpaceNullException : public BaseMyException 22{ 23public: 24 virtual void what() 25 { 26 cout << "源空间为空!" << endl; 27 } 28 ~SourceSpaceNullException() {} 29}; 30 31void copy_str(char *target, char *source) 32{ 33 if (target == nullptr) 34 { 35 throw TargetSpaceNullException(); 36 } 37 38 if (source == nullptr) 39 { 40 throw SourceSpaceNullException(); 41 } 42 43 while (*source != '\0') 44 { 45 *target = *source; 46 target++; 47 source++; 48 } 49 *target = '\0'; 50} 51 52int main(int argc, char **argv) 53{ 54 char *source = "abcdefg"; 55 char buf[1024] = { 0 }; 56 try 57 { 58 copy_str(nullptr, source); 59 } 60 catch (BaseMyException &e) 61 { 62 e.what(); 63 } 64 65 cout << buf << endl; 66 getchar(); 67 return 0; 68}
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

微信小程序new Date()转换时间异常问题

微信小程序苹果手机页面上显示时间异常,安卓机正常问题image(https://imghelloworld.osscnbeijing.aliyuncs.com/imgs/b691e1230e2f15efbd81fe11ef734d4f.png)错误代码vardate'2021030617:00:00'vardateT

mysql设置时区

mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0