C++从入门到放弃之:C++异常机制(exception)

C++异常机制

软件开发中的常见错误

  • 语法错误(编译器编译阶段就会检查出来)
  • 逻辑错误(程序代码编译没有问题,但是运行结果和想要的结果不符合,通过打印调试信息来查找错误)(GDB)
  • 设计缺陷(软件的整体框架不严谨,后期维护升级麻烦)
  • 需求不符(对需求的理解出现偏差)
  • 功能错误(定能和需求不匹配)
  • 环境异常(设计的产品需要的运行环境和实际运行的环境不匹配)
  • 操作不当(用户的操作不当导致程序的运行异常)

传统C语言的错误处理机制

  • 通过返回值表示错误
    优点函数的调用路径中的栈对象可以得到正确的析构,内存管理安全
    缺点
    缺点函数逐层返回,逐层判断,错误处理流程麻烦,代码臃肿
  • 通过跳转表示处理错误
    优点不需要逐层判断,逐层返回,错误处理流程简单,一步到位错误处理.代码精炼
    缺点函数调用路径中的栈对象失去被析构的机会,有内存泄漏的风险

setjmp()函数与longjmp()函数

#include csetjmp
void _Cdecl longjmp(jmp_buf jmpb, int retval);
int _Cdecl setjmp(jmp_buf jmpb);

C标准库提供两个特殊的函数:setjmp()longjmp(),这两个函数是结构化异常的基础,正是利用这两个函数的特性来实现异常。

  • 异常的处理过程:
    需要一个全局的环境变量,记录跳转时出现的问题
    jmp_buf g_env;
    设置一个跳转点(setjmp(g_env) 函数可以实现这一功能),然后在其后的代码中任意地方调用 longjmp() 跳转回这个跳转点上,以此来实现当发生异常时,转到处理异常的程序上

setjmp() 为跳转返回保存现场并为异常提供处理程序,longjmp() 则进行跳转(抛出异常),setjmp()longjmp() 可以在函数间进行跳转,这就像一个全局的 goto 语句,可以跨函数跳转。
举个例子,程序在 main() 函数内使用 setjmp() 设置跳转,并调用另一函数A,函数A内调用B,B抛出异常(调用longjmp() 函数),则程序直接跳回到 main() 函数内使用 setjmp() 的地方返回,并且返回一个值。

#include <iostream>
#include <csetjmp>

using namespace std;
jmp_buf g_env;

void func(){
    FILE *fp = fopen("demo.txt","r");
    if (!fp){
        longjmp(g_env,-1);
    }
}
int main() {
   if(setjmp(g_env) == -1){
       cout<<"open file fail"<<endl;
       return -1;
   }
   func()
    return 0;
}

jmp_buf 异常结构

使用 setjmp() 及 longjmp() 函数前,需要先认识一下 jmp_buf 异常结构。jmp_buf 将使用在 setjmp() 函数中,用于保存当前程序现场(保存当前需要用到的寄存器的值),jmp_buf 结构在 setjmp.h 文件内声明:

typedef struct

{

 unsigned j_sp;  // 堆栈指针寄存器

 unsigned j_ss;  // 堆栈段

 unsigned j_flag;  // 标志寄存器

 unsigned j_cs;  // 代码段

 unsigned j_ip;  // 指令指针寄存器

 unsigned j_bp; // 基址指针

 unsigned j_di;  // 目的指针

 unsigned j_es; // 附加段

 unsigned j_si;  // 源变址

 unsigned j_ds; // 数据段

} jmp_buf;

jmp_buf 结构存放了程序当前寄存器的值,以确保使用 longjmp() 后可以跳回到该执行点上继续执行。

setjmp() 与 longjmp() 函数都使用了 jmp_buf 结构作为形参,它们的调用关系是这样的:

首先调用 setjmp() 函数来初始化 jmp_buf 结构变量 jmpb,将当前CPU中的大部分影响到程序执行的寄存器的值存入 jmpb,为 longjmp() 函数提供跳转,setjmp() 函数是一个有趣的函数,它能返回两次,它应该是所有库函数中唯一一个能返回两次的函数,第一次是初始化时,返回零,第二次遇到 longjmp() 函数调用后,longjmp() 函数使 setjmp() 函数发生第二次返回,返回值由 longjmp() 的第二个参数给出(整型,这时不应该再返回零)。

在使用 setjmp() 初始化 jmpb 后,可以其后的程序中任意地方使用 longjmp() 函数跳转会 setjmp() 函数的位置,longjmp() 的第一个参数便是 setjmp() 初始化的 jmpb,若想跳转回刚才设置的 setjmp() 处,则 longjmp() 函数的第一个参数是 setjmp() 所初始化的 jmpb 这个异常,这也说明一件事,即 jmpb 这个异常,一般需要定义为全局变量,否则,若是局部变量,当跨函数调用时就几乎无法使用(除非每次遇到函数调用都将 jmpb 以参数传递,然而明显地,是不值得这样做的);longjmp() 函数的第二个参数是传给 setjmp() 的第二次返回值

C++异常机制语法

抛出(Throw)–> 检测(Try) --> 捕获(Catch)

异常抛出

throw 异常对象;
注:异常对象可以是基本类型变量,也可以是类类型的对象
抛出对象时可以直接抛出匿名对象,但是必须要加小括号

异常的检测和捕获

try{
		可能引发异常的语句;
	}
	catch(异常类型1){
		针对异常类型1处理;
	}
	catch(异常类型2){
		针对异常类型2处理;
	}
	....

注意:catch子句根据throw抛出的异常对象类型进行匹配时不是最优匹配,而是自上向下顺序匹配,因此对子类类型的异常捕获语句要写在对基类异常捕获语句的前面,否则会被对基类异常捕获语句提前截获

#include <iostream>
#include <csetjmp>

using namespace std;

void func(){
    FILE *fp = fopen("demo.txt","r");
    if (!fp){
        throw -1;
    } else{
        
    }
}
int main() {
    try {
        func();
        
    }
    catch (int ex) {
        if (ex==-1){
            cout<<"open file fail"<<endl;
            return -1;
        }

    }
   
       cout<<"open file fail"<<endl;
       return -1;
   }
   func()
    return 0;
}

catch 在匹配过程中的类型转换

C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:

  • 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
  • 向上转型:也就是派生类向基类的转换,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
  • const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
  • 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
  • 用户自定的类型转换。

catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。

#include <iostream>
using namespace std;
int main(){
    int nums[] = {1, 2, 3};
    try{
        throw nums;
        cout<<"This statement will not be executed."<<endl;
    }catch(const int *){
    //nums 本来的类型是int [3],但是 catch 中没有严格匹配的类型,所以先转换为int *,再转换为const int *。
        cout<<"Exception type: const int *"<<endl;
    }
    return 0;
}
Exception type: const int *

C++抛出异常时,抛出自定义类型的对象

  • 优点
    *在发生异常时,抛出自定义类型的对象,可以提高代码可读性,传出更多参数,对象的构造函数里进行错误处理
    **
#include <iostream>
#include <csetjmp>

using namespace std;

class FileError {
public:
    FileError(const string &file, int line) : m_file(file), m_line(line) {
        cout <<"file in \"" <<m_file <<"\" "<< m_line << "line"<< endl;
    }

private:
    string m_file;
    int m_line;
};

void func() {
    FILE *fp = fopen("demo.txt", "r");
    if (!fp) {
        throw FileError(__FILE__, __LINE__);
        //throw -1;
    } else {

    }
}

int main() {
    try {
        func();
    }
    catch (int ex) {
        if (ex == -1) {
            cout << "open file fail" << endl;
            return -1;
        }

    }
    catch (FileError &ex) {
        cout << "open file fail" << endl;
        return -1;
    }

    func();
    return 0;
}
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
file in "/home/panda/WorkSpace/CPP/main.cpp" 20line
open file fail

进程已结束,退出代码 255

使用继承语法,完善错误类

  • 概念
    使用继承的理念来处理一些错误中的共性问题,子类处理相对应的详细的信息
  • 常见问题
    在抛出异常时,catch块的匹配原则是自上而下的匹配,如果,抛出的是类类型的对象,一个子类对象,那么,在catch块中如果基类的捕获在子类类型的捕获之前,那么子类类型可以通过向上造型和基类类型的捕获相匹配,出现截获的问题,从而实现不了子类类型的异常捕获
  • 解决方法
    在对子类类型的异常捕获语句要写在最前面,否者会被基类类型的异常捕获语句提前捕获
  • 错误形式
#include <iostream>
#include <csetjmp>

using namespace std;

class ErrorBase {};
class ErrorDerived: public ErrorBase{};
void func() {
    throw ErrorDerived();
    //throw ErrorBase();
}

int main() {
    try {
        func();
    }
    catch (ErrorBase& ex) {//将会检测到异常的产生,子类类型的异常通过向上造型匹配到了基类类型,可以被截获
        cout << "Error base" << endl;
        return -1;
    }
    catch (ErrorDerived& ex) {
        cout << "Error derived" << endl;
        return -1;
    }

    func();
    return 0;
}
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
Error base

进程已结束,退出代码 255

  • 正确形式
#include <iostream>
#include <csetjmp>

using namespace std;

class ErrorBase {};
class ErrorDerived: public ErrorBase{};
void func() {
    throw ErrorDerived();
    throw ErrorBase();
}

int main() {
    try {
        func();
    }
    catch (ErrorDerived& ex) {
        cout << "Error derived" << endl;
        return -1;
    }
    catch (ErrorBase& ex) {
        cout << "Error base" << endl;
        return -1;
    }
    func();
    return 0;
}
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
Error derived

进程已结束,退出代码 255

函数的异常说明(C++11弃用)

  • 语法
    用于说明函数在执行期间可能抛出的异常类型
    返回类型 函数名(参数表) throw(异常类型表) {函数体}
    函数的异常说明是一种承诺,表示该函数所抛出的异常说明不会超出所承诺的范围;如果函数抛出了异常说明以外的其他类型,则无法被正常捕获.而会被系统捕获,导致进程终止
#include <iostream>
#include <csetjmp>

using namespace std;

class FileError {};
class MemoryError{};
void func()throw(FileError,MemoryError){
    throw FileError();
    throw MemoryError();
}

int main() {
    try {
        func();
    }
    catch (FileError& ex) {
        cout << "file Error " << endl;
        return -1;
    }
    catch (MemoryError& ex) {
        cout << "memory Error " << endl;
        return -1;
    }

    func();
    return 0;
}

/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
file Error 

进程已结束,退出代码 255

  • 函数说明的两种极端形式
    1>不写异常说明,表示可以抛出任何异常
    2>空异常说明,throw()表示不会抛出任何异常
  • 如果函数的声明和定义分开写要保证异常说明严格一致,但是顺序无所谓
  • 如果针对虚函数,在基类中的版本带有异常说明,那么该函数在子类中的覆盖版本不能比基类版本抛出更多的异常,否则将会因为"放松throw"限定而导致编译失败
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) 更严格
}
  • C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
//错!定义中有异常规范,声明中没有
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*) { }

标准异常类(exception)

class exception{//标准C++库定义好的类
public:
exception() throw() { } 
virtual ~exception() throw();

/*Returns a C-style character string describing the general cause of the current error.  */  
virtual const char* what() const throw();
};  

eg:
try{...}
catch(exception& ex){
	ex.what();
}

案例

#include <iostream>

using namespace std;

class fileError:public exception{
public:

    ~fileError()throw(){};
    const char *what()const throw(){
        cout<<"file error"<<endl;
        return "file error";
    }
private:

};
class memoryError:public exception{
public:

    ~memoryError()throw(){}
    const char * what() const throw(){
        cout<<"mem error\n";
        return "memory error";
    };
};
void func(){
    //throw fileError();
    throw memoryError();
}
int main()
{
    try {
        func();
    } catch (exception& ex) {//根据异常的实际类类型来调用重写的虚函数
        ex.what();
    }
    return 0;
}
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
mem error

进程已结束,退出代码 0

关于构造函数和析构函数中的异常

  • 构造函数可以抛出异常,但是如果构造函数抛出异常,那么该对象将会被不完整构造,对于这样的对象析构函数不会再被自动执行;因此在构造函数抛出异常之前需要手动销毁之前分配的动态资源
  • 析构函数最好不要抛出异常
#include <iostream>

using namespace std;

class A{
public:
    A(){
        cout<<"new\n";//动态分配内存
        if("error") {
            cout<<"delete"<<endl;//在抛出异常之前,要进行将动态分配的内存释放掉
            throw -1;
        }
        cout<<"A()\n";
    }
    ~A(){
        cout<<"~A()\n";//如果构造函数中抛出了异常,在异常的处理过程中不会调用析构函数
    }
};

int main()
{
    try {
        A a;
    } catch (int & i) {
        cout<<"异常处理"<<endl;//没有调用析构函数
    }
    return 0;
}//-->调用析构函数,无法被捕获到
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
new
delete
异常处理

进程已结束,退出代码 0

C++异常使用实例

C/C++ 规定,数组一旦定义后,它的长度就不能改变了;换句话说,数组容量不能动态地增大或者减小。这样的数组称为静态数组(Static array)。静态数组有时候会给编码代码不便,我们可以通过自定义的 Array 类来实现动态数组(Dynamic array)。所谓动态数组,是指数组容量能够在使用的过程中随时增大或减小。

#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<<"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<<"Unknown exception."<<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<<"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);

    //尝试访问第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;
}
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Error: out of range( array length 10, access index 20 )
Error: empty array, no elements to pop.
Empty array! No elements to print.

进程已结束,退出代码 0

Array 类实现了动态数组,它的主要思路是:在创建对象时预先分配出一定长度的内存(通过 malloc() 分配),内存不够用时就再扩展内存(通过 realloc() 重新分配)。Array 数组只能在尾部一个一个地插入(通过 push() 插入)或删除(通过 pop() 删除)元素。

我们通过重载过的[ ]运算符来访问数组元素,如果下标过小或过大,就会抛出异常(第53行代码);在抛出异常的同时,我们还记录了当前数组的长度和要访问的下标。

在使用 pop() 删除数组元素时,如果当前数组为空,也会抛出错误。

不推荐使用异常说明

异常规范的初衷是好的,它希望让程序员看到函数的定义或声明后,立马就知道该函数会抛出什么类型的异常,这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范,程序员必须阅读函数源码才能知道函数会抛出什么异常。

不过这有时候也不容易做到。例如,func_outer() 函数可能不会引发异常,但它调用了另外一个函数 func_inner(),这个函数可能会引发异常。再如,您编写的函数调用了老式的库函数,此时不会引发异常,但是库更新以后这个函数却引发了异常。总之,异常规范的初衷实现起来有点困难,所以大家达成的一致意见是,最好不要使用异常规范。

异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值