ANSI C中try/catch的等效实现?

25

我正在处理一些C代码,运行时出现错误,但是我对如何进行适当的try/catch(如C#或C ++中)知之甚少。

例如,在C++中,我只需执行以下操作:

try{
//some stuff
}
catch(...)
{
//handle error
}

但是在ANSI C中我有点迷茫。我尝试了一些在线搜索,但没有足够的信息告诉我如何实现它/所以我想问这里是否有人可以指引我走向正确的方向。

这是我正在处理的代码(相当简单,递归方法),我想用try/catch(或等效的错误处理结构)来包装它。

然而,我的主要问题仅仅是如何在ANSI C中进行try/catch...具体的实现/示例不必是递归的。

void getInfo( int offset, myfile::MyItem * item )
{
    ll::String myOtherInfo = item->getOtherInfo();
    if( myOtherInfo.isNull() )
        myOtherInfo = "";
    ll::String getOne = "";
    myfile::Abc * abc = item->getOrig();
    if( abc != NULL )
    {
        getOne = abc->getOne();
    }
    for( int i = 0 ; i < offset ; i++ )
    {
             printf("found: %d", i);
    }
    if( abc != NULL )
        abc->release();
    int childCount = item->getChildCount();
    offset++;
    for( int i = 0 ; i < childCount ; i++ )
        getInfo( offset, item->getChild(i) );
    item->release();
}

2
这里是一个可能有用的链接:http://www.nicemice.net/cexcept/ - anijhaw
24
这段代码不是C语言,无论是ANSI还是其他版本。C语言没有::作用域运算符。 - Steve Jessop
4
C语言没有异常处理机制,通常使用返回值和错误编号变量来处理错误。顺便说一句,获取一些关于C语言错误处理的详细专家评论将会很有益 :) - Kel
@Steve: 很好的观点。我的回答是针对C语言的,而不是任何形式的类似C的C++语言。 - David Thornley
2
我所知道的唯一可以编译@aiden发布的代码的语言是C++。如果是C++,你可以直接使用try/catch。如果你确实需要编写ANSI C代码,那么看起来你不仅需要按照答案中的建议进行修改,还需要重写你的代码以使其成为有效的C代码。 - jalf
8个回答

26

一般情况下,不建议使用 try/catch 语句。

可以使用 setjmp 和 longjmp 来实现类似于 try/catch 的功能,但是C语言中没有析构函数或堆栈展开等东西,因此无法使用 RAII。你可以使用所谓的“清理栈”(例如Symbian/C++)来近似实现 RAII,但这并不是非常准确的近似,而且需要投入大量工作。

C语言中通常表示错误或失败的方式是返回表示成功状态的值。调用者检查返回值并采取相应措施。例如,可以参考标准C函数:printf、read、open 等函数的写法来指定自己的函数。

在混合编程 C 和 C++ 代码时,必须确保 C++ 异常不会传播到 C 代码中。编写将从 C 调用的 C++ 函数时,务必捕获所有异常。


19
在C语言中,通常表示错误或失败的方式是返回一个被调用者忽略的值。:( - sbi
2
@sbi:在我看来,一些人仍然相信“返回一个值”是C++、Java和C#中的正确方式... :( - paercebal

17

C语言不支持异常处理。

关于解决这个问题的方法,可以参考这里。这个链接介绍了简单的setjmp/longjmp方法,也提供了一种更为复杂的替代方案,并进行了深入讲解。


今天第三次被踩了,有什么特别的原因吗?我觉得这个看起来很有前途啊。 - Steve Townsend
2
+1 是因为那个负评是错误的... 这是问题的完美链接。 - Hogan
@John Dibling - 这是不言而喻的... 我会留意的,也许只是个糟糕的一天。 - Steve Townsend

4

有一个经典的解开 goto 的模式:

FILE *if = fopen(...);
FILE *of = NULL;
if (if == NULL) return; 

of = fopen(...);
if (of == NULL) goto close_if;

/* ...code... */
if (something_is_wrong) goto close_of;

/* ... other code... */

close_of:
  fclose(of);
close_if:
  fclose(if);

return state;

或者你可以通过将“try”代码隔离在另一个函数中的有限方式来进行伪造。

int try_code(type *var_we_must_write, othertype var_we_only_read /*, ... */){
  /* ...code... */
  if (!some_condition) return 1;
  /* ...code... */
  if (!another_condition) return 2;
  /* ...code... */
  if (last_way_to_fail) return 4;
  return 0;
}

void calling_routine(){
  /* ... */
  if (try_code(&x,y/*, other state */) ) {
     /* do your finally here */
  }
 /* ... */
}

但是这两种方法都不完全等同。你必须自己管理所有的资源,直到找到处理程序之前,你没有自动回滚等功能。


哎呀!那看起来对我来说真的很乱。我更喜欢我下面建议的方法。不需要使用goto! - James
“goto” 是在 C 语言中的最佳选择(请原谅双关语)。拥有多个退出标签似乎有些过度,因此更清晰(尽管略微低效)的方法是只使用一个退出标签,并在必要时检查分配了哪些资源。 - jamesdlin
@jamesdlin:多个标签可以为您带来特定的好处:每个标签对应一个可能需要释放的资源。您可以通过释放条件避免它们,但在某些情况下(不是此情况),这需要额外的标志。就我所知,这是一种品味问题。 - dmckee --- ex-moderator kitten
是的,但这会增加一些额外的复杂度,并且在我看来它有点难以维护(因为顺序很重要)。在大多数情况下,不需要额外的标志(和额外的检查);清理函数(不是 fclose)通常如果在虚拟值上调用,则应该是无操作的(例如 NULL)。 - jamesdlin
@jamesdlin:当我进行这种清理工作时(通常是在汇编语言而不是C语言中),我发现使用goto版本更加简洁和简单,特别是在潜在资源总是按照相同顺序分配的情况下。你可以很容易地知道应该跳转到哪个位置,因为在代码的任何给定点上,你都清楚自己拥有哪些资源(希望如此)。 - Steve Jessop

4

我喜欢使用的一种有用的编码风格是以下内容。我不知道它是否有特定的名称,但当我将一些汇编代码反向工程为等效的C代码时,我遇到了它。你会失去一个缩进级别,但对我来说并没有那么大的问题。注意,要警惕会指出无限循环的审核人! :)

int SomeFunction() {
    int err = SUCCESS;

    do {
        err = DoSomethingThatMayFail();
        if (err != SUCCESS) {
            printf("DoSomethingThatMayFail() failed with %d", err);
            break;
        }

        err = DoSomethingElse();
        if (err != SUCCESS) {
            printf("DoSomethingElse() failed with %d", err);
            break;
        }

        // ... call as many functions as needed.

        // If execution gets there everything succeeded!
        return SUCCESS;
    while (false);

    // Something went wrong!
    // Close handles or free memory that may have been allocated successfully.

    return err;
}

1
不错。如果需要有条件地解开东西,您可能需要嵌套实例。 - dmckee --- ex-moderator kitten

4
这是我在C语言中实现的异常处理系统:exceptions4c。它使用宏,基于setjmplongjmp构建,并且完全可移植到ANSI C标准。
这里,你也可以找到我所知道的所有不同实现的列表。

3
您可以在这本书中找到使用longjmp的可能实现: 《C接口和实现:创建可重用软件的技术- David Hanson》 您可以在此处此处找到代码示例,展示了该书中使用的风格:
except.h
/* $Id$ */
#ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED
#include <setjmp.h>
#define T Except_T
typedef struct T {
    const char *reason;
} T;
typedef struct Except_Frame Except_Frame;
struct Except_Frame {
    Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const T *exception;
};
enum { Except_entered=0, Except_raised,
       Except_handled,   Except_finalized };
extern Except_Frame *Except_stack;
extern const Except_T Assert_Failed;
void Except_raise(const T *e, const char *file,int line);
#ifdef WIN32
#include <windows.h>

extern int Except_index;
extern void Except_init(void);
extern void Except_push(Except_Frame *fp);
extern void Except_pop(void);
#endif
#ifdef WIN32
/* $Id$ */
#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)
#define RERAISE Except_raise(Except_frame.exception, \
    Except_frame.file, Except_frame.line)
#define RETURN switch (Except_pop(),0) default: return
#define TRY do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    if (Except_index == -1) \
        Except_init(); \
    Except_push(&Except_frame);  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {
#define EXCEPT(e) \
        if (Except_flag == Except_entered) Except_pop(); \
    } else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;
#define ELSE \
        if (Except_flag == Except_entered) Except_pop(); \
    } else { \
        Except_flag = Except_handled;
#define FINALLY \
        if (Except_flag == Except_entered) Except_pop(); \
    } { \
        if (Except_flag == Except_entered) \
            Except_flag = Except_finalized;
#define END_TRY \
        if (Except_flag == Except_entered) Except_pop(); \
        } if (Except_flag == Except_raised) RERAISE; \
} while (0)
#else
#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)
#define RERAISE Except_raise(Except_frame.exception, \
    Except_frame.file, Except_frame.line)
#define RETURN switch (Except_stack = Except_stack->prev,0) default: return
#define TRY do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {
#define EXCEPT(e) \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;
#define ELSE \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } else { \
        Except_flag = Except_handled;
#define FINALLY \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } { \
        if (Except_flag == Except_entered) \
            Except_flag = Except_finalized;
#define END_TRY \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } if (Except_flag == Except_raised) RERAISE; \
} while (0)
#endif
#undef T
#endif

except.c

static char rcsid[] = "$Id$" "\n$Id$";
#include <stdlib.h>
#include <stdio.h>
#include "assert.h"
#include "except.h"
#define T Except_T
Except_Frame *Except_stack = NULL;
void Except_raise(const T *e, const char *file,
    int line) {
#ifdef WIN32
    Except_Frame *p;

    if (Except_index == -1)
        Except_init();
    p = TlsGetValue(Except_index);
#else
    Except_Frame *p = Except_stack;
#endif
    assert(e);
    if (p == NULL) {
        fprintf(stderr, "Uncaught exception");
        if (e->reason)
            fprintf(stderr, " %s", e->reason);
        else
            fprintf(stderr, " at 0x%p", e);
        if (file && line > 0)
            fprintf(stderr, " raised at %s:%d\n", file, line);
        fprintf(stderr, "aborting...\n");
        fflush(stderr);
        abort();
    }
    p->exception = e;
    p->file = file;
    p->line = line;
#ifdef WIN32
    Except_pop();
#else
    Except_stack = Except_stack->prev;
#endif
    longjmp(p->env, Except_raised);
}
#ifdef WIN32
_CRTIMP void __cdecl _assert(void *, void *, unsigned);
#undef assert
#define assert(e) ((e) || (_assert(#e, __FILE__, __LINE__), 0))

int Except_index = -1;
void Except_init(void) {
    BOOL cond;

    Except_index = TlsAlloc();
    assert(Except_index != TLS_OUT_OF_INDEXES);
    cond = TlsSetValue(Except_index, NULL);
    assert(cond == TRUE);
}

void Except_push(Except_Frame *fp) {
    BOOL cond;

    fp->prev = TlsGetValue(Except_index);
    cond = TlsSetValue(Except_index, fp);
    assert(cond == TRUE);
}

void Except_pop(void) {
    BOOL cond;
    Except_Frame *tos = TlsGetValue(Except_index);

    cond = TlsSetValue(Except_index, tos->prev);
    assert(cond == TRUE);
}
#endif

1
如果你想要进行多级跳转,请查阅 setjmp()longjmp()。它们可以被用作原始的异常抛出。函数 setjmp() 设置一个返回位置,并返回一个状态值。函数 longjmp() 跳转到返回位置,并提供状态值。你可以通过在 setjmp() 之后调用一个函数来创建一个 catch 函数,具体取决于状态值。
不要无论出于什么原因,在 C++ 中使用它们。它们不会进行堆栈展开或调用析构函数。

0

由于C++最初是作为C预处理器实现的,并且它具有Try/Catch,因此您可以重新执行Bjarne Stroustrup的工作并编写一个预处理器来完成它。


1
当C++引入异常机制时,C预处理器的实现已经成为历史。 - Nemanja Trifunovic
@Nemanja:对于原始的cfront确实如此。然而,Comeau C++仍然输出C代码(因为这样可以实现高度可移植性)。但我怀疑这比其他编译器的汇编输出更有用:它是机器生成的代码,因此不适合人类消费。 - sbi
1
我认为异常是压垮骆驼的最后一根稻草,而这只骆驼就是cfront C++编译器。请参见Stroupstrup的《C++的设计与演化》。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接