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

前言

最近在学习《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语言接口与实现》,才知道它们能够实现异常处理机制。所以很惭愧,这些都是很古老的知识了,但是我知道现在才知道他们的作用,感慨感慨,果然需要多多学习才是。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容

  • @[toc]  编写计算机程序时,通常能够区分正常和异常情况。异常事件可能是错误,也可能是通常不会发生的事情。为处...
    奋斗在阿尔卑斯的皮卡丘阅读 811评论 0 0
  • Python异常处理 异常概念: 异常:就是不正常的情况,程序开发过程中错误和BUG都是补充正常的情况 异常发生的...
    youngkun阅读 924评论 0 4
  • ——精而简 崔毛毛 失眠是很多人的困扰,夜里睡不着,不仅头痛,背痛。也影响第二天的工作和生活。长期以来,造成神经衰...
    c49345312d5f阅读 495评论 0 1
  • 一直以来,两位副会长的工作不太具体,影响了工作效率。经研究决定:殷号令副会长主抓协会外部活动,如开展各种爱心活动,...
    3d9e9483439f阅读 553评论 0 0
  • 老娘无业游民的第一天 起床:我是十点起床的 就寝:昨晚上一点四十睡得 天气:好到不得了 心情:和天气一样好,因为什...
    于婷阅读 84评论 0 0