尝试用C语言中的catch语句

时间:2012-05-14 15:07:03

标签: c

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

13 个答案:

答案 0 :(得分:80)

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

本网站有一个很好的教程,介绍如何使用setjmplongjmp来模拟例外

答案 1 :(得分:23)

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

答案 2 :(得分:10)

在C99,中,您可以使用setjmp / longjmp进行非本地控制流程。

在单个范围内,存在多个资源分配和多个出口的C的通用结构化编码模式使用goto,如in this example。这类似于C ++如何在引擎盖下实现自动对象的析构函数调用,如果你努力坚持这一点,即使在复杂的函数中它也应该允许你一定程度的清洁。

答案 3 :(得分:8)

好的,我无法拒绝回复此事。首先我要说我不认为在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块。然后试试块 使用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次尝试/捕获(无嵌套)
  • 您实际上无法像C ++中那样抛出异常或其他数据
  • 根本不是线程安全的
  • 你正在设置其他程序员失败,因为他们可能不会注意到hack并尝试使用它们,如C ++ try / catch块。

答案 4 :(得分:5)

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

  1. 嵌套try / catch块。对jmp_buf使用单个全局变量会使这些变量不起作用。
  2. 线程。在这种情况下,jmp_buf的单个全局变量会引起各种各样的痛苦。
  3. 这些解决方案是维护一个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来增加缓冲区,或类似的东西)。< / p>

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

答案 5 :(得分:4)

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

答案 6 :(得分:3)

快速谷歌搜索会产生像其他人提到的那样使用setjmp / longjmp的this等kludgey解决方案。没有像C ++ / Java的try / catch那样简单和优雅。我非常偏爱阿达的异常处理。

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

答案 7 :(得分: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,那么您将处于安全的一面。

答案 8 :(得分: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;
}

答案 9 :(得分: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;
}

答案 10 :(得分: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标签后面即可。

答案 11 :(得分:0)

如果您在Win32中使用C,则可以利用其Structured Exception Handling (SEH)来模拟try / catch。

如果您在不支持setjmp()longjmp()的平台上使用C,请查看此Exception Handling pjsip库,它确实提供了自己的实施

答案 12 :(得分:-1)

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

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

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