C++异常

本文最后更新于:2021年2月8日 晚上

概览:C++异常,自定义异常类型以及栈解旋。

c++异常之简单的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

int division(int a, int b) {
//a是被除数,b是除数

//当b为0时,抛出异常
if (b == 0) throw b;
else return a / b;
}

int main() {

try{
division(10, 0);
}
catch (int e) {//捕获异常,e为接收异常的参数,与抛出的异常同类型
cout << "被除数为" << e << endl;
}

return 0;
}

C++异常的优点

  • C++函数的返回值可以被忽略,但是异常是不能被忽略的。如果程序出现了异常,但是没有被捕获,程序就会终止。(C++异常必须被处理)这样可以使得程序更加健壮。
  • 函数返回值可能没有什么语义信息,但是异常却可以包含语义信息,例如异常类名就可以。
  • 异常作为一个类,可以拥有自身的成员,这些成员足以传递信息。
  • 异常的处理可以跳级调用,当在多个函数的调用栈时出现了异常,不需要每级函数都进行处理,只需要在某一处进行处理即可。

C++异常接口声明

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
void excpt1() throw (int,float,double)
{
//表明函数只能够抛出这三种类型的异常,及其子类型的异常

throw "abc";

//而"abc"是char*类型
//如果类的内部抛出的不是指定的异常,会导致程序调用terminate函数中断执行。
//在linux下是这样,windows处理不够
}

void excpt2() throw()
{
//表示函数不抛出任何异常
}

void excpt3()
{
//不写异常的声明接口 表示函数可以抛出任何类型的异常
}

void test3() {

try {
excpt1();
}
catch(...){ //...表示接收任意类型的异常
cout << "1发生异常" << endl;
}

excpt2();

excpt3();
}

异常类型自定义

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
//自定义一个异常类型
class MyException {
public:
MyException(string err) {
m_err = err;
}
MyException(const MyException& other) {
m_err = other.m_err;
}
MyException& operator=(const MyException& other) {
if (this == &other)//自赋值
return *this;

m_err = other.m_err;
return *this;
}

void showError() {
cout << this->m_err << endl;
}

private:
string m_err;
};

void throwEcp() {
throw MyException("异常发生");
}

void test4() {
try {
throwEcp();
}
catch (MyException e) {
e.showError();
}
}

我在Applied C++这本书中看到了可以在类的成员中定义一个异常类,然后发生异常时抛出这个异常类的对象。

C++异常跨函数

C++的异常机制可以跨函数进行处理,但是C++异常必须是需要处理的,否则就会报错。

函数逐层调用时,一个函数抛出异常,则该异常会逐级向上抛出,如果该异常到了顶层都没有被处理的话,程序就会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int division(int a, int b) {
//a是被除数,b是除数

//当b为0时,抛出异常
if (b == 0) throw b;
else return a / b;
}

int driver(int a, int b) {

//直接调用而不做异常处理
return division(a, b);
}

void test2() {
try{
driver(10,0);
}
catch(int e){
cout << "被除数为" << e << endl;
}
}

栈解旋

异常被抛出后,从进入try块起,到异常被抛出前的这段时间里,在栈上构造的所有对象,都会被自动析构

析构的顺序与构造的顺序相反,这就是栈解旋。

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
class Person {

public:
Person() {
cout << "对象创建"<<endl;
}
~Person() {
cout << "对象析构" << endl;
}

};

int personDriver(int a, int b) {

Person p;
return division(a, b);
}

void test4() {

//异常被抛出后,从进入try块起,到异常被抛出前的这段时间里,
//在栈上构造的所有对象,都会被自动析构。
//这就是栈解旋

try {
personDriver(10, 0);
}
catch (int e) {
cout << "捕获异常: " << e << endl;
}

}

输出结果:

1
2
3
对象创建
对象析构
捕获异常: 0

异常变量的生命周期

  • 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
//自定义一个异常类型
class MyException {
public:
MyException() {
cout << "普通构造函数" << endl;
}
MyException(const MyException& other) {
cout << "拷贝构造函数" << endl;
}
MyException& operator=(const MyException& other) {

cout << "重载=函数" << endl;
return *this;
}
~MyException() {
cout << "析构函数" << endl;
}

void showError() {
cout << "抛出异常" << endl;
}
};

void excp1() {
throw MyException();
}

void excp2() {
throw &MyException();
}
//不能返回局部变量的地址,因为这个对象在函数执行结束时会被释放掉

void excp3() {
throw new MyException();
}

1. 普通类型对象

1
2
3
4
5
6
7
8
9
10
void test1() {

try {
excp1();
}
catch (MyException e) {
e.showError();
}

}

执行结果:

1
2
3
4
5
普通构造函数
拷贝构造函数
抛出异常
析构函数
析构函数

2.引用

1
2
3
4
5
6
7
8
9
void test2() {

try {
excp1(); //注意这里是函数1,异常捕获采用引用的方式
}
catch (MyException &e) {
e.showError();
}
}

执行结果:

1
2
3
普通构造函数
抛出异常
析构函数

3.指针

对于使用指针来捕获异常对象,需要使用excp3()这样的方式,而不是excp2()这样的方式。函数2会产生错误。

1
2
3
4
5
6
7
8
9
10
11
void test3() {

try {
excp3();
}
catch (MyException *e) {
e->showError();
delete e;//注意记得手动释放
}

}

执行结果:

1
2
3
普通构造函数
抛出异常
析构函数

编写自己的异常类

  1. 建议自己的异常类要继承标准异常类,不容易导致混乱。
  2. 当继承标准异常类的时候,应当重载父类的what函数以及虚析构函数。
  3. 在栈展开过程中,要复制异常类型,即会有拷贝操作。那么要根据类的成员来判断是否需要自行提供拷贝构造函数。

标准异常类名:exception,其所在头文件为#include <stdexcept>

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
#include <iostream>
#include <stdexcept>//标准异常的头文件
using namespace std;

class MyException :public exception
{
public:
MyException(const char* error)
{
myError = new char[strlen(error) + 1];
strcpy_s(myError, strlen(error) + 1, error);
}
MyException(const MyException& e)
{
myError = new char[strlen(e.myError) + 1];
strcpy_s(myError, strlen(e.myError) + 1, e.myError);
}
MyException& operator=(const MyException& e)
{
myError = new char[strlen(e.myError) + 1];
strcpy_s(myError, strlen(e.myError) + 1, e.myError);
}
~MyException()
{
if (myError != nullptr)
{
delete[] myError;
myError = nullptr;
}
}
virtual const char* what() const
{
return myError;
}
public:
char* myError;
};

class Person {
public:
Person()
{
mAge = 0;
}

void setAge(int age)
{
if (age < 0 || age >= 120)
{
throw MyException("年龄超出正常人类!");
}
else this->mAge = age;
}
public:
int mAge;
};

int main()
{
Person p1;
try {
p1.setAge(-45);
}
catch (exception& e)//这里采用引用,用不到拷贝构造。但是如果不使用引用的话,会出现Unknow exception!
//必须使用引用,因为exception是抽象基类,不能够实例化对象(或者修改throw使其抛回一个指针,改用指针接收)
//这里是父类引用指向子类对象,并且有虚函数以及子类的重写,是多态
{
cout << e.what() << endl;
}
return 0;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!