cii——异常与断言

1. 为什么需要异常和断言

异常:用于release阶段,程序逻辑无问题,但是系统环境异常,导致程序错误。
断言:用于debug阶段,设置程序检查点,方便找到程序bug.

2. c语言与异常

2.1 无异常机制的C语言

C语言本身未提供异常处理,但可以自己实现。

2.2 实现异常处理的基础

可以使用 setjmp 和 longjmp 实现异常处理。

#include <setjmp.h>
#include <stdio.h>

#define EXCEPT_1 1

jmp_buf env;
static void
func()
{
    longjmp(env, 1);
}
int
main(int argc, const char *argv[])
{
    int ret;

    ret = setjmp(env);
    if (ret == 0) {
        func();
    }
    else if (ret == EXCEPT_1) {
        printf("catch except 1\n");
    }

    return 0;
}

执行输出:catch except 1
可见,可以用longjmp的第二个参数表示不同的异常 void longjmp(jmp_buf env, int val);
问题在于env的存储。
由于异常处理和函数调用逻辑相关,所以可以用栈存储env

#include <setjmp.h>
#include <stdio.h>

#define EXCEPT_1 1
#define EXCEPT_2 2

typedef struct except_s {
        jmp_buf env;
        struct except_s *next;
} except_t;

except_t *except_stack_top = NULL;

static void
func2()
{
        jmp_buf *env;

        env = &except_stack_top->env;
        except_stack_top = except_stack_top->next;
        longjmp(*env, EXCEPT_2);
}

static void
func()
{
        int ret;

        except_t e = {.next = NULL};
        if (except_stack_top == NULL) {
                except_stack_top = &e;
        }
        else {
                e.next = except_stack_top;
                except_stack_top = &e;
        }

        ret = setjmp(e.env);
        if (ret == 0) {
                func2();
        }
        else if (ret == EXCEPT_1) {
                printf("catch except 1\n");
        }
        else {
                // 没有找到异常处理,继续向上抛异常
                jmp_buf *env;
                env = &except_stack_top->env;
                except_stack_top = except_stack_top->next;
                longjmp(*env, ret);
        }

}

int
main(int argc, const char *argv[])
{
        int ret;

        except_t e = {.next = NULL};
        if (except_stack_top == NULL) {
                except_stack_top = &e;
        }
        else {
                e.next = except_stack_top;
                except_stack_top = &e;
        }
        ret = setjmp(e.env);
        if (ret == 0) {
                func();
        }
        else if (ret == EXCEPT_1) {
                printf("catch except 1\n");
        }
        else if (ret == EXCEPT_2) {
                printf("catch except 2\n");
        }
        else {
                printf("no catch except\n");
        }

        return 0;
}

用栈的缺点是不能一次找到异常处理点

3 接口设计

3.1 try-excpet

3.1.1 说明

try-except:
TRY
s
EXCEPT(e1)
s1
EXCEPT(e2)
s2
...
EXCEPT(en)
sn
ELSE
s0
END_TRY

TRY之后是可能引发异常的代码,即s。
EXCEPT(e)后是处理异常e的代码,
ELSE后是默认异常处理的代码
END_TRY是卸载异常捕获

如果代码s部分触发异常,则立即中断执行,跳转到对应的 e 处的代码,执行处理异常,
若没有找到对应的异常,则执行ELSE后的s0。最后执行END_TRY卸载异常捕获。

如果代码s没有触发异常,则执行END_TRY卸载异常。

3.1.2 示例

extern except_t allocate_failed;
char *buf;
TRY
buf = allocate(100);
EXCEPT(allocate_failed)
fprintf(stderr, "couldn't allocate the buffer\n");
exit(EXIT_FAILURE);
END_TRY;

3.2 try-finally

3.2.1 说明

TRY
S
FINALLY
S1
END_TRY
当触发异常e时,S执行被中断,执行S1部分,S1执行完成后,再次触发e异常,向上抛。
当没有触发异常,S执行完后,继续执行S1,然后执行END_TRY,后的代码

相当于
TRY
S
ELSE
S1
RERAISE;
END_TRY;
S1

所以:无论触发异常,都会执行S1
try-finally的目的是,当触发异常时给客户程序一个机会进行清理。

3.2.3 示例

FILE *fp = fopen();
char *buf;
TRY
buf = allocate(100);
...
FINALLY
fclose(fp);
END_TRY;

4. 实现

except.h

#ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED
#include <setjmp.h>
#define T Except_T
typedef struct T {
        char *reason;
} T;
typedef struct Except_Frame Except_Frame;
struct Except_Frame {
        Except_Frame *prev;
        jmp_buf env;
        const char *file;
        int line;
        const T *exception;
};
enum { Except_entered=0, Except_raised,
       Except_handled,   Except_finalized };
#ifdef WIN32
__declspec(thread)
#endif
extern Except_Frame *Except_stack;
extern const Except_T Assert_Failed;
void Except_raise(const 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)
#undef T
#endif

4.2 excpet.c

#include <stdlib.h>
#include <stdio.h>
#include "assert.h"
#include "except.h"
#define T Except_T
#ifdef WIN32
__declspec(thread)
#endif
Except_Frame *Except_stack = NULL;
void Except_raise(const 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);
}

5. 注意

如果S(被检查异常部分)改变了自动变量的值,如果异常导致转向某个处理程序Sn,或END_TRY之后的代码,
那么该修改可能无效。如
int i = 0;
TRY
i++;
RAISE(e);
EXCEPT(e)
;
END_TRY
printf("%d", i);
可能输出0,也可能输出1。这取决于setjmp和longjmp的实现。
S中改变的局部变量必须声明未volatile。如
volatile int i = 0;

6. 断言

6.1 说明

断言通常定义如下

#ifdef NDEBUG
#define assert(e) (void 0)
#else
extern void assert(int e);
#define assert(e) ((void) ( (e) || \
            fprintf(stderr, "%s:%d: Assertion failed: %s\n", \
                __FILE__, (int)__LINE__, #e), abort(), 0))
#endif

这里希望将断言为真,抛出异常。

6.2 实现

6.2.1 assert.h

#undef assert
#ifdef NDEBUG
#define assert(e) ((void)0)
#else
#include "except.h"
extern void assert(int e);
#define assert(e) ((void)((e)||(RAISE(Assert_Failed),0)))
#endif

6.2.2 assert.c

#include "assert.h"
const Except_T Assert_Failed = { "Assertion failed" };
void (assert)(int e) {
        assert(e);
}

posted on 2021-11-16 00:51  开心种树  阅读(125)  评论(0编辑  收藏  举报