程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
- 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
- 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
- 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。异常(Exception)机制就是为解决运行时错误而引入的。
#include <iostream>
#include <string>
using namespace std;
int main(){
string str = "https://www.baidu.com";
char ch1 = str[100]; //下标越界,ch1为垃圾值
cout<<ch1<<endl;
char ch2 = str.at(100); //下标越界,抛出异常
cout<<ch2<<endl;
return 0;
}
18.1 捕获异常
借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:
try{
// 可能抛出异常的语句
}catch(exceptionType variable){
// 处理异常的语句
}
try和catch都是 C++ 中的关键字,后跟语句块,不能省略{ }。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
int main(){
string str = "https://www.baidu.com/";
try{
char ch1 = str[100];
cout<<ch1<<endl;
}catch(exception e){
cout<<"越界1"<<endl;
}
try{
char ch2 = str.at(100);
cout<<ch2<<endl;
}catch(exception &e){ //exception类位于<exception>头文件中
cout<<"越界2"<<endl;
}
return 0;
}
image.png
第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。
异常的处理流程:
抛出(Throw)--> 检测(Try) --> 捕获(Catch)
18.2 多级catch匹配
catch 和真正的函数调用又有区别:
1、真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
2、而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。
总起来说,catch 和真正的函数调用相比,多了一个「在运行阶段将实参和形参匹配」的过程。
try{
//可能抛出异常的语句
}catch (exception_type_1 e){
//处理异常的语句
}catch (exception_type_2 e){
//处理异常的语句
}
//其他的catch
catch (exception_type_n e){
//处理异常的语句
}
当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。
#include <iostream>
#include <string>
using namespace std;
class Base{ };
class Derived: public Base{ };
int main(){
try{
throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
}catch(int){
cout<<"int 异常"<<endl;
}catch(char *){
cout<<"char* 异常"<<endl;
}catch(Base){ //匹配成功(向上转型)
cout<<"基类异常"<<endl;
}catch(Derived){
cout<<"派生类异常"<<endl;
}
return 0;
}
image.png
18.2.1 catch 在匹配过程中的类型转换
C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:
1、 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
2、 向上转型:也就是派生类向基类的转换。
3、 const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
4、数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
5、用户自定的类型转换。
catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。
const 转换以及数组和指针的转换例程:
#include <iostream>
using namespace std;
int main(){
int nums[] = {1, 2, 3};
try{
throw nums;
}catch(const int *){
cout<<"const int * 异常"<<endl;
}
return 0;
}
image.png
nums 本来的类型是int [3],但是 catch 中没有严格匹配的类型,所以先转换为int *,再转换为const int *。
18.3 throw(抛出异常)
throw 关键字来显式地抛出异常,它的用法为:
throw 异常数据;
异常数据,可以包含任意的信息,由程序员决定,可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。
char str[] = "https://www.baidu.com/";
char *pstr = str;
class Base{};
Base obj;
throw 100; //int 类型
throw str; //数组类型
throw pstr; //指针类型
throw obj; //对象类型
动态数组异常例程
#include <iostream>
#include <cstdlib>
using namespace std;
//自定义的异常类型
class OutOfRange{
public:
OutOfRange(): m_flag(1){ }
OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ }
public:
void what() const; //获取具体的错误信息
private:
int m_flag; //不同的flag表示不同的错误
int m_len; //当前数组的长度
int m_index; //当前使用的数组下标
};
void OutOfRange::what() const {
if(m_flag == 1){
cout<<"空数组,元素无法获取"<<endl;
}else if(m_flag == 2){
cout<<"数组越界当前长度是:"<<m_len<<", 使用了 "<<m_index<<" 下标位置"<<endl;
}else{
cout<<"未知异常"<<endl;
}
}
//实现动态数组
class Array{
public:
Array();
~Array(){ free(m_p); }
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){
cout<<"空数组无元素,无法打印"<<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);
//尝试访问第20个元素
try{
cout<<nums[20]<<endl;
}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;
}
image.png
18.4 throw 用作异常规范
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范,也称为异常指示符或异常列表。
double func (char param) throw (int);
这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char 类型的参数,并且只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,只能终止程序。
如果函数会抛出多种类型的异常,那么可以用逗号隔开:
double func (char param) throw (int, char, exception);
如果函数不会抛出任何异常,那么( )中什么也不写:
double func (char param) throw ();
如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到。
18.4.1 虚函数中的异常规范
派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。
class Base{
public:
virtual int fun1(int) throw();
virtual int fun2(int) throw(int);
virtual string fun3() throw(int, string);
};
class Derived:public Base{
public:
int fun1(int) throw(int); //错!异常规范不如 throw() 严格
int fun2(int) throw(int); //对!有相同的异常规范
string fun3() throw(string); //对!异常规范比 throw(int,string) 更严格
}
18.4.2 异常规范与函数定义和函数声明
异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
//错!定义中有异常规范,声明中没有
void func1();
void func1() throw(int) { }
//错!定义和声明中的异常规范不一致
void func2() throw(int);
void func2() throw(int, bool) { }
//对!定义和声明中的异常规范严格一致
void func3() throw(float, char*);
void func3() throw(float, char*) { }
注意:异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。我们只是了解这个知识就可以了。
18.5 C++异常基类exception
exception 类位于 <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() 函数返回一个能识别异常的字符串,C++标准并没有规定这个字符串的格式,各个编译器的实现也不同,所以 what() 的返回值仅供参考。
exception 类的继承层次 exception 类的直接派生类 logic_error 的派生类 runtime_error 的派生类
网友评论