实用性不大,可以当做熟悉调用栈的一个手段。
你写的代码当然不会有bug,但是有时候你实在太困了,写了一个一除以零,程序瞬间崩了。程序中难免会有异常,可预知的不可预知的都在时刻发生着,这些异常往往会导致程序运行终止。如果在程序中加上各种各样的判断,不仅会使代码变得臃肿、可读性低,而且总有你预想不到的情况。为了解决这种问题,一些编程语言中引入了异常处理机制,可以在发生异常的情况下不中断程序的运行,。然而,C语言作为一种古老的语言,尽管时刻散发着成熟的魅力,但却总显得有些落伍,本文将会为C语言量身订造一身潮牌,尽管这身潮牌比较省“布料“。
异常有三个主要的关键字,try、catch和throw。try和catche必须搭配使用,try关键字标识代码块运行受保护,出现异常时会立刻终止转向执行catche代码块,否则将跳过catche代码块,继续执行后面的代码,throw用于抛出异常。C语言中是没有这些功能的,只能简单的去模拟,所以说比较省“布料”。
1. 函数调用栈
栈是一种后进先出(LIFO)的数据结构,程序运行其实就是按照一定的逻辑调用不同的函数,在函数调用时,系统会在内存中维护一个“调用记录“,以栈的形式存储,称为调用栈,每一个函数调用称为调用帧,调用函数时会将一个新的调用帧压入栈,调用完毕后会将调用帧出栈,这样在程序启动时调用栈为空,正常结束时调用栈恢复为空,如果运行过程中出现异常导致程序终止,此时调用栈并未清空,所以可以通过调用栈判断那个函数出现了问题。C语言并没有提供调用栈相关的接口(才疏学浅可能没有找到,欢迎评论指正),所以只能动手写了一个,先定一个调用帧的结构体:
// runTimeStack.h
小啰嗦一句,上面这个结构体起了两个名字,我在读标准库的源码时经常看到这种写法,语义可以表达的很清楚。调用栈使用单向链表实现,除了链指针外只有一个属性message,用来存储当前栈的一些信息。一个线程应该只有一个调用栈,为了保证唯一性需要进行一点简单的封装:
// runTimeStack.cpp
其它文件引入runTimeStack.h时stack变量不会被暴露出去,外界可以通过getRunTimeStack()获取,这样就可以保证所有地方获取的都是一个stack,接下来是两个至关重要的宏,也是实现收集调用栈的核心(思考一下为什么不可以用函数?):
#define IN_STACK do {
要理解这两个宏,先要了解这三个标准宏(前后都是两个下划线)__func__、__FILE__和__LINE__,其实不用“谈宏色变“,理解了宏是什么以后特别简单,一言蔽之就是简单的文本替换,而有一些标准宏完全不用理会它的原理,就像上面这三个,只要知道怎么用就可以了,__func__可以得到当前所在函数的名称,把它放在main中,它的值就是main,放在test函数中它的值就是test,__FILE__可以得到当前所在文件的绝对路径,__LINE__就更简单了,表示当前在文件的哪一行。所以可以通过这三个值定位到一个精准的位置。
IN_STACK会生成当前位置的调用帧写入全局栈,OUT_STACK则会吧全局栈的栈顶移除,涉及到Error的代码可以先不用看,后文会提到。回到之前提到过的问题,IN_STACK和OUT_STACk可以用函数来代替吗?答案是不可以的,使用到的三个标准宏有一个共同的特点,它们都与自身所在位置紧密相关,如果放在一个函数中,那得到的结果都是这个函数的位置,而宏定义就是简单的文本替换,最终的代码会被放到宏的位置,那这三个标准宏得到的自然就是正确的结果了。
// runTimeStack.cpp
写一个简单的例子测试一下:
#include
运行结果如下:
test3............
test2............
test1............
at test1 (F:VS输出目录asyncmain.cpp: 11)
at test2 (F:VS输出目录asyncmain.cpp: 18)
at test3 (F:VS输出目录asyncmain.cpp: 25)
at main (F:VS输出目录asyncmain.cpp: 42)
效果还不错,有了调用栈以后就可以做很多事了。
2. 异常的捕获与处理
二话不说先上定义:
typedef
message属性表示异常的信息,stack属性是异常的调用栈信息,此处的异常栈和调用栈在没有发生异常的情况是一致的,当发生异常的时候异常栈还要记录一些异常信息,举个简单例子说明,函数A被调用(入调用栈),函数A内部抛出异常,函数A终止执行(出调用栈),返回上一层,此时捕获异常时,由于函数A已经出栈,无法获取到异常信息,所以还要维护一个异常栈。做好这些准备以后,真正的主角开始登场:
#define Try do { Error __error__ = initError();
先不着急解释代码的意思,先来看一个简单的例子:
#include
运行结果如下:
test3............
test2............
test1............
这里发生异常了!!!
at test1 (F:VS输出目录asyncmain.cpp: 7)
at test1 (F:VS输出目录asyncmain.cpp: 5)
at test2 (F:VS输出目录asyncmain.cpp: 12)
at test3 (F:VS输出目录asyncmain.cpp: 19)
at main (F:VS输出目录asyncmain.cpp: 26)
把函数中的宏翻译一下:
void
现在看来是不是很简单,所谓的Try-Catch就是一个break中断,同上文提到过的全局调用栈stack一样,也创建了一个被封装的全局Error,只不过初始值为NULL,只有在Try的时候会进行初始化:
Error
初始化Error的时候要把当前调用栈拷贝到Error中,message赋值为NULL(判断有无异常的标识),Catch则判断error的message属性有没有被赋值,不为NULL时说明发生了异常,则要执行后面处理异常的代码。CatchEnd负责回收Error的内存,并重新复制为NULL。还有一点需要提到,Throw是可以单独使用的宏,它会先获取全局Error,如果为空的先初始化,然后写入异常调用帧。在IN_STACK中也有一部分涉及到了Error的处理,如果Error不为NULL,则要将调用帧同步写入Error中。
完整的实现:源码