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);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程