HOWTO · C

C 语言中的 try...catch

本文演示了一个在 C 语言中实现 try-catch 行为的示例,默认情况下,它不支持 try-catch/异常处理机制。

本页内容

C 语言没有内置的 try-catch 或异常处理机制。你可以使用 <setjmp.h> 中的 setjmp 和 longjmp 将控制流转移到已保存的位置,并据此构造一个基于宏的简易 TRY、CATCH 和 THROW 模式。这只是一种有限的控制流技巧,并非完整的异常系统:它不会自动释放资源;在示例的 switch 设计中,抛出的值若没有匹配的 CATCH,就不会选择任何处理器。每次调用 TRY 都有独立的块作用域 buf_state 和 switch,因此这种宏布局支持嵌套调用。

C 语言中的 Try-Catch

setjmp 会将执行环境保存到 jmp_buf 中。直接调用时,它返回 0;之后若通过 longjmp 跳回该环境,它会返回传入的值(但传入 0 时,观察到的返回值为 1)。longjmp 会恢复已保存的执行上下文,并从 setjmp 调用处继续执行。它不会把状态复制到处理器上。

下面的宏使用 switch:初次调用 setjmp 所得的结果会选择 TRY 主体,而每个 CATCH(value) 都会添加一个匹配的 case。THROW(value) 会调用 longjmp。如果返回的是没有匹配项的非零值,就不会执行任何 catch 主体;switch 结束时,该值会被忽略,也不会返回 TRY 主体。每个 CATCH 生成的 break 可避免执行流继续落入后面的 case。

运行以下示例:

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

#define TRY do { jmp_buf buf_state; switch (setjmp(buf_state)) { case 0:
#define CATCH(value) break; case value:
#define ENDTRY } } while (0)
#define THROW(value) longjmp(buf_state, value)
#define CODE_ONE 1
#define CODE_TWO 2

int main(void) {
  TRY {
    puts("Starting work");
    THROW(CODE_TWO);
    puts("This line is skipped");
  }
  CATCH(CODE_ONE) { puts("Code one"); }
  CATCH(CODE_TWO) { puts("Code two"); }
  ENDTRY;
  return 0;
}
Starting work
Code two

提供的运行结果以状态码 0 退出。这个完整程序演示了一次匹配的控制流转移。调用 longjmp 时,jmp_buf 必须仍然有效:如果包含对应 setjmp 的函数已经返回,就不能再跳回该环境。此外,如果在调用 setjmp 后、调用 longjmp 前修改了该函数中的非 volatile 自动变量,这些变量的值可能变为不确定状态。只有在理解这些控制流和生命周期规则后,才应使用这种模式。

在 C 语言中为 Try-Catch 添加 Finally

C 没有 FINALLY 语句,但可以在 do { ... } while (0) 包装结构中的 switch 之后编写类似 finally 的操作。这种紧凑的控制流结构会把 case 标签放在循环内部,类似 Duff 装置式结构:

int status = 0; do { switch (setjmp(env)) { case 0: work(); break; case CODE_TWO: handle(); break; default: status = 1; break; } } while (0); cleanup(); return status;

正常完成时,work() 执行完毕,其 break 会退出 switch;单次循环随之结束,执行流接着到达 cleanup()。发生匹配的抛出时,longjmp 会让 setjmp 返回 CODE_TWO,于是执行 handle(),其 break 也会进入同一条清理路径。若 setjmp 返回了不匹配的非零值,则会执行 default,记录未匹配的结果,其 break 同样会进入清理路径。在这几种情况下,break 退出的是 switch,而不是外围函数。这只是对控制流的说明,并不代表会自动清理资源或具备完整的异常语义。

下面的可运行示例演示通过 switch 和共享清理标签处理匹配抛出的路径:

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

#define CODE_TWO 2

int main(void) {
  jmp_buf env;
  switch (setjmp(env)) {
    case 0:
      puts("Starting work");
      longjmp(env, CODE_TWO);
    case CODE_TWO:
      puts("Handling code two");
      break;
    default:
      puts("No matching handler");
      break;
  }

cleanup:
  puts("Cleanup path");
  return 0;
}
Starting work
Handling code two
Cleanup path

提供的运行结果以状态码 0 退出。在这条路径中,longjmp 会让 setjmp 返回 CODE_TWO,随后运行处理器并到达清理标签。该标签展示了实现共享路径的另一种方式;对于上面的 do/while 结构,正常完成、匹配的抛出以及未匹配的返回,都需要明确安排通往清理代码的路径。longjmp 不会释放已分配的内存、关闭文件、解锁资源或运行析构函数。请沿程序支持的各条路径显式释放这些资源。