HOWTO · C

C 語言中的 try...catch

本文演示了一個在 C 語言中實現 try-catch 行為的示例,預設情況下,它不支援 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 實現。