异常处理基本思想
C++的异常处理的基本思想大致可以概括为传统错误处理机制、通过函数返回值来处理错误。

1)C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理。
2)异常是专门针对抽象编程中的一系列错误处理的,C++中不能借助函数机制,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试,如图

3)异常超脱于函数机制,决定了其对函数的跨越式回跳。
4)异常跨越函数
异常基本语法

1) 若有异常则通过throw操作创建一个异常对象并抛掷。
2) 将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
3) 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
4) catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
5) 如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
6)处理不了的异常,可以在catch的最后一个分支,使用throw语法,向上扔
7)异常机制与函数机制互不干涉,但捕捉的方式是基于类型匹配。捕捉相当于函数返回类型的匹配,而不是函数参数的匹配,所以捕捉不用考虑一个抛掷中的多种数据类型匹配问题。
catch代码块必须出现在try后,并且在try块后可以出现多个catch代码块,以捕捉各种不同类型的抛掷。
异常机制是基于这样的原理:程序运行实质上是数据实体在做一些操作,因此发生异常现象的地方,一定是某个实体出了差错,该实体所对应的数据类型便作为抛掷和捕捉的依据。
8)异常捕捉严格按照类型匹配
异常捕捉的类型匹配之苛刻程度可以和模板的类型匹配媲美,它不允许相容类型的隐式转换,比如,抛掷char类型用int型就捕捉不到.例如下列代码不会输出“int exception.”,从而也不会输出“That's ok.” 因为出现异常后提示退出
| int main(){ try { throw ‘H'; } catch ( int ){ cout << "int exception.\n" ; } cout << "That's ok.\n" ; return 0; } |
栈解旋(unwinding)
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋(unwinding)。
#include <iostream> #include <cstdio> using namespace std; class MyException {}; class Test { public : Test( int a = 0, int b = 0) { this ->a = a; this ->b = b; cout << "Test 构造函数执行" << "a:" << a << " b: " << b << endl; } void printT() { cout << "a:" << a << " b: " << b << endl; } ~Test() { cout << "Test 析构函数执行" << "a:" << a << " b: " << b << endl; } private : int a; int b; }; void myFunc() throw (MyException) { Test t1; Test t2; cout << "定义了两个栈变量,异常抛出后测试栈变量的如何被析构" << endl; throw MyException(); } int main() { //异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象, //都会被自动析构。析构的顺序与构造的顺序相反。 //这一过程称为栈的解旋(unwinding) try { myFunc(); } //catch(MyException &e) //这里不能访问异常对象 catch (MyException) //这里不能访问异常对象 { cout << "接收到MyException类型异常" << endl; } catch (...) { cout << "未知类型异常" << endl; } return 0; } |
异常接口声明
1)为了加强程序的可读性,可以在函数声明中列出可能抛出的所有异常类型,例如:
void func() throw (A, B, C , D); //这个函数func()能够且只能抛出类型A B C D及其子类型的异常。
2)如果在函数声明中没有包含异常接口声明,则次函数可以抛掷任何类型的异常,例如:
void func();
3)一个不抛掷任何类型异常的函数可以声明为:
void func() throw();
4) 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexpected函数会被调用,该函数默认行为调用terminate函数中止程序。
传统处理错误
#include <iostream> #include <cstdio> using namespace std; // 传统的错误处理机制 int myStrcpy( char *to, char *from) { if (from == NULL) { return 1; } if (to == NULL) { return 2; } // copy时的场景检查 if (*from == 'a' ) { return 3; // copy时错误 } while (*from != '\0' ) { *to = *from; to++; from++; } *to = '\0' ; return 0; } int main() { int ret = 0; char buf1[] = "zbcdefg" ; char buf2[1024] = { 0 }; ret = myStrcpy(buf2, buf1); if (ret != 0) { switch (ret) { case 1: cout << "源buf出错!\n" ; break ; case 2: cout << "目的buf出错!\n" ; break ; case 3: cout << "copy过程出错!\n" ; break ; default : cout << "未知错误!\n" ; break ; } } cout << "buf2:\n" << buf2; cout << endl; return 0; } |
throw char*
#include <iostream> #include <cstdio> using namespace std; // throw char * void myStrcpy( char *to, char *from) { if (from == NULL) { throw "源buf出错" ; } if (to == NULL) { throw "目的buf出错" ; } // copy时的场景检查 if (*from == 'a' ) { throw "copy过程出错" ; // copy时错误 } while (*from != '\0' ) { *to = *from; to++; from++; } *to = '\0' ; return ; } int main() { int ret = 0; char buf1[] = "abcdefg" ; char buf2[1024] = { 0 }; try { myStrcpy(buf2, buf1); } catch ( int e) // e可以写可以不写 { cout << e << "int类型异常" << endl; } catch ( char *e) { cout << "char* 类型异常" << endl; } catch (...) { }; cout << endl; return 0; } |
throw 类对象
#include <iostream> #include <cstdio> using namespace std; class BadSrcType {}; class BadDestType {}; class BadProcessType { public : BadProcessType() { cout << "BadProcessType构造函数do \n" ; } BadProcessType( const BadProcessType &obj) { cout << "BadProcessType copy构造函数do \n" ; } ~BadProcessType() { cout << "BadProcessType析构函数do \n" ; } }; |
throw 类对象、类型异常
void my_strcpy3( char *to, char *from) { if (from == NULL) { throw BadSrcType(); } if (to == NULL) { throw BadDestType(); } //copy是的 场景检查 if (*from == 'a' ) { printf ( "开始 BadProcessType类型异常 \n" ); throw BadProcessType(); //会不会产生一个匿名对象? } if (*from == 'b' ) { throw &(BadProcessType()); //会不会产生一个匿名对象? } if (*from == 'c' ) { throw new BadProcessType; //会不会产生一个匿名对象? } while (*from != '\0' ) { *to = *from; to++; from++; } *to = '\0' ; } int main() { int ret = 0; char buf1[] = "cbbcdefg" ; char buf2[1024] = { 0 }; try { //my_strcpy1(buf2, buf1); //my_strcpy2(buf2, buf1); my_strcpy3(buf2, buf1); } catch ( int e) //e可以写 也可以不写 { cout << e << " int类型异常" << endl; } catch ( char *e) { cout << e << " char* 类型异常" << endl; } //--- catch (BadSrcType e) { cout << " BadSrcType 类型异常" << endl; } catch (BadDestType e) { cout << " BadDestType 类型异常" << endl; } //结论1: 如果 接受异常的时候 使用一个异常变量,则copy构造异常变量. /* catch( BadProcessType e) //是把匿名对象copy给e 还是e还是那个匿名对象 { cout << " BadProcessType 类型异常" << endl; } */ /*结论2: 使用引用的话 会使用throw时候的那个对象 catch( BadProcessType &e) //是把匿名对象copy给e 还是e还是那个匿名对象 { cout << " BadProcessType 类型异常" << endl; } */ //结论3: 指针可以和引用/元素写在一块 但是引用和元素不能写在一块 catch (BadProcessType *e) //是把匿名对象copy给e 还是e还是那个匿名对象 { cout << " BadProcessType 类型异常" << endl; delete e; } //结论4: 类对象时, 使用引用比较合适 // -- catch (...) { cout << "未知 类型异常" << endl; } return 0; } |
C++抛出和接收异常的顺序
异常(exception)是C++语言引入的错误处理机制。它 采用了统一的方式对程序的运行时错误进行处理,具有标准化、安全和高效的特点。C++为了实现异常处理,引入了三个关键字:try、throw、catch。异常由throw抛出,格式为throw[expression],由catch捕捉。Try语句块是可能抛出异常的语句块,它通常和一个或多个catch语句块连续出现。
try语句块和catch语句块必须相互配合,以下三种情况都会导致编译错误:
(1)只有try语句块而没有catch语句块,或者只有catch语句块而没有try语句块;
(2)在try语句块和catch语句块之间夹杂有其他语句;
(3)当try语句块后跟有多个catch语句块时,catch语句块之间夹杂有其他语句;
(4)同一种数据类型的传值catch分支与传引用catch分支不能同时出现。
在抛出和接收异常的过程中,我们还要注意以下几点。
1.被抛出的异常对象什么时候被销毁?
用throw语句抛出一个对象时,会构造一个新的对象,这个对象就是异常对象。该对象的生命周期从被抛出开始计算,一直到被某个catch语句捕捉,就会在该catch语句块执行完毕后被销毁。考察如下程序。
#include <iostream> using namespace std; class ExClass { int num; public : ExClass( int i) { cout<< "Constructing exception object with num=" <<i<<endl; num=i; } ExClass(ExClass& e) { cout<< "Copy Constructing exception object with num=" <<e.num+1<<endl; num=e.num+1; } ~ExClass() { cout<< "Destructing exception object with num=" <<num<<endl; } void show() { cout<< "the number is " <<num<<endl; } }; int main() { ExClass obj(99); try { throw obj; //导致输出:Constructing exception object with num=100 } catch ( double f) { cout<< "exception catched" <<endl; } //导致输出:Constructing exception object with num=101 catch (ExClass e) { e.show(); } cout<< "after catch" <<endl; } |
程序输出结果是:
Constructing exception object with num=99
Copy Constructing exception object with num=100
Copy Constructing exception object with num=101
the number is 101
Destructing exception object with num=101
Destructing exception object with num=100
after catch
Destructing exception object with num=99
用throw语句抛出一个对象时,会构造一个新的对象,这个对象就是异常对象。该对象的生命周期从被抛出时开始计算,一直到被某个catch语句捕获,就会在该catch语句块执行完毕后被销毁。在上面的程序中,异常对象的num值为100,“Destructing exception object with num=100”这句话在“after catch”之前输出,正好说明异常对象的销毁时间是在它被捕获的catch块执行之后。
所以的catch分支在执行时类似一次函数调用,catch 的参数相当于函数的形参,而被抛出的异常对象相当于函数调用时的实参。当形参与实参成功匹配时,就说明异常被某个catch分支所捕获。catch后面的参数只能采用传值、传引用和传指针三种方式,如果采用传值方式,则会生成实参的一个副本,如果实参是一个对象,就会导致构造函数被调用。在上面的程序中,执行catch(ExClass e) 语句就是利用异常对象构造一个对象e,因此会调用拷贝构造函数。
要注意的是:同一种数据类型的传值catch分支和传引用catch分支不能同时出现。
2.异常如果在当前函数没有被捕获会发生什么?
在某些情况下,可能所有的catch分支都无法捕获到抛出的异常,这将导致当前函数执行的结束,并返回到主调函数中。在主调函数中,将继续以上的捕捉异常的过程,直到异常被捕捉或最终结束整个程序。考察如下程序。
#include <iostream> using namespace std; class ExClass { int num; public : ExClass( int i) { cout<< "Constructing exception object with num=" <<i<<endl; num=i; } ExClass(ExClass& e) { cout<< "Copy Constructing exception object with num=" <<e.num+1<<endl; num=e.num+1; } ~ExClass() { cout<< "Destructing exception object with num=" <<num<<endl; } void show() { cout<< "the number is " <<num<<endl; } }; void throwExFunc() { try { throw ExClass(199); } catch ( double f){ cout<< "double exception catched" <<endl; } cout<< "exit throwExFunc()" <<endl; } int main() { try { throwExFunc(); } catch (ExClass e) { e.show(); } catch (...) { cout<< "all will fall in" <<endl; } cout<< "continue to execute" <<endl; } |
程序的输出结果:
Constructing exception object with num=199
Copy Constructing exception object with num=200
the number is 200
Destructing exception object with num=200
Destructing exception object with num=199
continue to execute
从程序的结果可以看出:
(1)被抛出的异常对象的num值为199,由于它没有在函数throwExFunc()中被捕捉,所以它导致了throwExFunc()的执行结束(否则会输出:exit throwExFunc())。在main()函数中,catch(ExClass e)捕获了异常对象,通过复制构造函数产生对象e,e的num值为200,catch语句块运行完结束后,对象e首先被销毁,紧接着销毁异常对象。在这之后,程序继续运行,输出:continue to execute。
(2)catch(…)的意思是可以捕获所有类型的异常。不提倡随意地使用catch(…),因为这会导致异常类型的不精确处理,并降低程序的运行效率。但是,在程序的开发阶段,catch(…)还是有用的,因为如果在精心安排异常捕获之后,还是进入了catch(…)语句块,说明前面的代码存在缺陷,需要进一步改正。
(3)在捕捉异常对象时,还可以采用传引用的方式,例如把catch语句写成catch(ExClass& e),这样就可以不必产生异常对象的副本,减少程序的运行开销,提高运行效率。
(4)在抛出异常时,还可以抛出一个指针。当然这种做法并不总是安全的。如果要确保安全,应该将指针指向全局(静态)对象的指针或指向动态申请的空间,或者被抛出的指针在本函数内被捕获。否则,利用一个被抛出的指向已经被销毁的对象指针很危险。如果实在要用,首先,必须保证对象的析构函数不能对对象的内容作损伤性的修改,其次,对象的空间没有被其他新产生的变量覆盖。也就说,尽管对象被释放,但它的有效内容依然保留在栈中。
C++异常捕捉与处理的深入讲解
前言
在阅读别人开发的项目中,也许你会经常看到了多处使用异常的代码,也许你也很少遇见使用异常处理的代码。那在什么时候该使用异常,又在什么时候不该使用异常呢?在学习完异常基本概念和语法之后,后面会有讲解。
(1)异常抛出和捕捉语句
//1.抛出异常 throw 异常对象 //2.异常捕捉 try { 可能会发生异常的代码 } catch (异常对象){ 异常处理代码 } |
- throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
- try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。
- catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。
(2)异常的处理规则
- throw抛出的异常类型与catch抓取的异常类型要一致;
- throw抛出的异常类型可以是子类对象,catch可以是父类对象;
- catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常捕获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获;
- 如果使用catch参数中,使用基类捕获派生类对象,一定要使用传递引用的方式,例如catch (exception &e);
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码;
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个;
- 在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问;
- 栈展开会沿着嵌套函数的调用链不断查找,直到找到了已抛出的异常匹配的catch子句。如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止。
(3)实例
实例1:抛出自定义类型异常。
class Data { public : Data() {} }; void fun( int n) { if (n==0) throw 0; //抛异常 int异常 if (n==1) throw "error" ; //抛字符串异常 if (n==2) { Data data; throw data; } if (n>3) { throw 1.0; } } int main() { try { fun(6); //当异常发生fun里面,fun以下代码就不会再执行,调到catch处执行异常处理代码,后继续执行catch以外的代码。当throw抛出异常后,没有catch捕捉,则整个程序会退出,不会执行整个程序的以下代码 cout<< "*************" <<endl; } catch ( int i) { cout<<i<<endl; } catch ( const char *ptr) { cout<<ptr<<endl; } catch (Data &d) { cout<< "data" <<endl; } catch (...) //抓取 前面异常以外的所有其他异常 { cout<< "all" <<endl; } return 0; } |
实例2:标准出错类抛出和捕捉异常。
#include <iostream> using namespace std; int main() { try { char * p = new char [0x7fffffff]; //抛出异常 } catch (exception &e){ cout << e.what() << endl; //捕获异常,然后程序结束 } return 0; } |
输出结果:
当使用new进行开空间时,申请内存失败,系统就会抛出异常,不用用户自定义异常类型,此时捕获到异常时,就可告诉使用者是哪里的错误,便于修改。

实例3:继承标准出错类的派生类的异常抛出和捕捉。
#include <iostream> #include <exception> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> using namespace std; class FileException : public exception { public : FileException(string msg) { this ->exStr = msg; } virtual const char *what() const noexcept //声明这个函数不能再抛异常 { return this ->exStr.c_str(); } protected : string exStr; }; void fun() { int fd = ::open( "./open.txt" ,O_RDWR); if (fd<0) { FileException openFail( "open fail" ); //创建异常对象 throw openFail; //抛异常 } } int main( ) { try { fun(); } catch (exception &e) { //一般需要使用引用 cout<<e.what()<<endl; } cout<< "end" <<endl; return 0; } |
当文件不存在时,输出结果:

如果在Linux上运行,上述代码需要根据环境修改:
98标准写法
~FileException() throw (){} //必须要 virtual const char *what() const throw () //声明这个函数不能再抛异常 { return this ->exStr.c_str(); } //编译 g++ main.cpp |
2011标准写法
~FileException()noexcept{} //必须要 virtual const char *what() const noexcept //声明这个函数不能再抛异常 { return this ->exStr.c_str(); } //编译 g++ main.cpp -std=c++11 指定用c++11标准编译 |
(4)总结
1. 使用异常处理的优点:
传统错误处理技术,检查到一个错误,只会返回退出码或者终止程序等等,我们只知道有错误,但不能更清楚知道是哪种错误。使用异常,把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现的错误是什么错误,并去处理,而是否终止程序就把握在调用者手里了。
2. 使用异常的缺点:
如果使用异常,光凭查看代码是很难评估程序的控制流:函数返回点可能在你意料之外,这就导致了代码管理和调试的困难。启动异常使得生成的二进制文件体积变大,延长了编译时间,还可能会增加地址空间的压力。
C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。 这个需要使用RAII来处理资源的管理问题。学习成本较高。
C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
3. 什么时候使用异常?
建议:除非已有的项目或底层库中使用了异常,要不然尽量不要使用异常,虽然提供了方便,但是开销也大。
4. 程序所有的异常都可以catch到吗?
并非如此,只有发生异常,并且又抛出异常的情况才能被catch到。例如,数组下标访问越界的情况,系统是不会自身抛出异常的,所以我们无论怎么catch都是无效的;在这种情况,我们需要自定义抛出类型,判断数组下标是否越界,然后再根据自身需要throw自定义异常对象,这样才可以catch到异常,并进行进一步处理。