在C中尝试catch语句


101

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


3
如果没有一种在堆栈解卷时自动释放资源的机制,类似异常的机制通常不会有用。C ++使用RAII;Java,C#,Python等使用垃圾收集器。(请注意,垃圾收集器仅释放内存。要自动释放其他类型的资源,它们还添加了终结器或上下文管理器之类的东西……)
jamesdlin

@jamesdlin,为什么我们不能用C做RAII?
Pacerier

1
@Pacerier RAII需要在销毁对象(即析构函数)时自动调用函数。您如何建议在C语言中这样做?
jamesdlin

Answers:


90

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

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


1
很棒的解决方案!这个解决方案交叉吗?它在MSVC2012上对我有用,但在MacOSX Clang编译器中不起作用。
mannysz

1
提示我:我认为try catch子句允许您捕获异常(例如除以零)。该功能似乎只允许您捕获自己抛出的异常。调用longjmp不会抛出真正的异常吗?如果我使用此代码执行类似操作try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; 将无法正常工作?
山姆

零偏差甚至不是C ++中的异常,要处理该问题,您需要检查除数是否为零并处理它,或处理运行零偏差公式时抛出的SIGFPE。
詹姆斯

25

您可以在C中使用goto处理类似的错误处理情况。
那是您可以在C语言中获得的最接近的例外。


3
@JensGustedt这正是goto当前经常使用的示例,并且是有意义的示例(setjmp / ljmp是更好的替代方法,但通常使用label + goto 更多)。
Tomas Pruzina 2013年

1
@AoeAoe,可能goto更多地用于错误处理,但是那又如何呢?问题不在于错误处理本身,而是与尝试/捕获等效项有关。goto它不限于尝试/捕获,因为它仅限于同一功能。
詹斯·古斯特

@JensGustedt我对goto的仇恨和恐惧以及使用它的人都做出了反应(我的老师也告诉我在大学使用goto的可怕故事)。[OT]关于goto的唯一真正,真正有风险和“阴天”的事情是“向后退”,但是我已经在Linux VFS中看到过(git怪人发誓这对性能至关重要)。
Tomas Pruzina

请参阅systemctl源,以goto作为在现代,广为接受的,同行评审的源中使用的try / catch机制的合法使用。搜索goto“投掷”等效项和finish“捕获”等效项。
斯图尔特

13

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

我们可以滥用预处理和局部堆栈变量给使用C ++的try /投/捕获的有限版本。

版本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块。

使用此代码有很多弊端(但这是一项有趣的心理锻炼):

  • 因为没有解构函数被调用,它不会释放分配的内存。
  • 一个作用域中的try / catch不能超过1个(无嵌套)
  • 您实际上不能像在C ++中那样引发异常或其他数据
  • 根本不是线程安全的
  • 您正在设置其他程序员失败,因为他们可能不会注意到黑客并尝试像C ++ try / catch块一样使用它们。

不错的替代解决方案。
HaseeB Mir

版本1是个不错的主意,但是__HadError变量将需要重置或确定范围。否则,您将无法在同一块中使用多个try-catch。也许使用像这样的全局函数bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}。但是局部变量也将被重新定义,因此事情变得有些失控。
flamewave000

是的,它仅限于同一功能中的一个try-catch。然而,更大的问题是变量,因为您在同一函数中不能有重复的标签,所以变量是标签。
Paul Hutchinson

10

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

在一个范围内,存在多个资源分配和多个出口的情况下,C的通用结构化编码模式就可以使用goto,如本例所示。这类似于C ++在后台实现自动对象的析构函数调用的方式,并且如果您坚持这一点,即使在复杂的函数中,它也应允许您达到一定程度的简洁。


5

尽管其他一些答案已经涵盖了使用setjmp和的简单情况longjmp,但在实际应用中,有两个问题确实很重要。

  1. 尝试/捕获块的嵌套。为您使用一个全局变量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来增加缓冲区,或者类似的东西)。

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


4

快速谷歌搜索产量kludgey解决方案,如是使用了setjmp / longjmp的如其他人所说的。没有C ++ / Java的try / catch这么简单明了。我比较喜欢Ada自己处理的异常。

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


4

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


2

这是在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;
}

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,那将是安全的一面。


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;
}

代码已损坏。errno只能在失败的系统调用之后使用,而不能在以后的三个调用中使用。
ceving 2015年

这段代码在多个地方重复了错误处理逻辑,并且可能会做不正确的事情,例如多次调用fclose(fp)。最好使用多个标签并使用这些标签对仍需要回收的内容进行编码(而不是仅对所有错误使用一个标签),然后根据错误发生在代码中的位置跳到正确的错误处理位置。
jschultz410

1

在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 '" #X "' failed! %d, %s\n", __FILE__, __LINE__, 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标签之后。


1
非常好。我倾向于做类似的事情。goto非常适合这种情况。唯一的区别是我看不到最后一个“ goto END”的必要,我只是在那一点插入成功返回值,其余插入一个失败返回值。
尼尔·罗伊,

1
谢谢@NeilRoy转到END的原因是,我希望绝大多数函数具有一个入口点和一个出口点。这样,如果我想向任何函数添加一些“最终”逻辑,我便总是可以轻松地进行操作,而不必担心某些其他隐藏的收益潜伏在某个地方。:)
jschultz410 '19


-1

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

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

如果X发生错误,则将执行Y(通常是错误处理)。


2
嗨,mappo,欢迎来到StackOverflow。尽管很有趣,但问题特别是关于在C语言中执行此操作的问题。
luser droog
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.