Hello,大家好,今天这篇博客是我们关于C++语法部分的倒数第二篇博客了,我们大家在公司中完成一部分代码后,就会产生一个问题,就是这个代码的正确性,今天我们就针对这个检测来看一看,这个异常的分析。
目录
1 异常的概念及使用
1.1 异常的概念
1.2 异常的抛出和捕获
1.3 栈展开
1.4 查找匹配的处理代码
1.5 异常重新抛出
1.6 异常安全问题
1.7 异常规范
2 标准库的异常
1 异常的概念及使用
1.1 异常的概念
1>.异常处理机制允许程序中独立开发部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给出现的另一部分,检测环节无须知道问题的处理模块的所有细节。
2>.C语言主要是通过错误码的形式处理错误,错误码本质上就是对错误信息进行分类编号,拿到错误码以后还需要我们自己去查询错误信息,比较麻烦。异常时会抛出一个对象,这个对象可以涵盖更全面的各种信息。
1.2 异常的抛出和捕获
1>.程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了改由哪个catch的处理代码来处理异常。
2>.被选中的处理代码是调用链中与该类型匹配且离抛出异常的位置最近的那一个catch的处理代码。根据抛出对象的类型与内容,程序的抛出异常部分要告知异常处理部分到底发生了什么错误。
3>.当throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置会跳到与之匹配的catch模块,catch可能是同一个函数中的一个局部catch模块,也可能是调用链中的另一个函数中的catch模块,控制权从throw位置转移到了catch模块的位置。这里还有两个重要的含义:1.沿着调用链的函数可能会提早推出;2.一旦程序开始执行异常处理程序,沿着调用链创建的对象都将会自动被编译器销毁。
4>.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch模块结束后就被销毁了。(这里的处理类似于函数的传值返回)
5>.在C++的异常处理过程中,我们常常选择使用try-catch去处理异常,我们这里就先来讲解一下这个try-catch:1.try:表示将有可能出现异常的代码书写在try代码块中;2.catch:try不能单独使用,必须结合catch / finally / catch-finally(这里try结合catch),catch也不能单独使用,必须结合try一起用。
int Divide(int a, int b)
{try{if (b == 0)//如果b等于0,就抛异常。{string s("Divide by zero condition!");throw s;//这里会将类型为string的对象s抛出去,去找这条调用链中与s这个对象类型匹配且离抛出异常的哪个位置的那一个catch代码块(抛出的并不是s对象,而是s这个异常对象的一个拷贝对象)。}else{return a / b;}}catch (int errid)//catch这个代码块接收的是int类型的一个对象。{cout << errid << endl;}
}
void Func(int a, int b)
{try{cout << Divide(a, b) << endl;}catch (const char* errmsg){cout << errmsg << endl;}
}
int main()
{int a = 0, b = 0;cin >> a >> b;try{Func(a, b);}catch (const string* errmsg)//catch接收的是一个string类型的对象。{cout << errmsg << endl;}return 0;
}//我们开始运行程序,输入两个变量分别为10和0,在main函数中进入try代码块中,去调用Func这个函数,进入Func这个局部栈帧中,又进入到try代码中,再去调用Divide函数,首先,又去进入到try代码块中,由于b==0,会进入到if语句中去执行代码,通过throw来将s对象跑出来引发异常,编译器这里会顺着调用链去找接收string类型对象的catch模块,首先找到第15这行代码的catch模块(因为离throw的位置最近),类型不符合,再顺着调用链去找,找到第26这行代码的catch模块,类型又不符合,再找到第39这行代码的catch模块,OK了,类型符合,就是errmsg这个对象接收到了Divide函数中跑出来的哪个对象s,既然是第39这行代码的catch模块接收了,那么程序的执行就从抛出的位置跳到了第39这行代码的catch模块这里来了。
//当我们在Divide函数中抛出s对象时,那么第7这句代码之后的语句将不会再被执行(仅限于Divide这个栈帧中),而且Func函数在这里其实是提早退出了的,Func这个函数中,如果在调用了这个函数之前多余开辟了空间的话,那么编译器在这里会自动地将Func函数中开辟的那块空间给销毁掉。
上述函数的调用链:
1.3 栈展开
1>.抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try模块的内部,如果在的话则查找匹配的那个catch模块,如果有匹配的,则跳到那个与之匹配的catch模块的那个地方去进行处理。
2>.如果当前所在的这个函数中没有try/catch,或者有try/catch子句但是类型不匹配,则退出当前函数,进行在外层调用函数链中去查找,上述查找的catch模块的过程被称之为是栈展开。
3>.如果我们到达main函数的栈帧,并且依旧没有找到与之匹配的catch模块,那么程序在这里会自动去调用标准库中的terminate这个函数去终止程序,简单来说就是报错。
4>.如果找到匹配的catch模块去处理后,catch模块中的以及后续的代码则会进行执行。
上图就是一个栈展开的过程。
1.4 查找匹配的处理代码
1>.一般情况下抛储对象和catch接收的那个对象的类型是完全匹配的,如果有多个类型匹配的catch子句,那么就选择离他位置更近的那个catch子句。
2>.但是也有一些例外,允许从非常量向常量的类型准换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这一点非常实用,实际中继承体系基本都是用这个方式去设计的。
3>.如果到main函数中,异常人就没有被匹配的话就会被终止程序,不是发生严重错误的情况下,我们是不期望程序最终的,所以一般的main函数中在最后都会使用catch(...),它可以捕获任意类型的异常,但是我们是不知道异常的错误是什么。注:一个try模块我们可以搭配多个catch模块。
//由于时间等等各种原因,我们这里就不一一为大家展示匹配的过程代码了,我们接下来就来模拟设计一个继承的匹配机制。
class person
{
public:person(const string& name):_name(name){}
protected:string _name;
};
class student :public person
{
public:student(const string& name, int id):person(name), _id(id){}
private:int _id;
};
class teacher :public person
{
public:teacher(const string& name, int teach):person(name), _teach(teach){}
private:int _teach;
};
void Print()
{if (rand() % 5 == 0){throw student("学号", 20);}else if (rand() % 2 == 0){throw teacher("工号", 32);}else{throw string();}
}
int main()
{try{Print();}catch (const person& p){ }//可以捕捉所有继承了person类型的对象。catch (...)//可以捕捉任意类型的异常对象。{ }return 0;
}//好了,我们这里直接来看Print函数中抛异常的操作,首先看第36到39这段代码,它抛出的student类型的对象,在第55到56这段代码中的catch子句被捕获了,派生类的对象被基类类型的对象给捕获了;再来看第40到43这段代码,它抛出的是一个teacher类型的对象,在第55到56这段代码中的catch子句被捕获了,teacher这个派生类对象被person这个基类对象给捕获了;最后看第44到47这段代码,它所抛出的是一个string类型的对象,是被第57到58这段代码中的catch子句捕获的,第55到56这段代码中的catch子句它主要捕获的是person类型的对象以及继承了person类的派生类对象,string类型与其不匹配,第55到56这段代码中的catch子句捕获不到,而第57到58这段代码中的catch子句可以捕捉到任意类型的异常对象,因此就被第57到58这段代码中的catch子句给捕捉到了。
1.5 异常重新抛出
1>.有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常需要重新抛出,直接throw;就可以把捕捉到的对象再次抛出。
void Print()
{int a = rand() % 2;try{throw string();}catch (string& s){if (a == 1){throw;//如果a==1的话,就将捕获到的那个string类型的对象再次抛出。}else{cout << s << endl;}}
}
int main()
{try{Print();}catch (string& s)//Print函数将捕捉到的那个对象重新抛出后,被这个catch子句重新捕捉到了。{cout << s << endl;}return 0;
}
1.6 异常安全问题
1>.异常抛出后,后面的代码就不再执行了,前面申请了资源(内存、锁等),后面要进行释放(这里指的是我们自己用new/malloc向内存申请的一块资源,它在释放时需要我们自己去调用delete函数),但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄露,会产生安全性的问题。为了解决这个问题,那么我们就要在抛出到外层调用链之前要提前捕获到这个异常对象,将那些资源释放之后再将其重新抛出。当然我们下一章要讲解的智能指针章节中所讲的RALL方式解决这种问题时更好的。
2>.其次在析构函数中,如果在析构函数的过程中抛出了异常的话,那么就也需要慎重处理(在C类语言中,只要是开创资源的函数,如new、malloc或释放资源的函数,如free、delete,这几个函数都有可能会抛异常),比如析构函数要释放10个资源,在释放到第5个时抛出异常,则也需要捕获处理,否则的话后面的5个资源就没有释放,也会造成资源泄露。
void Print()
{int* array = new int[10] {0};//创建一个int类型的数组空间,数组的对象为10。try{string s;throw s;//抛出一个string类型的对象。}catch (...)//我们在抛出异常对象之前就申请了一块有10个int类型空间大小的资源,为了防止出现资源泄露的问题,异常,我们需要Print函数内部就捕获到了这个异常对象,等将array执行的那块资源说服力之后,再将捕获到的那个异常对象重新抛出即可。{delete[] array;throw;//将捕获的那个对象重新抛出。}delete[] array;//如果这里并不会抛异常的话,编译器不会走catch子句,异常这里还需再写上一句删除array指向的那块资源的代码。
}
1.7 异常规范
1>.对于用户和编译器而言,预先知道某个程序会不会抛出异常大有益处,知道某个函数是否会抛出异常会有助于简化调用函数的代码。
2>.C++98中函数参数列表的后面接throw(),表示该函数不会抛异常,函数参数列表的后面接throw(类型1,类型2,...)表示可能会抛出多种类型的异常,将可能会抛出的类型之间均用逗号分割。
3>.C++98的这种方式有点过于复杂,在实践中其实并不好用,C++11中对其进行了简化,函数参数列表后面若加noexcept这个关键字就表示该函数不会抛异常,若啥都不加的话则表示可能会抛出异常。
4>.编译器并不会在编译时去检查noexcept修饰了,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利通过的(有些编译器可能会报个警告)。但是如果一个声明了noexcept的函数抛出了异常的话,程序便会去调用terminate终止程序。
5>.noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会抛出异常的话则返回false,不会的话就会返回true。
void Print()noexcept
{int a = 0;cin >> a;if (a == 10){throw "a==10";}
}
int main()
{try{Print();}catch (char* errmsg){cout << errmsg << endl;}return 0;
}//如果我们大家仔细看上述这段代码时,稍微有一点问题,Print函数有抛出异常的风险,但是Print函数的参数后面加了noexcept这个关键字,理论上来说的话是不能加这个关键字的,通过前面的解析,我们可知,这种情况下有的编译器是不会报错的。我们现在来运行这个代码来看一下,如果我们输入5的话,编译器确实不会报错,而且还完整地运行了下来,但如果我们输入10的话,程序在这里就别破中止运行了,原因是因为Print这个用noexcept修饰的函数在运行时抛出了一个异常对象。
2 标准库的异常
1>.C++标准库也定义了一套自己的异常继承体系,基类是exception;所以我们日常在写程序时,需要在主函数捕获exception即可,要获取异常信息,调用what函数,what函数是一个虚函数,派生类可以重写。
OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持!