C 语言中的 try...catch

Muhammad Husnain 2023年10月12日
  1. C 语言中的 Try-Catch
  2. 在 C 语言中将 Finally 添加到 Try-Catch
C 语言中的 try...catch

Try-Catch 机制在 Python、C++ 和 JavaScript 等许多编程语言中都很常见。一般结构如下。

try {
  /*
  Insert some lines of code that will probably give you errors
  */
} catch {
  /*
  Write some code to handle the errors you're getting.
  */
}

它们允许你编写代码而无需测试每个语句。如果在 try 块中运行的程序遇到异常,则将异常传递给 catch 块。

如果异常与某些异常类型匹配,则执行 catch 块内的代码。否则,异常将传递回 try 块。

C 语言中的 Try-Catch

C 不支持异常处理。至少,它没有任何内置机制。

本指南将演示在 C 语言中提供 try-catch 功能的可能解决方案。应该注意,该解决方案不一定是完整的。

如果没有在遍历堆栈时释放内存的机制,异常处理系统是不完整和安全的,并且 C 没有垃圾收集器。我们可能还需要包含上下文管理器来释放内存。

该解决方案不打算提供完整而广泛的 try-catch 机制。这个概念在技术上可以用来处理一些异常。

我们将逐步构建解决方案,对代码进行更新。我们将使用 C 提供的两个函数,longjmpsetjmp,它们可以从 setjmp.h 头文件中获得。

我们将仔细研究这两个函数的定义。

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp 接受一个 jmp_buf 类型的变量。直接调用此函数时,它返回 0

longjmp 接受两个变量,当使用相同的 jmp_buf 变量调用 longjmp 时,setjmp 函数返回与 longjmp 的第二个参数(val)相同的值。

这里的 env 变量本质上是调用环境,代表了寄存器的状态和函数调用时在代码中的位置。当调用 longjmp 时,调用环境中的状态被复制到处理器,并返回存储在 longjmpval 参数中的值。

对于一个简单的 Try-Catch 块,想法是将 Try 语句映射到 if 语句,然后 Catch 语句将成为条件的 else。在这里,我们可以巧妙地利用 setjmp 可以返回不同值的事实。

如果函数返回 0,那么我们知道唯一运行的代码是 TRY 块中的代码。如果函数返回任何其他内容,我们需要以与开始时相同的状态进入我们的 CATCH 块。

当我们 THROW 异常时,我们可以调用 longjmp 函数。

正如你将在下面的代码中看到的,我们还需要关闭 TRY 块。我们创建了一个 ENDTRY 函数,它提供了 do-while 块的结束部分。

这也有助于我们在同一个块中创建多个 TRY 语句。应该注意的是,它们不能嵌套,因为我们将重用 buf_state 变量。

下面是这个实现的一个例子。

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

#define TRY            \
  do {                 \
    jmp_buf buf_state; \
    if (!setjmp(buf_state)) {
#define CATCH \
  }           \
  else {
#define ENDTRY \
  }            \
  }            \
  while (0)
#define THROW longjmp(buf_state, 1)

int main() {
  TRY {
    printf("Testing Try statement \n");
    THROW;
    printf(
        "Statement should not appear, as the THROW block has already thrown "
        "the exception \n");
  }
  CATCH { printf("Got Exception \n"); }
  ENDTRY;

  return 0;
}

输出:

Testing Try statement
Got Exception

对于实际系统,这还不够。我们需要有不同类型的异常。

上面的例子只支持一种异常。再一次,我们可以使用 setjmp 的不同返回值。

代替使用 if-else,我们将使用 switch-case 来切换它。

设计如下:TRY 语句将使用 switch 语句,CATCH 将是一个带有参数的宏,表示异常类型。每个 CATCH 语句的条件是它必须使用 break 关闭前一个 case

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

#define TRY                      \
  do {                           \
    jmp_buf buf_state;           \
    switch (setjmp(buf_state)) { \
      case 0:
#define CATCH(x) \
  break;         \
  case x:
#define ENDTRY \
  }            \
  }            \
  while (0)
#define THROW(x) longjmp(buf_state, x)

#define EXCEPTION1 (1)
#define EXCEPTION2 (2)
#define EXCEPTION3 (3)

int main() {
  TRY {
    printf("Inside Try statement \n");
    THROW(EXCEPTION2);
    printf("This does not appear as exception has already been called \n");
  }
  CATCH(EXCEPTION1) { printf("Exception 1 called \n"); }
  CATCH(EXCEPTION2) { printf("Exception 2 called \n"); }
  CATCH(EXCEPTION3) { printf("Exception 3 called \n"); }
  ENDTRY;

  return 0;
}

输出:

Inside Try statement
Exception 2 called

在 C 语言中将 Finally 添加到 Try-Catch

我们需要为完整的功能性 Try-Catch 实现添加一个 FINALLY 块。finally 块通常在 trycatch 块完成后执行。

无论是否抛出异常,它都会执行。

我们将如何实现这一点?关键思想是使用 switch 案例的 default 案例来实现 FINALLY 块。

但是,如果在正常情况下调用了异常,则 switch-case 将不会运行 default 情况。

我们将使用类似于 Duff’s Device 的机制。我们基本上将把 switch-case 语句与 do-while 语句交织在一起。

它的逻辑是这样的。

switch (an expression) {
  case 0:
    while (1) {
      // code for case 0
      break;
      case 1:
        // code for case 1
        break;
    }
  default:
    // code for default case
}

我们使用了 C 语言中最具争议的特性之一:在每个 case 标签之前开关不会自动断开。在这里,当调用 break 时,while 语句嵌套在 switch-case 中;它退出 while 循环并继续遍历案例。

在我们的代码上下文中(如下所示),switch 案例是我们所有的异常,default 案例在我们的 FINALLY 块中。自然,它会落入默认情况,因为异常代码已经被执行。

这显示在下面的代码中。

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

#define TRY                      \
  do {                           \
    jmp_buf buf_state;           \
    switch (setjmp(buf_state)) { \
      case 0:                    \
        while (1) {
#define CATCH(x) \
  break;         \
  case x:
#define ENDTRY \
  }            \
  }            \
  while (0)
#define THROW(x) longjmp(buf_state, x)
#define FINALLY \
  break;        \
  }             \
  default:
#define EXCEPTION1 (1)
#define EXCEPTION2 (2)
#define EXCEPTION3 (3)

int main() {
  TRY {
    printf("Inside Try statement \n");
    THROW(EXCEPTION2);
    printf("This does not appear as exception has already been called \n");
  }
  CATCH(EXCEPTION1) { printf("Exception 1 called \n"); }
  CATCH(EXCEPTION2) { printf("Exception 2 called \n"); }
  CATCH(EXCEPTION3) { printf("Exception 3 called \n"); }
  FINALLY { printf("This will always be called! \n"); }
  ENDTRY;

  return 0;
}

输出:

Inside Try statement
Exception 2 called
This will always be called!

用 C 语言制作 try-catch 系统的指南到此结束。当然,这里可能存在内存问题和一些限制(例如缺乏对嵌套 try-catch 系统的支持),但这是一个 C 语言中的功能性 try-catch 实现。

Muhammad Husnain avatar Muhammad Husnain avatar

Husnain is a professional Software Engineer and a researcher who loves to learn, build, write, and teach. Having worked various jobs in the IT industry, he especially enjoys finding ways to express complex ideas in simple ways through his content. In his free time, Husnain unwinds by thinking about tech fiction to solve problems around him.

LinkedIn