C语言异常

- 程序在运行过程中可能产生异常

- 异常( Exception ) 与 Bug 的区别
异常是程序运行时可预料的执行分支
Bug 是程序中的错误,不是希望的,是不被预期的运行方式

异常(Exception)和Bug的对比

- 异 常

  • 运行时产生除0的情况
  • 需要打开的外部文件不存在
  • 数组访问时越界

- Bug

  • 使用野指针
  • 堆数组使用结束后未释放
  • 选择排序无法处理长度为 0 的数组

C语言经典处理方式if..else

1
2
3
4
5
void func (... )
if ( 判断是否产生异常 )
正常情况代码逻辑;
else
异常情况代码逻辑;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>

using namespace std;

double divide(double a, double b, int* valid)
{
const double delta = 0.000000000000001;
double ret = 0;

if( !((-delta < b) && (b < delta)) )//除数的值不为0
{
ret = a / b;

*valid = 1;
}
else
{
*valid = 0;
}

return ret;
}

int main(int argc, char *argv[])
{
int valid = 0;
double r = divide(1, 0, &valid);

if( valid )
{
cout << "r = " << r << endl;
}
else
{
cout << "Divided by zero..." << endl;
}

return 0;
}

我们用C语言方式来处理,if…else判断,并且额外携带第三个参数用于判断是否有异常。

缺 陷
- divide 函数有 3 个参数 , 难以理解其用法

- divide 函数调用后必须判断 valid 代表的结果
当 valid 为 true 时 , 运算结果正常
当 valid 为 false 时 , 运算过程出现异常

通过 setjmp() 和 longjmp() 进行优化

  • int setjmp(jmp_buf env) 将当前上下文保存在 jmp_buf 结构体中
  • void longjmp(jmp_buf env, int val) 从 jmp_buf 结构体中恢复 setjmp()保存的上下文, 最终从setjmp 函数调用点返回 , 返回值为val
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>
#include <csetjmp>

using namespace std;

static jmp_buf env;

double divide(double a, double b)
{
const double delta = 0.000000000000001;
double ret = 0;

if( !((-delta < b) && (b < delta)) )
{
ret = a / b;
}
else
{
longjmp(env, 1);//跳转到之前保存的上下文env
}

return ret;
}

int main(int argc, char *argv[])
{
if( setjmp(env) == 0 )//保存上下文
{
double r = divide(1, 1);

cout << "r = " << r << endl;
}
else
{
cout << "Divided by zero..." << endl;
}

return 0;
}

缺 陷
- setjmp() 和 longjmp()的引入

  • 必然涉及到使用全局变量
  • 暴力跳导致代码可读性降低
  • 本质还是if …else …异常处理方式

C++异常处理

try…catch

C++内置了异常处理的语法元素 try … catch …

try 语句处理正常代码逻辑

  • - catch 语句处理异常情况
  • - try 语句中的异常甶对应的 catch 语句处理
1
2
3
4
try
double r = divide(1, 0);
catch (.....)
cout << "Divided by zero..." << endl;

C++ 通过throw 语句抛出异常信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
double divide(double a, double b)
{
const double delta = 0.000000000000001;
double ret = 0;

if( !((-delta < b) && (b < delta)) )
{
ret = a / b;
}
else{
throw 0;
}
return ret;
}

C+ + 异常处理分析
- throw 拋出的异常必须被 catch 处理
• 当前函数能够处理异常 , 程序继续往下执行
• 当前函数无法处理异常 , 则函数停止执行 , 并返回

未被处理的异常会顺着函数调用栈向上传播 ,直到被处理为止 , 否则程序将停止执行。

image-20220305095506039

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>
using namespace std;
double divide(double a, double b){

const double delta = 0.000000000000001;
double ret = 0;

if( !((-delta < b) && (b < delta)) ){
ret = a / b;
}
else{
throw 0;
}
return ret;
}

int main(int argc, char *argv[])
{
try
{
double r = divide(1, 0);
cout << "r = " << r << endl;
}
catch(...)
{
cout << "Divied by zero..." << endl;
}
return 0;
}

同 一 个 try 语句可以跟上多个 catch 语句

  • - catch 语句可以定义具体处理的异常类型
  • - 不同类型的异常不同的 catch 语句负责处理
  • - try 语句中可以抛出任何类型的异常
  • - catch(…) 用于处理所有类型的异常
  • - 任何异常都只能被捕获( catch ) —次

注意是严格匹配,不会进行任何的类型转换

image-20220305100612880

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <string>

using namespace std;

void Demo1()
{
try
{
throw 1;
}
catch(char c)
{
cout << "catch(char c)" << endl;
}
catch(short c)
{
cout << "catch(short c)" << endl;
}
catch(int c)// throw 1 类型严格匹配int
{
cout << "catch(int c)" << endl;
}
catch(double c)
{
cout << "catch(double c)" << endl;
}
catch(...)
{
cout << "catch(...)" << endl;
}
}

void Demo2()
{
throw string("fengyun");
}

int main(int argc, char *argv[])
{
Demo1();

try
{
Demo2();
}
catch(char* s)
{
cout << "catch(char *s)" << endl;
}
catch(const char* cs)
{
cout << "catch(const char *cs)" << endl;
}
catch(string ss)//类型非常严格的匹配
{
cout << "catch(string ss)" << endl;
}

return 0;
}
  • C+ + 中直接支持异常处理的概念
  • try … catch … 是 C+ + 中异常处理的专用语句
  • try 语句处理正常代码逻辑 , catch 语句处理异常廣况
  • 同一个try语句可以跟上多个catch语句
  • 异常处理必须严格匹配,不进行任何的类型转换

catch语句块可以抛出异常

image-20220305101925068

为什么要在 catch 中重新拋出异常 ?

catch 中捕获的异常可以被重新解释后拋出
工程开发中使用这样的方式统一异常类型

image-20220305102215913

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <iostream>
#include <string>

using namespace std;

void Demo()
{
try
{
try
{
throw 'c';
}
catch(int i)
{
cout << "Inner: catch(int i)" << endl;
throw i;
}
catch(...)
{
cout << "Inner: catch(...)" << endl;
throw;
}
}
catch(...)
{
cout << "Outer: catch(...)" << endl;
}
}


/*
假设: 当前的函数式第三方库中的函数,因此,我们无法修改源代码

函数名: void func(int i)
抛出异常的类型: int
-1 ==》 参数异常
-2 ==》 运行异常
-3 ==》 超时异常
*/
void func(int i)
{
if( i < 0 )
{
throw -1;
}

if( i > 100 )
{
throw -2;
}

if( i == 11 )
{
throw -3;
}

cout << "Run func..." << endl;
}

void MyFunc(int i)
{
try
{
func(i);
}
catch(int i)
{
switch(i)
{
case -1:
throw "Invalid Parameter";
break;
case -2:
throw "Runtime Exception";
break;
case -3:
throw "Timeout Exception";
break;
}
}
}

int main(int argc, char *argv[])
{
// Demo();

try
{
MyFunc(11);
}
catch(const char* cs)
{
cout << "Exception Info: " << cs << endl;
}

return 0;
}
  • 异常的类型可以是自定义类类型
  • 对于类类型异常的匹配依旧是至上而下严格匹配
  • 但是赋值兼容性原则在异常匹配中依然适用
  • 一般而言
    **匹配子类异常的 catch放上部**
    **匹配父类异常的 catch 放下部**  
    
  • 在工程中会定义一系列的异常类
  • 每个类代表工程中可能出现的一种异常类型
  • 代码复用时可能需要重解释不同的异常类
  • 在 定 义 catch 语句块时推荐使用引用作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <iostream>
#include <string>

using namespace std;

class Base
{
};

class Exception : public Base
{
int m_id;
string m_desc;
public:
Exception(int id, string desc)
{
m_id = id;
m_desc = desc;
}

int id() const
{
return m_id;
}

string description() const
{
return m_desc;
}
};


/*
假设: 当前的函数式第三方库中的函数,因此,我们无法修改源代码

函数名: void func(int i)
抛出异常的类型: int
-1 ==》 参数异常
-2 ==》 运行异常
-3 ==》 超时异常
*/
void func(int i)
{
if( i < 0 )
{
throw -1;
}

if( i > 100 )
{
throw -2;
}

if( i == 11 )
{
throw -3;
}

cout << "Run func..." << endl;
}

void MyFunc(int i)
{
try
{
func(i);
}
catch(int i)
{
switch(i)
{
case -1:
throw Exception(-1, "Invalid Parameter");
break;
case -2:
throw Exception(-2, "Runtime Exception");
break;
case -3:
throw Exception(-3, "Timeout Exception");
break;
}
}
}

int main(int argc, char *argv[])
{
try
{
MyFunc(11);
}
catch(const Exception& e)
{
cout << "Exception Info: " << endl;
cout << " ID: " << e.id() << endl;
cout << " Description: " << e.description() << endl;
}
catch(const Base& e)//父类对象放在下面
{
cout << "catch(const Base& e)" << endl;
}

return 0;
}

C+ + 标准库中提供了实用异常类族

  • 标准库中的异常都是从 exception 类派生的
  • exception 类有两个主要的分支
    - logic_error
     常用于程序中的可避免逻辑错误
    
    - runtime_error
     常用于程序中无法避免的恶性错误  
    

image-20220305110749280

迷惑的写法

image-20220306100635757

  • try … catch 用于分隔正常功能代码与异常处理代码
  • try … catch 可以直接将函数实现分隔为 2 部分
  • 函数声明和定义时可以直接指定可能抛出的异常类型
  • 异常声明成为函数的一部分可以提高代码可读性

函数异常声明的注意事项
- 函数异常声明是一种与编译器之间的契约
- 函数声明异常后就只能抛出声明的异常
拋出其它异常将导致程序运行终止
可以直接通过异常声明定义无异常函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>

using namespace std;

int func(int i, int j) throw(int, char)
{
if( (0 < j) && (j < 10) )
{
return (i + j);
}
else
{
throw '0';
}
}

void test(int i) try
{
cout << "func(i, i) = " << func(i, i) << endl;
}
catch(int i)
{
cout << "Exception: " << i << endl;
}
catch(...)
{
cout << "Exception..." << endl;
}


int main(int argc, char *argv[])
{
test(5);

test(10);

return 0;
}