129

我今天在想其他语言中存在的 try/catch 块。谷歌了一段时间,但没有结果。据我所知,C 中没有 try/catch 这样的东西。但是,有没有办法“模拟”它们?
当然,有断言和其他技巧,但没有像 try/catch 这样的技巧,它们也可以捕获引发的异常。谢谢

4

13 回答 13

109

C 本身不支持异常,但您可以使用setjmplongjmp调用在一定程度上模拟它们。

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened here\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjmp(s_jumpBuffer, 42);
}

这个网站有一个很好的教程,介绍如何使用setjmp和模拟异常longjmp

于 2012-05-14T15:12:03.250 回答
26

您可以在 C 中使用goto来处理类似的错误处理情况。
这是您可以在 C 中获得的最接近异常的等价物。

于 2012-05-14T15:08:14.517 回答
21

好的,我忍不住回复了这个。首先让我说我认为在 C 中模拟它不是一个好主意,因为它对于 C 来说确实是一个陌生的概念。

我们可以滥用预处理器和本地堆栈变量来使用有限版本的 C++ try/throw/catch。

版本 1(本地范围抛出)

#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) {__HadError=true;goto ExitJmp;}

版本 1 仅是本地抛出(不能离开函数的范围)。它确实依赖于 C99 在代码中声明变量的能力(如果 try 是函数中的第一件事,它应该在 C89 中工作)。

这个函数只是创建一个本地变量,所以它知道是否有错误并使用 goto 跳转到 catch 块。

例如:

#include <stdio.h>
#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) {__HadError=true;goto ExitJmp;}

int main(void)
{
    try
    {
        printf("One\n");
        throw();
        printf("Two\n");
    }
    catch(...)
    {
        printf("Error\n");
    }
    return 0;
}

这适用于:

int main(void)
{
    bool HadError=false;
    {
        printf("One\n");
        {
            HadError=true;
            goto ExitJmp;
        }
        printf("Two\n");
    }
ExitJmp:
    if(HadError)
    {
        printf("Error\n");
    }
    return 0;
}

版本 2(范围跳跃)

#include <stdbool.h>
#include <setjmp.h>

jmp_buf *g__ActiveBuf;

#define try jmp_buf __LocalJmpBuff;jmp_buf *__OldActiveBuf=g__ActiveBuf;bool __WasThrown=false;g__ActiveBuf=&__LocalJmpBuff;if(setjmp(__LocalJmpBuff)){__WasThrown=true;}else
#define catch(x) g__ActiveBuf=__OldActiveBuf;if(__WasThrown)
#define throw(x) longjmp(*g__ActiveBuf,1);

版本 2 要复杂得多,但基本上以相同的方式工作。它使用从当前函数到 try 块的长跳转。try 块然后使用 if/else 将代码块跳过到 catch 块,该块检查局部变量以查看它是否应该捕获。

该示例再次扩展:

jmp_buf *g_ActiveBuf;

int main(void)
{
    jmp_buf LocalJmpBuff;
    jmp_buf *OldActiveBuf=g_ActiveBuf;
    bool WasThrown=false;
    g_ActiveBuf=&LocalJmpBuff;

    if(setjmp(LocalJmpBuff))
    {
        WasThrown=true;
    }
    else
    {
        printf("One\n");
        longjmp(*g_ActiveBuf,1);
        printf("Two\n");
    }
    g_ActiveBuf=OldActiveBuf;
    if(WasThrown)
    {
        printf("Error\n");
    }
    return 0;
}

这使用全局指针,因此 longjmp() 知道上次运行的尝试。我们正在滥用堆栈因此子函数也可以有一个 try/catch 块。

使用此代码有许多缺点(但这是一种有趣的心理练习):

  • 它不会释放分配的内存,因为没有调用解构函数。
  • 一个范围内不能有超过 1 个 try/catch(无嵌套)
  • 您实际上不能像 C++ 中那样抛出异常或其他数据
  • 根本不是线程安全的
  • 您正在将其他程序员设置为失败,因为他们可能不会注意到黑客并尝试像 C++ try/catch 块一样使用它们。
于 2016-09-06T16:26:29.943 回答
10

在 C99 中,您可以将setjmp/longjmp用于非本地控制流。

在单个范围内,在存在多个资源分配和多个出口的情况下,C 的通用结构化编码模式使用goto,就像在这个例子中一样。这类似于 C++ 如何在后台实现自动对象的析构函数调用,如果您坚持这一点,即使在复杂的函数中也应该允许您在一定程度上保持清洁。

于 2012-05-14T15:10:01.730 回答
6

虽然其他一些答案已经涵盖了使用setjmpand的简单案例longjmp,但在实际应用程序中,有两个真正重要的问题。

  1. try/catch 块的嵌套。为您使用单个全局变量jmp_buf将使这些不起作用。
  2. 穿线。在这种情况下,一个单一的全局变量jmp_buf会给你带来各种痛苦。

解决这些问题的方法是维护一个线程本地堆栈,jmp_buf该堆栈会随着您的使用而更新。(我认为这是 lua 内部使用的)。

所以而不是这个(来自JaredPar的真棒答案)

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjump(s_jumpBuffer, 42);
}

你会使用类似的东西:

#define MAX_EXCEPTION_DEPTH 10;
struct exception_state {
  jmp_buf s_jumpBuffer[MAX_EXCEPTION_DEPTH];
  int current_depth;
};

int try_point(struct exception_state * state) {
  if(current_depth==MAX_EXCEPTION_DEPTH) {
     abort();
  }
  int ok = setjmp(state->jumpBuffer[state->current_depth]);
  if(ok) {
    state->current_depth++;
  } else {
    //We've had an exception update the stack.
    state->current_depth--;
  }
  return ok;
}

void throw_exception(struct exception_state * state) {
  longjump(state->current_depth-1,1);
}

void catch_point(struct exception_state * state) {
    state->current_depth--;
}

void end_try_point(struct exception_state * state) {
    state->current_depth--;
}

__thread struct exception_state g_exception_state; 

void Example() { 
  if (try_point(&g_exception_state)) {
    catch_point(&g_exception_state);
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
    end_try_point(&g_exception_state);
  }
}

void Test() {
  // Rough equivalent of `throw`
  throw_exception(g_exception_state);
}

同样,一个更现实的版本将包括一些将错误信息存储到的方法exception_state,更好地处理MAX_EXCEPTION_DEPTH(可能使用 realloc 来增加缓冲区,或类似的东西)。

免责声明:上面的代码是在没有任何测试的情况下编写的。这纯粹是为了让您了解如何构建事物。不同的系统和不同的编译器需要以不同的方式实现线程本地存储。该代码可能同时包含编译错误和逻辑错误 - 因此,尽管您可以随意使用它,但请在使用之前对其进行测试;)

于 2014-11-20T00:31:43.357 回答
5

这是在 C 中进行错误处理的另一种方法,它比使用 setjmp/longjmp 更高效。不幸的是,它不适用于 MSVC,但如果只使用 GCC/Clang 是一个选项,那么您可能会考虑它。具体来说,它使用“标签作为值”扩展,它允许您获取标签的地址,将其存储在一个值中并无条件跳转到它。我将使用一个例子来展示它:

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    /* Declare an error handler variable. This will hold the address
       to jump to if an error occurs to cleanup pending resources.
       Initialize it to the err label which simply returns an
       error value (NULL in this example). The && operator resolves to
       the address of the label err */
    void *eh = &&err;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    if (!engine)
        goto *eh; /* this is essentially your "throw" */

    /* Now make sure that if we throw from this point on, the memory
       gets deallocated. As a convention you could name the label "undo_"
       followed by the operation to rollback. */
    eh = &&undo_malloc;

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    if (!engine->window)
        goto *eh;   /* The neat trick about using approach is that you don't
                       need to remember what "undo" label to go to in code.
                       Simply go to *eh. */

    eh = &&undo_window_open;

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}

如果您愿意,您可以在定义中重构​​通用代码,从而有效地实现您自己的错误处理系统。

/* Put at the beginning of a function that may fail. */
#define declthrows void *_eh = &&err

/* Cleans up resources and returns error result. */
#define throw goto *_eh

/* Sets a new undo checkpoint. */
#define undo(label) _eh = &&undo_##label

/* Throws if [condition] evaluates to false. */
#define check(condition) if (!(condition)) throw

/* Throws if [condition] evaluates to false. Then sets a new undo checkpoint. */
#define checkpoint(label, condition) { check(condition); undo(label); }

那么例子就变成了

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    declthrows;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    checkpoint(malloc, engine);

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    checkpoint(window_open, engine->window);

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}
于 2017-07-04T18:04:27.940 回答
4

一个快速的谷歌搜索会产生像其他人提到的那样使用 setjmp/longjmp 的kludgey 解决方案。没有什么比 C++/Java 的 try/catch 更直接和优雅了。我自己比较偏爱 Ada 的异常处理。

使用 if 语句检查所有内容 :)

于 2012-05-14T15:10:49.053 回答
4

这可以setjmp/longjmp在 C中完成。P99有一个非常舒适的工具集,这也与 C11 的新线程模型一致。

于 2012-05-14T15:11:26.577 回答
3

在 C 中,您可以通过手动使用 if + goto 来“模拟”异常以及自动“对象回收”以进行显式错误处理。

我经常编写如下 C 代码(归结为突出错误处理):

#include <assert.h>

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    if ( ( ret = foo_init( f ) ) )
        goto FAIL;

    if ( ( ret = goo_init( g ) ) )
        goto FAIL_F;

    if ( ( ret = poo_init( p ) ) )
        goto FAIL_G;

    if ( ( ret = loo_init( l ) ) )
        goto FAIL_P;

    assert( 0 == ret );
    goto END;

    /* error handling and return */

    /* Note that we finalize in opposite order of initialization because we are unwinding a *STACK* of initialized objects */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

这是完全标准的 ANSI C,将错误处理与您的主线代码分开,允许(手动)堆栈展开初始化对象,就像 C++ 所做的那样,这里发生的事情是完全显而易见的。因为您在每个点都明确地测试失败,所以在每个可能发生错误的地方插入特定的日志记录或错误处理确实变得更容易。

如果您不介意一点宏魔术,那么您可以在执行其他操作(例如使用堆栈跟踪记录错误)时使其更加简洁。例如:

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define TRY( X, LABEL ) do { if ( ( X ) ) { fprintf( stderr, "%s:%d: Statement '%s' failed! %d, %s\n", __FILE__, __LINE__, #X, ret, strerror( ret ) ); goto LABEL; } while ( 0 )

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    TRY( ret = foo_init( f ), FAIL );
    TRY( ret = goo_init( g ), FAIL_F );
    TRY( ret = poo_init( p ), FAIL_G );
    TRY( ret = loo_init( l ), FAIL_P );

    assert( 0 == ret );
    goto END;

    /* error handling and return */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

当然,这并不像 C++ 异常 + 析构函数那样优雅。例如,以这种方式在一个函数中嵌套多个错误处理堆栈不是很干净。相反,您可能希望将它们分解为类似处理错误的自包含子函数,像这样显式地初始化 + 终结。

这也只适用于单个函数,并且不会继续向上跳转堆栈,除非更高级别的调用者实现类似的显式错误处理逻辑,而 C++ 异常只会继续向上跳转堆栈,直到找到合适的处理程序。它也不允许您抛出任意类型,而只能抛出错误代码。

以这种方式系统地编码(即 - 具有单个入口和单个出口点)也使得插入无论如何都会执行的前后(“最终”)逻辑变得非常容易。您只需将“最终”逻辑放在 END 标签之后。

于 2018-07-17T19:15:53.973 回答
2

警告:以下内容不是很好,但可以完成工作。

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    unsigned int  id;
    char         *name;
    char         *msg;
} error;

#define _printerr(e, s, ...) fprintf(stderr, "\033[1m\033[37m" "%s:%d: " "\033[1m\033[31m" e ":" "\033[1m\033[37m" " ‘%s_error’ " "\033[0m" s "\n", __FILE__, __LINE__, (*__err)->name, ##__VA_ARGS__)
#define printerr(s, ...) _printerr("error", s, ##__VA_ARGS__)
#define printuncaughterr() _printerr("uncaught error", "%s", (*__err)->msg)

#define _errordef(n, _id) \
error* new_##n##_error_msg(char* msg) { \
    error* self = malloc(sizeof(error)); \
    self->id = _id; \
    self->name = #n; \
    self->msg = msg; \
    return self; \
} \
error* new_##n##_error() { return new_##n##_error_msg(""); }

#define errordef(n) _errordef(n, __COUNTER__ +1)

#define try(try_block, err, err_name, catch_block) { \
    error * err_name = NULL; \
    error ** __err = & err_name; \
    void __try_fn() try_block \
    __try_fn(); \
    void __catch_fn() { \
        if (err_name == NULL) return; \
        unsigned int __##err_name##_id = new_##err##_error()->id; \
        if (__##err_name##_id != 0 && __##err_name##_id != err_name->id) \
            printuncaughterr(); \
        else if (__##err_name##_id != 0 || __##err_name##_id != err_name->id) \
            catch_block \
    } \
    __catch_fn(); \
}

#define throw(e) { *__err = e; return; }

_errordef(any, 0)

用法:

errordef(my_err1)
errordef(my_err2)

try ({
    printf("Helloo\n");
    throw(new_my_err1_error_msg("hiiiii!"));
    printf("This will not be printed!\n");
}, /*catch*/ any, e, {
    printf("My lovely error: %s %s\n", e->name, e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err2_error_msg("my msg!"));
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printerr("%s", e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err1_error());
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printf("Catch %s if you can!\n", e->name);
})

输出:

Helloo
My lovely error: my_err1 hiiiii!

Helloo
/home/naheel/Desktop/aa.c:28: error: ‘my_err2_error’ my msg!

Helloo
/home/naheel/Desktop/aa.c:38: uncaught error: ‘my_err1_error’ 

请记住,这是使用嵌套函数和__COUNTER__. 如果您使用 gcc,您将是安全的。

于 2017-05-21T19:39:54.547 回答
1

Redis 使用 goto 来模拟 try/catch,恕我直言,它非常干净优雅:

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success. */
int rdbSave(char *filename) {
    char tmpfile[256];
    FILE *fp;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
            strerror(errno));
        return REDIS_ERR;
    }

    rioInitWithFile(&rdb,fp);
    if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = REDIS_OK;
    return REDIS_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    return REDIS_ERR;
}
于 2014-12-10T09:45:13.087 回答
0

如果您将 C 与 Win32 一起使用,则可以利用其结构化异常处理 (SEH)来模拟 try/catch。

如果您在不支持setjmp()and的平台上使用 C,请longjmp()查看 pjsip 库的这个异常处理,它确实提供了自己的实现

于 2013-07-22T11:19:45.880 回答
-1

也许不是主要语言(不幸的是),但在 APL 中,有 ⎕EA 操作(代表 Execute Alternate)。

用法:'Y' ⎕EA 'X' 其中 X 和 Y 是作为字符串或函数名称提供的代码片段。

如果 X 遇到错误,将改为执行 Y(通常是错误处理)。

于 2013-04-09T15:28:22.920 回答