异常
一、为什么要有异常——WHY?
1.通过返回值表达错误
局部对象都能正确的析构
层层判断返回值,流程繁琐
2.采用<setjmp.h>里面定义的setjmp/longjmp远程跳转
一步到位进入错误处理,流程简单
setjmp是给C程序员使用的,根本没有考虑到C++程序员定义的类类型,直接采用setjmp实现跳转会使得某些对象(大部分的局部对象)失去被析构的机会,即使是栈对象。
3.异常处理
局部对象都能正确的析构
一步到位进入错误处理,流程简单
二、异常的语法——WHAT?
1.异常的抛出
throw 异常对象;
异常对象可以是基本类型的变量,也可以是类类型的对象。
当程序执行错误分支时抛出异常。
2.异常的捕获
try {
可能抛出异常的语句块;
}
catch (异常类型1 异常对象1) {
处理异常类型1的语句块;
}
catch (异常类型2 异常对象2) {
处理异常类型2的语句块;
}
...
catch (...) {
处理其它类型异常的语句块;
}
异常处理的流程:throw 异常对象 之后,立即开始寻找并执行try的右大括号,如果当前函数没有try右大括号则执行函数右大括号。始终沿着函数调用的逆序,依次执行右花括号,直到try的右花括号,保证所有的局部对象都能被正确地析构,然会根据异常对象的类型,匹配相应的catch分支,进行有针对性的错误处理。
三、异常处理的使用方法——HOW?
1.抛出基本类型的异常,用不同的值代表不同的错误。
2.抛出类类型的异常,用不同的类型表示不同的错误。
3.多采用跑出类类型的异常,通过类类型的异常可以携带更多诊断信息。
4.忽略异常和继续抛出异常。
5.异常说明
在一个函数的形参表后面写如下语句:
...形参表) throw (异常类型1, 异常类型2, ...) { ... } 表示这个函数可以被捕获的异常。
...形参表) throw () - 这个函数所抛出的任何异常均无法捕获。
...形参表) 没有异常说明 - 这个函数所抛出的任何异常均可捕获。
(p.s. 派生类的虚函数不能抛出比基类被覆盖版本更多的异常类型,举例:
class A {
virtual void foo (void)
throw (int, double) { ... }
virtual void bar (void)
throw () { ... }
};
class B : public A {
void foo (void)
throw (int, char) { ... }
// ERROR
void bar (void) { ... } // ERROR
void bar (void)
throw () { ... }
};
)
6.使用标准异常:标准异常库定义了多种异常类
四、构造函数中的异常
构造函数可以抛出异常,而且有些时候还必须抛出异常,以通知调用者构造过程中所发生的错误。
构造函数抛出异常后,开始执行try的右大括号,如果没有则执行函数右大括号。
如果在一个对象的构造过程中抛出了异常,那么这个对象就称为不完整对象。不完整对象的析构函数永远不会被执行,但构造函数在未能成功构造完整对象的情况下会自动回滚(逆序析构已经创建的成员), 但因为回滚不会释放已经动态申请的资源,所以还是需要在throw之前,手动释放动态的资源。
五、析构函数中的异常
析构函数也可以抛出异常,但是会出现操作系统吐核,出现未定义错误。举例:
class A {
public:
~A(void) {
throw -1;
}
};
main()
{
try {
A a;//构造a
} //遇到块作用域结束大括号(try右大括号)开始析构a, 析构a时候抛出异常对象,抛出异常对象后最终执行try右大括号,遇到try右大括号开始析构a...这样就死循环了,部分编译器(例如gcc)经过编译期间处理就避免了死循环,但是部分编译器就没有处理这个问题,最终程序执行可能导致未定义错误,操作系统吐核。
catch(int & a) {
}
}
永远不要在析构函数中抛出异常。可以通过try{}catch(...){}来全面拦截所有可能引发的异常,让析构函数没有可能向外抛出异常。例如:
class A {
public:
~A (void) {
//throw -1;
try {
sysfunc ();
}
catch (...) {}
}
};
try {
A a;
a.foo ();
}
catch (...) { ... }
一、为什么要有异常——WHY?
1.通过返回值表达错误
局部对象都能正确的析构
层层判断返回值,流程繁琐
2.采用<setjmp.h>里面定义的setjmp/longjmp远程跳转
一步到位进入错误处理,流程简单
setjmp是给C程序员使用的,根本没有考虑到C++程序员定义的类类型,直接采用setjmp实现跳转会使得某些对象(大部分的局部对象)失去被析构的机会,即使是栈对象。
3.异常处理
局部对象都能正确的析构
一步到位进入错误处理,流程简单
二、异常的语法——WHAT?
1.异常的抛出
throw 异常对象;
异常对象可以是基本类型的变量,也可以是类类型的对象。
当程序执行错误分支时抛出异常。
2.异常的捕获
try {
可能抛出异常的语句块;
}
catch (异常类型1 异常对象1) {
处理异常类型1的语句块;
}
catch (异常类型2 异常对象2) {
处理异常类型2的语句块;
}
...
catch (...) {
处理其它类型异常的语句块;
}
异常处理的流程:throw 异常对象 之后,立即开始寻找并执行try的右大括号,如果当前函数没有try右大括号则执行函数右大括号。始终沿着函数调用的逆序,依次执行右花括号,直到try的右花括号,保证所有的局部对象都能被正确地析构,然会根据异常对象的类型,匹配相应的catch分支,进行有针对性的错误处理。
三、异常处理的使用方法——HOW?
1.抛出基本类型的异常,用不同的值代表不同的错误。
2.抛出类类型的异常,用不同的类型表示不同的错误。
3.多采用跑出类类型的异常,通过类类型的异常可以携带更多诊断信息。
4.忽略异常和继续抛出异常。
5.异常说明
在一个函数的形参表后面写如下语句:
...形参表) throw (异常类型1, 异常类型2, ...) { ... } 表示这个函数可以被捕获的异常。
...形参表) throw () - 这个函数所抛出的任何异常均无法捕获。
...形参表) 没有异常说明 - 这个函数所抛出的任何异常均可捕获。
(p.s. 派生类的虚函数不能抛出比基类被覆盖版本更多的异常类型,举例:
class A {
virtual void foo (void)
throw (int, double) { ... }
virtual void bar (void)
throw () { ... }
};
class B : public A {
void foo (void)
throw (int, char) { ... }
// ERROR
void bar (void) { ... } // ERROR
void bar (void)
throw () { ... }
};
)
6.使用标准异常:标准异常库定义了多种异常类
#include <stdexcept>
/*
* 异常练习
*/
#include <iostream>
#include <cstdio>
using namespace std;
class A {
public:
A (void) {
cout << "A构造" << endl;
}
~A (void) {
cout << "A析构" << endl;
}
};
void func3 (void) {
A a;
FILE* fp = fopen ("none", "r");
if (! fp) {
cout << "throw前" << endl;
throw -1;
cout << "throw后" << endl;
}
cout << "文件打开成功!" << endl;
// ...
fclose (fp);
}
void func2 (void) {
A a;
cout << "func3()前" << endl;
func3 ();
cout << "func3()后" << endl;
// ...
}
void func1 (void) {
A a;
cout << "func2()前" << endl;
func2 ();
cout << "func2()后" << endl;
// ...
}
int main (void) {
try {
cout << "func1()前" << endl;
func1 ();
cout << "func1()后" << endl;
}
catch (int ex) {
if (ex == -1) {
cout << "执行失败!改天再见!" << endl;
return -1;
}
}
// ...
cout << "执行成功!恭喜恭喜!" << endl;
return 0;
}
/*
*异常练习
*/
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
void foo (void) {
FILE* fp = fopen ("none", "r");
if (! fp)
throw "打开文件失败!";
void* pv = malloc (0xFFFFFFFF);
if (! pv)
throw "内存分配失败!";
// ...
}
int main (void) {
try {
foo ();
}
catch (const char* ex) {
cout << ex << endl;
return -1;
}
return 0;
}
/*
* 异常练习
* */
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Error {};
class FileError : public Error {};
class MemError : public Error {};
void foo (void) {
FILE* fp = fopen ("none", "r");
if (! fp)
throw FileError ();
void* pv = malloc (0xFFFFFFFF);
if (! pv)
throw MemError ();
// ...
}
int main (void) {
try {
foo ();
}
catch (FileError& ex) {
cout << "打开文件失败!" << endl;
return -1;
}
catch (MemError& ex) {
cout << "内存分配失败!" << endl;
return -1;
}
catch (Error& ex) {
cout << "一般性错误!" << endl;
return -1;
}
return 0;
}
/*
* 异常处理
* */
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Error {
public:
virtual void print (void) const = 0;
};
class FileError : public Error {
public:
FileError (const string& file, int line) :
m_file (file), m_line (line) {}
void print (void) const {
cout << "在" << m_file << "文件的第"
<< m_line << "行,发生了文件错误!"
<< endl;
}
private:
string m_file;
int m_line;
};
class MemError : public Error {
public:
void print (void) const {
cout << "内存不够啦!!!" << endl;
}
};
void foo (void) {
FILE* fp = fopen ("none", "r");
if (! fp)
throw FileError (__FILE__, __LINE__);
void* pv = malloc (0xFFFFFFFF);
if (! pv)
throw MemError ();
// ...
}
int main (void) {
try {
foo ();
}
catch (Error& ex) {
ex.print ();
return -1;
}
return 0;
}
/*
*异常练习
*/
#include <iostream>
using namespace std;
void foo (void) {
throw 10;
}
void bar (void) {
try {
foo ();
}
catch (int& ex) {
--ex;
throw; // 继续抛出
}
// ...
}
int main (void) {
try {
bar ();
}
catch (int& ex) {
cout << ex << endl; // 9
}
return 0;
}
/*
*异常练习
*/
#include <iostream>
using namespace std;
void foo (void) throw (int, double, const char*) {
// throw 1;
// throw 3.14;
throw "Hello, Exception !";
}
int main (void) {
try {
foo ();
}
catch (int ex) {
cout << ex << endl;
}
catch (double ex) {
cout << ex << endl;
}
catch (const char* ex) {
cout << ex << endl;
}
return 0;
}
/*
* 异常处理练习
*/
#include <iostream>
#include <stdexcept>
#include <cstdio>
using namespace std;
class FileError : public exception {
private:
const char* what (void) const throw () {
return "文件访问失败!";
}
};
class B {
public:
B (void) {
cout << "B构造" << endl;
}
~B (void) {
cout << "B析构" << endl;
}
};
class C {
public:
C (void) {
cout << "C构造" << endl;
}
~C (void) {
cout << "C析构" << endl;
}
};
class A : public C {
public:
A (void) : m_b (new B) {
FILE* fp = fopen ("none", "r");
if (! fp) {
delete m_b;
throw FileError ();
}
// ...
fclose (fp);
}
~A (void) {
delete m_b;
}
private:
B* m_b;
// C m_c;
};
int main (void) {
try {
A a;
// ...
}
catch (exception& ex) {
cout << ex.what () << endl;
return -1;
}
return 0;
}
四、构造函数中的异常
构造函数可以抛出异常,而且有些时候还必须抛出异常,以通知调用者构造过程中所发生的错误。
构造函数抛出异常后,开始执行try的右大括号,如果没有则执行函数右大括号。
如果在一个对象的构造过程中抛出了异常,那么这个对象就称为不完整对象。不完整对象的析构函数永远不会被执行,但构造函数在未能成功构造完整对象的情况下会自动回滚(逆序析构已经创建的成员), 但因为回滚不会释放已经动态申请的资源,所以还是需要在throw之前,手动释放动态的资源。
五、析构函数中的异常
析构函数也可以抛出异常,但是会出现操作系统吐核,出现未定义错误。举例:
class A {
public:
~A(void) {
throw -1;
}
};
main()
{
try {
A a;//构造a
} //遇到块作用域结束大括号(try右大括号)开始析构a, 析构a时候抛出异常对象,抛出异常对象后最终执行try右大括号,遇到try右大括号开始析构a...这样就死循环了,部分编译器(例如gcc)经过编译期间处理就避免了死循环,但是部分编译器就没有处理这个问题,最终程序执行可能导致未定义错误,操作系统吐核。
catch(int & a) {
}
}
永远不要在析构函数中抛出异常。可以通过try{}catch(...){}来全面拦截所有可能引发的异常,让析构函数没有可能向外抛出异常。例如:
class A {
public:
~A (void) {
//throw -1;
try {
sysfunc ();
}
catch (...) {}
}
};
try {
A a;
a.foo ();
}
catch (...) { ... }