好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
1) 语法错误在编译和链接阶段就能发现
2) 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
3) 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。
异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
void func(){"Unknown Exception"; //抛出异常, 抛的类型要和接的类型一致
throw "[1]This statement will not be executed."<<endl;
cout<<
}
int main(){
try{
func();"[2]This statement will not be executed."<<endl;
cout<<
}catch(const char* &e){
cout<<e<<endl;
}0;
return }
try{// 可能抛出异常的语句
}catch(exceptionType variable){// 处理异常的语句
}
exceptionType是异常类型,它指明了当前的 catch 可以处理什么类型的异常;variable是一个变量,用来接收异常信息。
当程序抛出异常时,会创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来怎么处理。
exceptionType variable和函数的形参非常类似,当异常发生后,会将异常数据传递给 variable 这个变量,这和函数传参的过程类似。
总起来说,catch 和真正的函数调用相比,多了一个在运行阶段将实参和形参匹配的过程,如果不匹配就会忽略掉的。
C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。
另外需要注意的是,如果不希望 catch 处理异常数据,也可以将 variable 省略掉,也即写作:
try{// 可能抛出异常的语句
}catch(exceptionType){// 处理异常的语句
}
try{//可能抛出异常的语句
}catch (exception_type_1 e){//处理异常的语句
}catch (exception_type_2 e){//处理异常的语句
}//其他的catch
catch (exception_type_n e){//处理异常的语句
}
当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。
catch在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行
* 向上转型
* const 转换
* 数组或函数指针转换
异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。
语法:
throw exceptionData;
exceptionData 可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型
#include <iostream>
#include <cstdlib>
using namespace std;
//自定义的异常类型
class OutOfRange{
public:
m_flag(1){ };
OutOfRange(): int len, int index): m_len(len), m_index(index), m_flag(2){ }
OutOfRange(public:
void what() const; //获取具体的错误信息
private:
int m_flag; //不同的flag表示不同的错误
int m_len; //当前数组的长度
int m_index; //当前使用的数组下标
};
void OutOfRange::what() const {
if(m_flag == 1){
"Error: empty array, no elements to pop."<<endl;
cout<<else if(m_flag == 2){
}"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl;
cout<<else{
}"Unknown exception."<<endl;
cout<<
}
}
//实现动态数组
class Array{
public:
Array();m_p); };
~Array(){ free(public:
int operator[](int i) const; //获取数组元素
int push(int ele); //在末尾插入数组元素
int pop(); //在末尾删除数组元素
int length() const{ return m_len; }; //获取数组长度
private:
int m_len; //数组长度
int m_capacity; //当前的内存能容纳多少个元素
int *m_p; //内存指针
private:
static const int m_stepSize = 50; //每次扩容的步长
};
Array::Array(){m_p = (int*)malloc( sizeof(int) * m_stepSize );
m_capacity = m_stepSize;
m_len = 0;
}int Array::operator[](int index) const {
if( index<0 || index>=m_len ){ //判断是否越界
throw OutOfRange(m_len, index); //抛出异常(创建一个匿名对象)
}
return *(m_p + index);
}int Array::push(int ele){
if(m_len >= m_capacity){ //如果容量不足就扩容
m_capacity += m_stepSize;
m_p = (int*)realloc( m_p, sizeof(int) * m_capacity ); //扩容
}
m_p + m_len) = ele;
*(m_len++;
return m_len-1;
}int Array::pop(){
if(m_len == 0){
throw OutOfRange(); //抛出异常(创建一个匿名对象)
}
m_len--;
return *(m_p + m_len);
}
//打印数组元素
void printArray(Array &arr){
int len = arr.length();
//判断数组是否为空
if(len == 0){
"Empty array! No elements to print."<<endl;
cout<<return;
}
for(int i=0; i<len; i++){
if(i == len-1){
cout<<arr[i]<<endl;else{
}", ";
cout<<arr[i]<<
}
}
}
int main(){
Array nums;//向数组中添加十个元素
for(int i=0; i<10; i++){
nums.push(i);
}
printArray(nums);
//尝试访问第20个元素
try{
20]<<endl;
cout<<nums[catch(OutOfRange &e){
}
e.what();
}
//尝试弹出20个元素
try{
for(int i=0; i<20; i++){
nums.pop();
}catch(OutOfRange &e){
}
e.what();
}
printArray(nums);
return 0;
}
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),有些教程也称为异常指示符或异常列表。请看下面的例子:
double func (char param) throw (int, char, exception);
只能抛出异常规范中的异常,如果抛出其他类型的异常,try 将无法捕获,只能终止程序
1. 虚函数中的异常规范
C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。 不能说我父类只能抛int的,而子类int char float的什么都能抛,就没有起到规范的作用了。
2. 异常规范与函数定义和函数声明
C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致
异常规范的初衷是好的,它希望让程序员看到函数的定义或声明后,立马就知道该函数会抛出什么类型的异常,这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范,程序员必须阅读函数源码才能知道函数会抛出什么异常。
不过这有时候也不容易做到
例如,func_outer() 函数可能不会引发异常,但它调用了另外一个函数 func_inner(),这个函数可能会引发异常。
再如,您编写的函数调用了老式的库函数,此时不会引发异常,但是库更新以后这个函数却引发了异常。
总之,异常规范的初衷实现起来有点困难,所以大家达成的一致意见是,最好不要使用异常规范。
异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。
C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。
通过下面的语句来捕获所有的标准异常(有个兜底儿的异常):
try{//可能抛出异常的语句
}catch(exception &e){//处理异常的语句
}
之所以使用引用,是为了提高效率。如果不使用引用,就要经历一次对象拷贝(要调用拷贝构造函数)的过程。
exception 类的继承层次:
异常只是 C++ 为了处理错误而提出的一种解决方案,当然也不会是唯一的一种。
在 C++ 之前,处理异常的基本手段是错误码。函数执行后,需要检查返回值或者全局的 errno,看是否正常,如果出错了,就执行另外一段代码处理错误.
错误码缺点:
1. 这种做法很直观,但也有一个问题,那就是正常的业务逻辑代码与错误处理代码混在了一起,看起来很乱,你的思维要在两个本来不相关的流程里来回跳转。而且,有的时候,错误处理的逻辑要比正常业务逻辑复杂、麻烦得多,看了半天,你可能都会忘了它当初到底要干什么了,容易引起新的错误。
2. 错误码还有另一个更大的问题:它是可以被忽略的。也就是说,你完全可以不处理错误,“假装”程序运行正常,继续跑后面的代码,这就可能导致严重的安全隐患。
用异常的好处:
1. 异常的处理流程是完全独立的,throw 抛出异常后就可以不用管了,错误处理代码都集中在专门的 catch 块里。这样就彻底分离了业务逻辑与错误逻辑,看起来更清楚。
2. 异常是绝对不能被忽略的,必须被处理。如果你有意或者无意不写 catch 捕获异常,那么它会一直向上传播出去,直至找到一个能够处理的 catch 块。如果实在没有,那就会导致程序立即停止运行,明白地提示你发生了错误,而不会“坚持带病工作”。
3. 异常可以用在错误码无法使用的场合,这也算是 C++ 的“私人原因”。因为它比 C 语言多了构造 / 析构函数、操作符重载等新特性,有的函数根本就没有返回值,或者返回值无法表示错误,而全局的 errno 实在是“太不优雅”了,与 C++ 的理念不符,所以也必须使用异常来报告错误。
C++ 里对异常的定义非常宽松,任何类型都可以用 throw 抛出,也就是说,你可以直接把错误码(int)、或者错误消息(char*
、string
)抛出,catch 也能接住,然后处理。
但我建议你最好不要“图省事”,因为 C++ 已经为处理异常设计了一个配套的异常类型体系,定义在标准库的 <stdexcept>
头文件里。见上边:exception 类的继承层次
异常也与上一讲的智能指针密切相关,如果你决定使用异常,为了确保出现异常的时候资源会正确释放,就必须禁用裸指针,改成智能指针,用 RAII 来管理内存。
由于异常出现和处理的时机都不好确定,当前的 C++ 也没有在语言层面提出更好的机制,所以,你还要在编码阶段写好文档和注释,说清楚哪些函数、什么情况下会抛出什么样的异常,应如何处理,加上一些“软约束”。
小结:
1. 异常是针对错误码的缺陷而设计的,它不能被忽略,而且可以“穿透”调用栈,逐层传播到其他地方去处理;
2. 使用 try-catch 机制处理异常,能够分离正常流程与错误处理流程,让代码更清晰;
3. throw 可以抛出任何类型作为异常,但最好使用标准库里定义的 exception 类;
4. 完全用或不用异常处理错误都不可取,而是应该合理分析,适度使用,降低异常的成本;
5. 关键字 noexcept 标记函数不抛出异常,可以让编译器做更好的优化。