C++——异常

异常
C语言中的异常
在C语言里,我们传统的处理错误的方式只有两种:
- 手动条件判断并终止程序,比如assert断言;
- 返回错误码, 然后对照错误码找错误。
但是,无论是哪种方法,最后都会面临一种问题:

C语言中的错误码都只是告诉你大致错误的类型,却不会告诉你到底哪里出错了,而且也不会告诉特别详细的错误,所以让修bug的人往往直接开摆

但是,C++中便有了新的处理错误的方式:抛异常
C++中的异常
在C++里,如果某一个函数有自身无法处理的错误,便可以进行抛异常。抛出异常后,该函数立即终止,返回到上一层函数中;如果上一个函数也没办法处理这个异常,则接着抛出到更外一层,一直到能接收这个异常为止。

C++处理异常的方法
异常的抛出和捕获
在C++中,有三个处理异常的关键字:
- throw:当问题出现时,便会通过throw抛出异常,结束该函数并将异常抛到上一层函数
- catch:捕获异常,当异常被抛出时,catch可以接收到异常,并判断是否可以被捕获
- try:try是catch的先制条件,catch只可以捕获try中的异常
直接看概念非常抽象,不妨看一个例子:
void push_back(char* a,char ins, int num, int size)
{//num代表数组元素个数,size代表数组大小//如果数组元素个数等于数组大小,则表示数组已满,抛出异常if (num == size){throw "数组已满";}//如果没有抛出异常,则程序正常运行//如果抛出了异常,则throw相当于return,往后不会继续运行a[num] = ins;
}void test()
{char a[1];try {push_back(a, 'c', 0, 1);}//catch只会捕获try中的异常,然后将异常捕获到s中//这里函数没有抛出异常,catch捕获了个寂寞,所以不走catch语句catch (const char* s){cout << s << endl;}try {push_back(a, 'c', 1, 1);}//这里函数抛出了异常,catch捕获到异常:“数组已满”,并将异常存储到s中//于是程序输出“数组已满”,然后该test函数继续运行catch (const char* s){cout << s << endl;}}int main()
{test();
}
但是可能会出现一种情况:catch中捕获的变量和抛出异常的变量类型不一样,那又是个啥效果?
void push_back(char* a, char ins, int num, int size)
{if (num == size){throw "数组已满";}a[num] = ins;
}void test()
{char a[1] = { 'c' };try {push_back(a, 'c', 1, 1);}//抛出和捕获的不一样catch (int i){cout << i << endl;}
}int main()
{test();
}

但是,是不是只要捕获的类型和抛出的类型不一样,程序就会报错呢?也不是
void push_back(char* a, char ins, int num, int size)
{if (num == size){throw "数组已满";}a[num] = ins;
}void test()
{char a[1] = { 'c' };try {push_back(a, 'c', 1, 1);}//抛出和捕获的不一样catch (int* i){cout << i << endl;}//当该函数无法处理这个异常(即捕获的类型和抛出的类型不一样)//则函数也会立即结束,抛出异常cout << "test" << endl;
}int main()
{//在main函数再捕获一次try {test();}//一直到最后一个函数也无法处理这个错误,程序才会终止并报错catch (const char* s){cout << s << endl;}
}
当本函数无法处理的时候,则会抛出异常到再外一层,然后本函数立即终止, 往复循环,一直到能完美捕获到这个异常为止

总结以上:假设我们有函数1,2,3,栈帧调用顺序为1->2->3,运行到了函数3
- 如果没有达到异常条件,没有抛出异常,则函数和程序都正常运行
- 如果3达到异常条件,抛出了异常,则该throw相当于return,函数3立即销毁,并将异常信息返回到函数2try对应的catch中。
- 如果函数2的catch类型可以匹配,则运行catch中的内容,然后程序再继续正常运行。
- 如果函数2的catch类型无法匹配,则函数2立即销毁,将该异常信息继续向函数1抛出。
- 如果函数1的catch类型可以匹配,则函数1运行catch中的内容,然后函数1继续正常运行
- 如果函数1的catch类型无法匹配,则函数1立即销毁。如果函数1是主函数,则代表该异常无法被捕获到,程序报错
同时,为了避免一个try可能收到多种异常类型,catch也可以根据类型写出不同的情况:
void test()
{try{//可能会返回多种异常类型func();}catch (int i){//...}catch (string s){//...}catch (char c){//...}//表示其他的所有情况catch (...){//...}
}
异常的再抛出
catch在捕获到异常时,有时该函数并不会处理异常,而是在将该异常信息进行修正后,再去将异常抛到下一层,起到传球的作用
void throw_error()
{string s="error";throw s;
}void func1()
{try {throw_error();}//修正异常信息,表示异常来自于函数func1//一定要用引用,抛出的是最初的异常信息catch (string& s){s += "_from_func1";//再抛出throw;}
}void func2()
{try {throw_error();}//修正异常信息,表示异常来自于函数func2catch (string& s){s += "_from_func2";//再抛出throw;}
}int main()
{try {func1();func2();}catch(const string& s){cout << s << endl;}
}
异常的安全
异常的一大缺点便是——会产生内存泄漏的问题
void func1()
{int* a = new int[10];if (1){throw "error";}//如果产生异常,则a的空间不会被释放delete[]a;
}
如果程序中产生了异常,则后面的代码段不会继续运行。一般情况下,不会出什么问题,但是如果后面进行了delete或者free,那就会出大问题——内存泄漏了。
一般来说,我们只能通过RAII来解决这一问题。但是RAII是什么?别急,在智能指针中马上就会有讲解。
