美文网首页
C语言接口与实现之异常处理try-except

C语言接口与实现之异常处理try-except

作者: wipping的技术小栈 | 来源:发表于2019-05-03 14:14 被阅读0次

    前言

    最近在学习《C语言接口与实现》,目前阅读到第四章,关于如何实现C语言异常捕获和断言处理,其中的异常捕获的栈和收尾处理有点不大明白,直到从网上查找到一篇文章才明白栈和结尾触发异常的作用是能够使得该机制可以处理多层异常。

    在这一章节中的代码例程稍显晦涩难懂,主要是因为该机制使用宏来实现,所以语法上并不友好,这就考验到各位的C语言水平了

    代码综述

    在面向对象的语言中,经常有异常处理机制的使用,那么C语言的异常处理机制按照常规分为 TRY EXCEPT ELSE FINALLY END_TRY 这5个部分,下面按照这5个部分来讲。

    这里先贴上全部代码,可见,该机制是使用setjmp来实现。这里不讲解setjmplongjmp的用法,请各位自行百度学习

    typedef struct _Except_t {  
        char *reason;
    } Except_t;
    
    typedef struct Except_Frame Except_Frame;
    struct Except_Frame {
        Except_Frame *prev;
        jmp_buf env;
        const char *file;
        int line;
        const Except_t *exception;
    };
    
    enum { Except_entered=0, Except_raised,
           Except_handled,   Except_finalized };
           
    extern Except_Frame *Except_stack;
    
    extern const Except_t Assert_Failed;
    
    Except_Frame *Except_stack = NULL;
    
    void Except_raise(const Except_t *e, const char *file,
        int line) {
        Except_Frame *p = Except_stack;
        assert(e);
        if (p == NULL) {
            fprintf(stderr, "Uncaught exception");
            if (e->reason)
                fprintf(stderr, " %s", e->reason);
            else
                fprintf(stderr, " at 0x%p", e);
            if (file && line > 0)
                fprintf(stderr, " raised at %s:%d\n", file, line);
            fprintf(stderr, "aborting...\n");
            fflush(stderr);
            abort();
        }
        p->exception = e;
        p->file = file;
        p->line = line;
        Except_stack = Except_stack->prev;
        longjmp(p->env, Except_raised);
    }
    
    void Except_raise(const Except_t *e, const char *file,int line);
    
    #define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)
    
    #define RERAISE Except_raise(Except_frame.exception, \
        Except_frame.file, Except_frame.line)
        
    #define RETURN switch (Except_stack = Except_stack->prev,0) default: return
    
    #define TRY do { \
        volatile int Except_flag; \
        Except_Frame Except_frame; \
        Except_frame.prev = Except_stack; \
        Except_stack = &Except_frame;  \
        Except_flag = setjmp(Except_frame.env); \
        if (Except_flag == Except_entered) {
        
    #define EXCEPT(e) \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } else if (Except_frame.exception == &(e)) { \
            Except_flag = Except_handled;
            
    #define ELSE \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } else { \
            Except_flag = Except_handled;
            
    #define FINALLY \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } { \
            if (Except_flag == Except_entered) \
                Except_flag = Except_finalized;
                
    #define END_TRY \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
            } if (Except_flag == Except_raised) RERAISE; \
    } while (0);
    
    #endif
    

    TRY

    先上代码

    #define TRY do { \
        volatile int Except_flag; \
        Except_Frame Except_frame; \
        Except_frame.prev = Except_stack; \
        Except_stack = &Except_frame;  \
        Except_flag = setjmp(Except_frame.env); \
        if (Except_flag == Except_entered) {
    

    TRY 部分很简单,主要是定义一个异常帧,并将其压入栈中,主要用于保存当前变量jmp_buf。其实就是设置传送点,我们触发异常后可以传送到这里来。这里各位朋友可以想一下,为什么这里需要使用栈,我个人觉得这里使用栈来保存是一个很大的妙处。

    这个异常帧在这里是没有任何信息的,只有在触发异常时,触发异常函数Except_raise会填充其成员,所以只有在返回setjmp时才会有异常信息。

    如果我们需要其他的异常信息,我们可以在这里写上我们需要的成员来保存我们想要的异常信息

    Except_entered 表示的是进入异常捕获区域,如果发生异常,那么Except_flag将会被改变为 Except_raised

    EXCEPT

    #define EXCEPT(e) \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } else if (Except_frame.exception == &(e)) { \
            Except_flag = Except_handled;
    

    EXCEPT 部分也很简单,就是判断Except_flag是否Except_entered,即初始化时的状态。

    如果是,则说明没有发生异常,并弹出栈顶异常帧。

    如果不是,查找所有EXCEPT,如果找到了异常e,则执行异常处理函数,并将Except_flag改变为Except_handled,表明当前异常已经被处理。

    这部分相信不需要过多解释各位朋友也能明白

    ELSE

    #define ELSE \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } else { \
            Except_flag = Except_handled;
    

    ELSE也很简单,同EXCEPT一样,只是没有判断查找异常的过程,这部分相当于默认分支处理

    END_TRY

    #define END_TRY \
            if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
            } if (Except_flag == Except_raised) RERAISE; \
    } while (0);
    

    重点来了,我个人觉得END_TRY部分是整个机制实现的精髓。

    当程序执行到了这里后有2种判断情况,一种是没有发生异常,直接过。而第二种情况就是
    Except_flag 是值为 Except_raised

    这里是为什么会出现 Except_raised呢,不应该只有2种情况吗,一种是被Except_raise发生并处理异常,最后被修改为Except_handled,一种是程序无异常Except_entered

    笔者当初有这一点困惑(可能智商不够用),仔细翻读书本也没找到相关的叙述。

    我在这里解说一下,还有一种情况是如果捕获为未知异常呢?也就是在使用EXCEPT时没有写出来的异常,那么此时状态就是 Except_raised

    那么什么情况下会捕获未知异常呢?

    第一、程序员自己写的异常给忘了,没有捕获到该异常,那么此时程序将会直接退出abort

    第二、捕获异常可以是多层次的,也就是异常捕获里面再套一层异常捕获。

    我们给出下面的一段代码

    #include <stdio.h>
    #include "except.h"
    Except_t error1 = {"error1"};
    Except_t error2 = {"error2"};
    Except_t error3 = {"error3"};
    Except_t error4 = {"error4"};
    Except_t error5 = {"error5"};
    Except_t error6 = {"error6"};
    Except_t error7 = {"error7"};
    int main()
    {
        TRY//第一层TRY
            printf("first try\n");
            TRY//第二层TRY
                printf("second try\n");
                RAISE(error1);
            EXCEPT(error5)
                printf("catch error5\n");
            END_TRY//第二层END_TRY
        EXCEPT(error1)
            printf("catch error1\n");
        EXCEPT(error2)
            printf("catch error2\n");
        EXCEPT(error3)
            printf("catch error3\n");
        ELSE
            printf("catch default error\n");
        END_TRY//第一层END_TRY
    
    }
    

    可以看出我们使用 2 次TRY,那么这个时候栈的作用就体现出来了,每次都会压入最外层的异常帧。这里我们压入了2个异常帧。

    那么在代码中,第二层的TRY触发了第一层的error1,此时代码的工作流程是这样的。

    抛出异常后,代码先检查第二层的所有EXCEPT,发现没有处理error1的,那么此时在第二层的END_TRY中栈里面的栈顶异常帧将会被弹出。而此时因为没有经过任何EXCEPT处理,那么第二层的Except_flag的值就是Except_raised,所以此时会触发RERAISE(其实就是Except_raise)。

    Except_raise中,又会去取出栈顶的异常帧并使用longjmp返回到第一层的TRY,在这里又会继续去查找所有的EXCEPT,直到找到了error1

    到了这里,处理结束,总结来说就是:

    栈的作用压入每一层的异常帧,而END_TRY中的Except_raised作用是再次触发更上一层的异常,直到将所有的异常帧弹出

    好了,以上大致就是本文章的所有内容了。当然了,这个异常处理机制并非完美的,比如在EXCEPT再次抛出异常,那么程序将会直接dump掉,又例如在多线程中,一个栈会被压入不同线程的异常帧。当然改进的办法总是有的,在于各位如何实现而已,这里就不多说了。

    那么在linux中实现当然是没有问题的,在嵌入式平台中呢?其实有些嵌入式平台的库是支持setjmp和longjmp的,只要能够支持这2个接口,那么就完全可以实现该机制了

    后记

    笔者以前翻阅过一遍APUE(现在忘得差不多了),当时仅仅是为了加强对Linux编程的理解,那时看到setjmp和longjmp并不知道他们的作用。直到阅读了《C语言接口与实现》,才知道它们能够实现异常处理机制。所以很惭愧,这些都是很古老的知识了,但是我知道现在才知道他们的作用,感慨感慨,果然需要多多学习才是。

    相关文章

      网友评论

          本文标题:C语言接口与实现之异常处理try-except

          本文链接:https://www.haomeiwen.com/subject/huaanqtx.html