1. 写在前面
c++在线编译工具,可快速进行实验: https://www.bejson.com/runcode/cpp920/
这段时间打算重新把c++捡起来, 实习给我的一个体会就是算法工程师是去解决实际问题的,所以呢,不能被算法或者工程局限住,应时刻提高解决问题的能力,在这个过程中,我发现cpp很重要, 正好这段时间也在接触些c++开发相关的任务,所有想借这个机会把c++重新学习一遍。 在推荐领域, 目前我接触到的算法模型方面主要是基于Python, 而线上的服务全是c++(算法侧, 业务那边基本上用go),我们所谓的模型,也一般是训练好部署上线然后提供接口而已。所以现在也终于知道,为啥只单纯熟悉Python不太行了, cpp,才是yyds。
和python一样, 这个系列是重温,依然不会整理太基础性的东西,更像是查缺补漏, 不过,c++对我来说, 已经5年没有用过了, 这个缺很大, 也差不多相当重学了, 所以接下来的时间, 重温一遍啦 😉
资料参考主要是C语言中文网和光城哥写的C++教程,然后再加自己的理解和编程实验作为辅助,加深印象。 关于更多的细节,还是建议看这两个教程。
今天这篇文章比较简单, 主要是整理C++的异常处理,所谓异常,就是程序运行时可能会发生一些错误,比如除数为0, 数组下标越界等 ,如果不事先处理,可能导致程序崩溃,无法运行, 在写程序中,其实这些异常情况也需要尽可能的想到,然后用C++的异常处理机制捕获处理这些错误。 C++的异常处理机制主要包括try, catch, throw
三个关键字, 下面就来看看啦。
主要内容:
- C++异常处理初识
- C++异常类型以及多级catch匹配
- C++throw关键字
- C++的exception类: C++标准异常的基类
Ok, let’s go!
2. C++异常处理初识
程序错误大致上分为三种类型,语法错误,逻辑错误和运行时错误:
- 语法错误: 编译和链接阶段可以发现,只有100%符合语法规则的代码才能生成可执行程序。语法错误是最容易发现,最容易排除的错误
- 逻辑错误: 编写代码思路有问题,无法达到预期目标, 可通过调试解决
- 运行错误: 运行期间的错误,比如除数为0,内存分配失败,数组越界,文件不存在等,C++异常机制就是解决这哥们而引入的
运行时,如果放任不管,系统就会执行默认操作,终止运行。 C++的异常机制,能捕获运行时错误,给程序一次"起死回生"的机会
从一个例子开始:
int main(){string str = "http://c.biancheng.net";char ch1 = str[100]; //下标越界,ch1为垃圾值cout<<ch1<<endl;char ch2 = str.at(100); //下标越界,抛出异常cout<<ch2<<endl;return 0;
}
运行代码,在控制台输出 ch1 的值后程序崩溃(我实验发现,也不崩溃呀,可能编译器不同?)。 但至少ch1那里确实存在了越界。
at()
是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与[ ]
不同,at()
会检查下标是否越界,如果越界就抛出一个异常;而[ ]
不做检查,不管下标是多少都会照常访问。
ch1那里不会检查下标越界,虽然逻辑错误,但程序会正常运行。 而ch2那里, 由于at函数会检查越界问题,所以会抛出异常来, 如果我们不处理这个异常,程序就终止了。 这样的好处就是我们能立刻认识到我们的一些错误。
那么,如何捕获异常,避免程序崩溃呢? 语法如下:
try{// 可能抛出异常的语句char ch2 = str.at(100)
}catch(exceptionType variable){// 处理异常的语句cout << "下标越界啦" << endl;return;
}
try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。
修改代码:
int main(){string str = "http://c.biancheng.net";try{char ch1 = str[100];cout<<ch1<<endl;}catch(exception e){cout<<"[1]out of bound!"<<endl;}try{char ch2 = str.at(100);cout<<ch2<<endl;}catch(exception &e){ //exception类位于<exception>头文件中cout<<"[2]out of bound!"<<endl;}
}
此时只会输出[2]这个数组越界, 第一个try依然没有捕获到异常,这是因为, []不会检查下标越界,不会抛出异常,即使有错误,try也检测不到。所以发生异常的时候,必须明确将其抛出,try才能检测到,如果不抛出,即使异常try检测不到。所谓抛出异常,就是告诉程序发生了什么错误。
第二个try检测到了异常,交给catch处理,但注意,一旦异常抛出,try检测到,就不会在执行异常点后面的语句了,直接跳到catch里面执行。
所以,先明确异常的处理流程: 抛出(Throw) --> 检测(Try) --> 捕获(Catch)
看一个终极例子:
void func_inner(){throw "Unknown Exception"; //抛出异常cout<<"[1]This statement will not be executed."<<endl;
}
void func_outer(){func_inner();cout<<"[2]This statement will not be executed."<<endl;
}
int main(){try{func_outer();cout<<"[3]This statement will not be executed."<<endl;}catch(const char* &e){cout<<e<<endl; // Unknown Exception}return 0;
}
异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。
发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过,没有执行的机会了。
3. C++异常类型及多级catch匹配
3.1 异常类型
try-catch语法里面,catch捕获异常的时候:
try{// 可能抛出异常的语句
}catch(exceptionType variable){// 处理异常的语句
}
这里有个exceptionType是异常类型,指明了当前的catch可以处理什么类型的异常; variable是一个变量,用来接收异常信息。 当程序抛出异常的时候,会创建一份数据,这份数据包含了错误信息,我们可以根据这些信息判断到底出了什么问题,怎么处理。
异常既然是一份数据,就应该有数据类型。 C++规定,异常类型可以是int,char,float, bool等基本类型,也可以是指针,数组,字符串,结构体,类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象
exceptionType variable
和函数的形参非常类似,异常发生后,会将异常数据传递给variable这个变量, 和函数传参很类似。 只有跟exceptionType
类型匹配的异常数据才会被传递给variable,否则catch不会接收这份异常数据,也不会执行catch块中的语句。
可以将catch看做一个没有返回值的函数,当异常发生后catch会被调用,并且会接收实参, 但是catch和真正的函数调用又有区别:
- 真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则编译阶段就报错
- catch,异常是在运行阶段产生,可以是任何类型,所以没法提前预测, 不能在编译阶段判断类型是否正确,只有等程序运行后,真抛出异常了,再将异常类型和catch能处理的类型进行匹配,如果匹配成功,就调用当前catch,否则,忽略当前catch。
- catch和真正的函数调用相比,多了一个运行阶段实参和形参匹配的过程。
如果不希望catch处理异常数据,也可以把variable省略:
try{// 可能抛出异常的语句
}catch(exceptionType){// 处理异常的语句
}
3.2 多级catch
一个try后面可以跟多个catch:
try{//可能抛出异常的语句
}catch (exception_type_1 e){//处理异常的语句
}catch (exception_type_2 e){//处理异常的语句
}
//其他的catch
catch (exception_type_n e){//处理异常的语句
}
当异常发生时,程序按照从上到下的顺序, 将异常类型和catch所能接收的类型逐个匹配。一旦找到类型匹配的catch就停止检索,并将异常交给当前的catch处理。如果最终也没有找到匹配的catch,就终止程序运行。
class Base{ };
class Derived: public Base{ };
int main(){try{throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象cout<<"This statement will not be executed."<<endl;}catch(int){cout<<"Exception type: int"<<endl;}catch(char *){cout<<"Exception type: cahr *"<<endl;}catch(Base){ //匹配成功(向上转型)cout<<"Exception type: Base"<<endl;}catch(Derived){cout<<"Exception type: Derived"<<endl;}return 0;
}
在catch中,只给出了异常类型,没有给出接收异常信息的变量。这个最终输出, Exception type:Base
。 异常类型是Derived的, 但catch在匹配类型时发生了向上转型,让catch(Base)
捕获了。
3.3 catch匹配过程中的类型转换
C/C++存在很多种类型转换,普通函数的话,如果实参和形参的类型不是严格匹配,会将实参的类型进行适当转换,以适应形参类型,主要包括:
- 算数转换:
int->float, char->int, double->int
- 向上转型: 派生类向基类转换
- 数组或函数指针转换: 如果函数形参不是引用类型,那么数组名会转为数组指针,函数名也会转为函数指针
- 用户自定义转换等
catch匹配异常过程中,也会进行类型转换,但仅能向上转型,const转换,数组或函数指针转换,其他都不能用于catch。 like this:
int main(){int nums[] = {1, 2, 3};try{throw nums;cout<<"This statement will not be executed."<<endl;}catch(const int *){cout<<"Exception type: const int *"<<endl;}return 0;
}
nums本来是int[3]
, 但catch中没有严格匹配类型,所以先转成int*
, 再转成const int *
。
4. C++的throw关键字
C++异常处理的流程: 抛出(Throw) -> 检测(Try) -> 捕获(Catch)
, 异常必须显式的抛出,才能被检测和捕获。
C++中, 使用throw关键字显式抛出异常,用法:
throw exceptionData;
下面通过一个动态数组的典型异常应用场景来深入了解下,详细细节参考第一个链接文档:
// 自定义的异常类型
class OutOfRange{
public:OutOfRange(): m_flag(1){};OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){}void what() const; // 获取具体的错误信息
private:int m_flag; // 不同flag表示不同的错误int m_index; // 当前数组的长度int m_len; // 当前使用的数组下标
};
void OutOfRange::what() const{if (m_flag == 1){cout << "Error: empty array, no elements to pop. " << endl;}else if(m_flag == 2){cout << "Error: out of range( array length " << m_len << ", access index " << m_index << " )" << endl;}else{cout << "Unkonwn exception. " << endl;}
}// 实现动态数组
class Array{
public:Array();~Array(){free(m_p);}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; // 内存指针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); // index不合法,抛出异常2 }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){cout << "Empty array! No elements to print. " << endl;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); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9// 尝试访问第20个元素try{cout <<nums[20] << endl;}catch(OutOfRange &e){e.what(); // Error: out of range( array length 10, access index 20 )}// 尝试弹出20个元素try{for (int i=0; i<20; i++){nums.pop();}}catch(OutOfRange &e){e.what(); // Error: empty array, no elements to pop. }printArray(nums); // Empty array! No elements to print. return 0;
}
另外, throw
关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification)
double func (char param) throw (int);
这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char 类型的参数,并且只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,只能终止程序。
但由于C++11不再建议使用了,所以这里就不整理了,关于异常规范的具体细节,参考第一篇链接。
5. C++的exception类
C++语言本身或标准库抛出的异常都是exception子类,称为标准异常,可以通过下面语句捕获:
try{// 可能抛出异常的句子
}catch(exception &e){ // 引用是为了提高效率,如果不使用引用,就要经历一次对象拷贝// 处理异常的语句
}
exception类位于头文件
class exception{
public:exception () throw(); //构造函数exception (const exception&) throw(); //拷贝构造函数exception& operator= (const exception&) throw(); //运算符重载virtual ~exception() throw(); //虚析构函数virtual const char* what() const throw(); //虚函数
}
what()
函数返回一个能识别异常的字符串,正如它的名字“what”一样,可以粗略地告诉你这是什么异常。
关于具体的exception继承体系,参考这里叭, 等具体用到再进行补充这块。