C语言
首发于C语言
C语言模拟异常

C语言模拟异常

实用性不大,可以当做熟悉调用栈的一个手段。

你写的代码当然不会有bug,但是有时候你实在太困了,写了一个一除以零,程序瞬间崩了。程序中难免会有异常,可预知的不可预知的都在时刻发生着,这些异常往往会导致程序运行终止。如果在程序中加上各种各样的判断,不仅会使代码变得臃肿、可读性低,而且总有你预想不到的情况。为了解决这种问题,一些编程语言中引入了异常处理机制,可以在发生异常的情况下不中断程序的运行,。然而,C语言作为一种古老的语言,尽管时刻散发着成熟的魅力,但却总显得有些落伍,本文将会为C语言量身订造一身潮牌,尽管这身潮牌比较省“布料“。

异常有三个主要的关键字,trycatchthrow。try和catche必须搭配使用,try关键字标识代码块运行受保护,出现异常时会立刻终止转向执行catche代码块,否则将跳过catche代码块,继续执行后面的代码,throw用于抛出异常。C语言中是没有这些功能的,只能简单的去模拟,所以说比较省“布料”。

1. 函数调用栈

栈是一种后进先出(LIFO)的数据结构,程序运行其实就是按照一定的逻辑调用不同的函数,在函数调用时,系统会在内存中维护一个“调用记录“,以栈的形式存储,称为调用栈,每一个函数调用称为调用帧,调用函数时会将一个新的调用帧压入栈,调用完毕后会将调用帧出栈,这样在程序启动时调用栈为空,正常结束时调用栈恢复为空,如果运行过程中出现异常导致程序终止,此时调用栈并未清空,所以可以通过调用栈判断那个函数出现了问题。C语言并没有提供调用栈相关的接口(才疏学浅可能没有找到,欢迎评论指正),所以只能动手写了一个,先定一个调用帧的结构体:

// runTimeStack.h

typedef struct frame {
    string message;
    struct frame * next;
}*Frame, *RunTimeStack;

#define newFrame() NEW(struct frame)

小啰嗦一句,上面这个结构体起了两个名字,我在读标准库的源码时经常看到这种写法,语义可以表达的很清楚。调用栈使用单向链表实现,除了链指针外只有一个属性message,用来存储当前栈的一些信息。一个线程应该只有一个调用栈,为了保证唯一性需要进行一点简单的封装:

// runTimeStack.cpp

RunTimeStack stack = createRunTimeStack();

RunTimeStack * getRunTimeStack() {
    return &stack;
}

RunTimeStack createRunTimeStack() {
    RunTimeStack stack = newFrame();
    stack->message = newString("\n");
    stack->next = NULL;
    return stack;
}

其它文件引入runTimeStack.h时stack变量不会被暴露出去,外界可以通过getRunTimeStack()获取,这样就可以保证所有地方获取的都是一个stack,接下来是两个至关重要的宏,也是实现收集调用栈的核心(思考一下为什么不可以用函数?):

#define IN_STACK do { \
    RunTimeStack * stack = getRunTimeStack(); \
    Frame frame = newFrame(); \
    frame->message = (char *)malloc(14 + strlen(__func__) + strlen(__FILE__) + getNumberLength(__LINE__)); \
    sprintf(frame->message, "    at %s (%s: %d)", __func__, __FILE__, __LINE__); \
    frame->next = *stack; \
    *stack = frame; \
    Error error = getError(); \
    if (error != NULL) { \
        Frame errorFrame = newFrame(); \
        errorFrame->message = newString(frame->message); \
        setErrorFrame(errorFrame); \
    } \
} while(0);

#define OUT_STACK do { \
    RunTimeStack * stack = getRunTimeStack(); \
    Frame frame = *stack; \
    *stack = (*stack)->next; \
    free(frame->message); \
    free(frame); \
} while(0);

要理解这两个宏,先要了解这三个标准宏(前后都是两个下划线)__func____FILE____LINE__,其实不用“谈宏色变“,理解了宏是什么以后特别简单,一言蔽之就是简单的文本替换,而有一些标准宏完全不用理会它的原理,就像上面这三个,只要知道怎么用就可以了,__func__可以得到当前所在函数的名称,把它放在main中,它的值就是main,放在test函数中它的值就是test,__FILE__可以得到当前所在文件的绝对路径,__LINE__就更简单了,表示当前在文件的哪一行。所以可以通过这三个值定位到一个精准的位置。

IN_STACK会生成当前位置的调用帧写入全局栈,OUT_STACK则会吧全局栈的栈顶移除,涉及到Error的代码可以先不用看,后文会提到。回到之前提到过的问题,IN_STACK和OUT_STACk可以用函数来代替吗?答案是不可以的,使用到的三个标准宏有一个共同的特点,它们都与自身所在位置紧密相关,如果放在一个函数中,那得到的结果都是这个函数的位置,而宏定义就是简单的文本替换,最终的代码会被放到宏的位置,那这三个标准宏得到的自然就是正确的结果了。

// runTimeStack.cpp

RunTimeStack stack = createRunTimeStack();

RunTimeStack * getRunTimeStack() {
    return &stack;
}

RunTimeStack createRunTimeStack() {
    RunTimeStack stack = newFrame();
    stack->message = newString("\n");
    stack->next = NULL;
    return stack;
}

写一个简单的例子测试一下:

#include<"runTimeStack.h">
void test1() {
    IN_STACK;
    printf("test1............\n");
    displayRunTimeyStack();
    OUT_STACK;
}

void test2() {
    IN_STACK;
    printf("test2............\n");
    test1();
    OUT_STACK;
}

void test3() {
    IN_STACK;
    printf("test3............\n");
    test2();
    OUT_STACK;
}

int main() {
    IN_STACK;
    test3();
    OUT_STACK;
    return 0;
}

运行结果如下:

test3............
test2............
test1............
    at test1 (F:\VS输出目录\async\main.cpp: 11)
    at test2 (F:\VS输出目录\async\main.cpp: 18)
    at test3 (F:\VS输出目录\async\main.cpp: 25)
    at main (F:\VS输出目录\async\main.cpp: 42)

效果还不错,有了调用栈以后就可以做很多事了。

2. 异常的捕获与处理

二话不说先上定义:

typedef struct error {
    string message;
    RunTimeStack stack;
}*Error;
#define newError() NEW(struct error)

message属性表示异常的信息,stack属性是异常的调用栈信息,此处的异常栈和调用栈在没有发生异常的情况是一致的,当发生异常的时候异常栈还要记录一些异常信息,举个简单例子说明,函数A被调用(入调用栈),函数A内部抛出异常,函数A终止执行(出调用栈),返回上一层,此时捕获异常时,由于函数A已经出栈,无法获取到异常信息,所以还要维护一个异常栈。做好这些准备以后,真正的主角开始登场:

#define Try do { Error __error__ = initError();

#define Catch if (__error__->message == NULL) break;

#define CatchEnd destoryError(); } while(0);

#define Throw(errMsg) do { \
    Error error = getError(); \
    if (error == NULL) error = initError(); \
    error->message = errMsg; \
    Frame errorFrame = newFrame(); \
    errorFrame->message = (char *)malloc(14 + strlen(__func__) + strlen(__FILE__) + getNumberLength(__LINE__)); \
    sprintf(errorFrame->message, "    at %s (%s: %d)", __func__, __FILE__, __LINE__); \
    setErrorFrame(errorFrame); \
} while(0)

先不着急解释代码的意思,先来看一个简单的例子:

#include<"runTimeStack.h">
void test1() {
    IN_STACK;
    printf("test1............\n");
    Throw(newString("这里发生异常了!!!"));
    OUT_STACK;
}

void test2() {
    IN_STACK;
    printf("test2............\n");
    test1();
    OUT_STACK;
}

void test3() {
    IN_STACK;
    printf("test3............\n");
    test2();
    OUT_STACK;
}

int main() {
    IN_STACK;

    Try
        test3();
    Catch
        disPlayError();
    CatchEnd

    OUT_STACK;
    return 0;
}

运行结果如下:

test3............
test2............
test1............
这里发生异常了!!!
    at test1 (F:\VS输出目录\async\main.cpp: 7)
    at test1 (F:\VS输出目录\async\main.cpp: 5)
    at test2 (F:\VS输出目录\async\main.cpp: 12)
    at test3 (F:\VS输出目录\async\main.cpp: 19)
    at main (F:\VS输出目录\async\main.cpp: 26)

把函数中的宏翻译一下:

void test1() {
    IN_STACK;
    printf("test1............\n");
    Throw(newString("这里发生异常了!!!"));
    do {
        Error error = getError();
        if (error == NULL) error = initError();
        error->message = errMsg;
        Frame errorFrame = newFrame();
        errorFrame->message = (char *)malloc(14 + strlen(__func__) + strlen(__FILE__) + getNumberLength(__LINE__));
        sprintf(errorFrame->message, "    at %s (%s: %d)", __func__, __FILE__, __LINE__);
        setErrorFrame(errorFrame);
    } while(0)
    OUT_STACK;
}

int main() {
    IN_STACK;
    do {
        Error __error__ = initError();
        test3();
        if (__error__->message == NULL) break;
        disPlayError();
    } while(0);

    OUT_STACK;
    return 0;
}

现在看来是不是很简单,所谓的Try-Catch就是一个break中断,同上文提到过的全局调用栈stack一样,也创建了一个被封装的全局Error,只不过初始值为NULL,只有在Try的时候会进行初始化:

Error error = NULL;
void setErrorFrame(Frame frame) {
    if (error != NULL) {
        frame->next = error->stack;
        error->stack = frame;
    }
}

Error getError() {
    return error;
}

void destoryError() {
    if (error != NULL) {
        free(error->message);
        while (error->stack != NULL) {
            Frame frame = error->stack;
            error->stack = error->stack->next;
            free(frame->message);
            free(frame);
        }
    }
    free(error);
    error = NULL;
}

Error initError() {
    error = newError();
    error->message = NULL;
    error->stack = newFrame();
    Frame frame = error->stack;
    Frame p = stack;
    while (p != NULL) {
        frame->message = newString(p->message);
        if (p->next != NULL) {
            frame->next = newFrame();
            frame = frame->next;
        }
        else {
            frame->next = NULL;
        }
        p = p->next;
    }
    return error;
}

初始化Error的时候要把当前调用栈拷贝到Error中,message赋值为NULL(判断有无异常的标识),Catch则判断error的message属性有没有被赋值,不为NULL时说明发生了异常,则要执行后面处理异常的代码。CatchEnd负责回收Error的内存,并重新复制为NULL。还有一点需要提到,Throw是可以单独使用的宏,它会先获取全局Error,如果为空的先初始化,然后写入异常调用帧。在IN_STACK中也有一部分涉及到了Error的处理,如果Error不为NULL,则要将调用帧同步写入Error中。

完整的实现:源码

编辑于 2019-01-08

文章被以下专栏收录