异常
一、为什么要有异常——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>
1 /* 2 * 异常练习 3 */ 4 #include <iostream> 5 #include <cstdio> 6 using namespace std; 7 class A { 8 public: 9 A (void) { 10 cout << "A构造" << endl; 11 } 12 ~A (void) { 13 cout << "A析构" << endl; 14 } 15 }; 16 void func3 (void) { 17 A a; 18 FILE* fp = fopen ("none", "r"); 19 if (! fp) { 20 cout << "throw前" << endl; 21 throw -1; 22 cout << "throw后" << endl; 23 } 24 cout << "文件打开成功!" << endl; 25 // ... 26 fclose (fp); 27 } 28 void func2 (void) { 29 A a; 30 cout << "func3()前" << endl; 31 func3 (); 32 cout << "func3()后" << endl; 33 // ... 34 } 35 void func1 (void) { 36 A a; 37 cout << "func2()前" << endl; 38 func2 (); 39 cout << "func2()后" << endl; 40 // ... 41 } 42 int main (void) { 43 try { 44 cout << "func1()前" << endl; 45 func1 (); 46 cout << "func1()后" << endl; 47 } 48 catch (int ex) { 49 if (ex == -1) { 50 cout << "执行失败!改天再见!" << endl; 51 return -1; 52 } 53 } 54 // ... 55 cout << "执行成功!恭喜恭喜!" << endl; 56 return 0; 57 }
1 /* 2 *异常练习 3 */ 4 #include <iostream> 5 #include <cstdio> 6 #include <cstdlib> 7 using namespace std; 8 void foo (void) { 9 FILE* fp = fopen ("none", "r"); 10 if (! fp) 11 throw "打开文件失败!"; 12 void* pv = malloc (0xFFFFFFFF); 13 if (! pv) 14 throw "内存分配失败!"; 15 // ... 16 } 17 int main (void) { 18 try { 19 foo (); 20 } 21 catch (const char* ex) { 22 cout << ex << endl; 23 return -1; 24 } 25 return 0; 26 }
1 /* 2 * 异常练习 3 * */ 4 #include <iostream> 5 #include <cstdio> 6 #include <cstdlib> 7 using namespace std; 8 class Error {}; 9 class FileError : public Error {}; 10 class MemError : public Error {}; 11 void foo (void) { 12 FILE* fp = fopen ("none", "r"); 13 if (! fp) 14 throw FileError (); 15 void* pv = malloc (0xFFFFFFFF); 16 if (! pv) 17 throw MemError (); 18 // ... 19 } 20 int main (void) { 21 try { 22 foo (); 23 } 24 catch (FileError& ex) { 25 cout << "打开文件失败!" << endl; 26 return -1; 27 } 28 catch (MemError& ex) { 29 cout << "内存分配失败!" << endl; 30 return -1; 31 } 32 catch (Error& ex) { 33 cout << "一般性错误!" << endl; 34 return -1; 35 } 36 return 0; 37 }
1 /* 2 * 异常处理 3 * */ 4 #include <iostream> 5 #include <cstdio> 6 #include <cstdlib> 7 using namespace std; 8 class Error { 9 public: 10 virtual void print (void) const = 0; 11 }; 12 class FileError : public Error { 13 public: 14 FileError (const string& file, int line) : 15 m_file (file), m_line (line) {} 16 void print (void) const { 17 cout << "在" << m_file << "文件的第" 18 << m_line << "行,发生了文件错误!" 19 << endl; 20 } 21 private: 22 string m_file; 23 int m_line; 24 }; 25 class MemError : public Error { 26 public: 27 void print (void) const { 28 cout << "内存不够啦!!!" << endl; 29 } 30 }; 31 void foo (void) { 32 FILE* fp = fopen ("none", "r"); 33 if (! fp) 34 throw FileError (__FILE__, __LINE__); 35 void* pv = malloc (0xFFFFFFFF); 36 if (! pv) 37 throw MemError (); 38 // ... 39 } 40 int main (void) { 41 try { 42 foo (); 43 } 44 catch (Error& ex) { 45 ex.print (); 46 return -1; 47 } 48 return 0; 49 }
1 /* 2 *异常练习 3 */ 4 #include <iostream> 5 using namespace std; 6 void foo (void) { 7 throw 10; 8 } 9 void bar (void) { 10 try { 11 foo (); 12 } 13 catch (int& ex) { 14 --ex; 15 throw; // 继续抛出 16 } 17 // ... 18 } 19 int main (void) { 20 try { 21 bar (); 22 } 23 catch (int& ex) { 24 cout << ex << endl; // 9 25 } 26 return 0; 27 }
1 /* 2 *异常练习 3 */ 4 #include <iostream> 5 using namespace std; 6 void foo (void) throw (int, double, const char*) { 7 // throw 1; 8 // throw 3.14; 9 throw "Hello, Exception !"; 10 } 11 int main (void) { 12 try { 13 foo (); 14 } 15 catch (int ex) { 16 cout << ex << endl; 17 } 18 catch (double ex) { 19 cout << ex << endl; 20 } 21 catch (const char* ex) { 22 cout << ex << endl; 23 } 24 return 0; 25 }
1 /* 2 * 异常处理练习 3 */ 4 #include <iostream> 5 #include <stdexcept> 6 #include <cstdio> 7 using namespace std; 8 class FileError : public exception { 9 private: 10 const char* what (void) const throw () { 11 return "文件访问失败!"; 12 } 13 }; 14 class B { 15 public: 16 B (void) { 17 cout << "B构造" << endl; 18 } 19 ~B (void) { 20 cout << "B析构" << endl; 21 } 22 }; 23 class C { 24 public: 25 C (void) { 26 cout << "C构造" << endl; 27 } 28 ~C (void) { 29 cout << "C析构" << endl; 30 } 31 }; 32 class A : public C { 33 public: 34 A (void) : m_b (new B) { 35 FILE* fp = fopen ("none", "r"); 36 if (! fp) { 37 delete m_b; 38 throw FileError (); 39 } 40 // ... 41 fclose (fp); 42 } 43 ~A (void) { 44 delete m_b; 45 } 46 private: 47 B* m_b; 48 // C m_c; 49 }; 50 int main (void) { 51 try { 52 A a; 53 // ... 54 } 55 catch (exception& ex) { 56 cout << ex.what () << endl; 57 return -1; 58 } 59 return 0; 60 }
四、构造函数中的异常
构造函数可以抛出异常,而且有些时候还必须抛出异常,以通知调用者构造过程中所发生的错误。
构造函数抛出异常后,开始执行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 (...) { ... }