尝试使用 C 语言中的 catch 语句

我今天想到的是另一种语言中存在的 try/catch 块。在谷歌上搜索了一会儿,但没有结果。据我所知,在 C 语言中没有 try/catch 这种东西。但是,有没有一种方法可以“模拟”它们?
当然,也有一些断言和其他技巧,但是没有像 try/catch 那样能够捕获提出的异常

317723 次浏览

在 C 中使用 译自: 美国《科学》杂志网站(http://www.roseindia.net/c-Tutorials/c-goto-statement.shtml)处理类似的错误处理情况。
这是在 C 语言中可以得到的最接近于异常的等价物。

在 C99中, 可以对非局部控制流使用 setjmp/longjmp

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

一个快速的谷歌搜索可以产生诸如 这个这样使用 setjmp/longjmp 的组合式解决方案,正如其他人所提到的那样。没有比 C + +/Java 的 try/catch 更简单和优雅的了。我比较喜欢艾达处理自己的例外。

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

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

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模拟异常的很好的教程

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

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

如果 X 运行到一个错误,Y (通常是错误处理)将被执行。

如果您在 Win32中使用 C,您可以利用它的 结构化异常处理(SEH)来模拟 try/catch。

如果您在不支持 setjmp()longjmp()的平台中使用 C,那么可以看一下 pjsip 库的 异常处理,它提供了自己的实现

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

  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 来增长缓冲区,或类似的东西)。

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

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

好吧,我忍不住回复了这个。首先,我不认为用 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 块,这个 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 是什么。 我们是 使用滥用堆栈,所以子函数也可以有一个 try/catch 块。

使用这段代码有很多缺点(但是是一个有趣的思维练习) :

  • 它不会释放已分配的内存,因为没有调用解构器。
  • 作用域中不能有多于1个 try/catch (不能嵌套)
  • 实际上不能像 C + + 那样抛出异常或其他数据
  • 一点都不安全
  • 您正在将其他程序员设置为失败,因为他们可能不会注意到黑客攻击,并尝试像使用 C + + try/catch 块那样使用它们。

警告: 下面的内容不是很好,但它确实起到了作用。

#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 的话,你会比较安全。

这是在 C 中执行错误处理的另一种方法,它比使用 setjmp/longjmp 性能更好。不幸的是,它不能与 MSVC 一起工作,但是如果只使用 GCC/Clang 是一个选项,那么您可以考虑使用它。具体来说,它使用了“ label as value”扩展,这允许您获取标签的地址,将其存储在一个值中,然后无条件地跳转到它。我将用一个例子来说明:

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

在 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 + + 异常只会一直跳转到堆栈中,直到找到合适的处理程序。它也不允许您抛出任意类型,而只允许抛出错误代码。

以这种方式进行系统编码(例如,使用单个进入点和单个退出点)还可以非常容易地插入不管如何都会执行的 pre 和 post (“ finally”)逻辑。您只需将您的“ finally”逻辑放在 END 标签之后。

在研究了上面给出的答案之后,我建立了一个能够很好地自动处理嵌套异常的系统。下面是我为测试系统而编写的代码:

#include "MyOtherTricks.h"
#include "Exceptions.h"


void Testing_InnerMethod();
void Testing_PossibleExceptionThrower();


void TestExceptionHandling()
{
try
{
Testing_InnerMethod();
Say("The inner method exited without an exception.");
}
catch (Exception)
{
Say("I caught an Exception that the inner method did not catch.");
}
end_try
}


void Testing_InnerMethod()
{
try
{
Say("I am in a try block.");
Testing_PossibleExceptionThrower();
Say("The possible exception thrower didn't throw an exception.");
}
catch (ExceptionSubtype1)
Say("I caught an exception, subtype 1.");
catch (ExceptionSubtype2)
{
Say("I caught an exception, subtype 2.");
Say("I will now rethrow it.");
throw(exception);
}
end_try
}


void Testing_PossibleExceptionThrower()
{
Say("Here is the possible exception thrower.");
throw(new(ExceptionSubtype2));                          // To further test exception handling, replace ExceptionSubtype2 in this line with Exception or ExceptionSubtype1, or comment out this line entirely.
Say("No, I won't throw an exception!");
}

示例代码依赖于两个文件 Exceptions.h 和 Exceptions.c:

#include <setjmp.h>


extern jmp_buf* Exception_Handler;


#define try                     do                                                                  \
{                                                                   \
jmp_buf* outerExceptionHandler = Exception_Handler;             \
jmp_buf exceptionHandler;                                       \
Exception_Handler = &exceptionHandler;                          \
Exception exception = (Exception)setjmp(exceptionHandler);      \
if (exception != 0) Exception_Handler = outerExceptionHandler;  \
if (exception == 0)                                             \
{                                                               \
// The try block goes here. It must not include a return statement or anything else that exits the try...end_try block, because then the outer exception handler will not be restored.
#define catch(exceptionType)            Exception_Handler = outerExceptionHandler;                  \
}                                                               \
else if (Object_IsSomeTypeOf(exception, exceptionType))         \
{
// The catch block goes here. It may include a return statement or anything else that exits the try...end_try block. A break statement will exit only the try...end_try block.
#define end_try                     }                                                               \
else                                                            \
throw(exception);                                           \
} while(0);


void throw(Exception exception);

这是例外。 c:

#include "MyOtherTricks.h"
#include "Exceptions.h"


jmp_buf* Exception_Handler = 0;


void throw(Exception exception)
{
if (Exception_Handler == 0) FailBecause("Uncaught exception.");
longjmp(*Exception_Handler, (int)exception);
}

注意,这段代码引用了一些额外的方法,这里没有包括这些方法(因为 C 中的类继承与主题无关)。要使这段代码为您工作,您必须充分理解这段代码,以便替换一些内容。特别是,如果你想区分不同类型的异常,你需要意识到这段代码假设 Object _ IsSome TypeOf (new (ExceptionSubtype1) ,Exception)返回 true,Object _ IsSome TypeOf (new (ExceptionSubtype1) ,ExceptionSubtype2)返回 false,你需要创建你自己版本的 Object _ IsSome TypeOf 宏,或者用别的东西替换它。