我今天在想其他语言中存在的try / catch块。用谷歌搜索了一段时间,但没有结果。据我所知,在C语言中没有try / catch这样的东西。但是,有没有一种方法可以“模拟”它们?
当然,有一些断言和其他技巧,但没有其他类似try / catch的技巧也可以捕获引发的异常。谢谢
我今天在想其他语言中存在的try / catch块。用谷歌搜索了一段时间,但没有结果。据我所知,在C语言中没有try / catch这样的东西。但是,有没有一种方法可以“模拟”它们?
当然,有一些断言和其他技巧,但没有其他类似try / catch的技巧也可以捕获引发的异常。谢谢
Answers:
C本身不支持异常,但是您可以使用setjmp
and 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
try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')};
将无法正常工作?
您可以在C中使用goto处理类似的错误处理情况。
那是您可以在C语言中获得的最接近的例外。
goto
更多地用于错误处理,但是那又如何呢?问题不在于错误处理本身,而是与尝试/捕获等效项有关。goto
它不限于尝试/捕获,因为它仅限于同一功能。
好的,我忍不住要回复这个。首先让我说,我认为用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块。
使用此代码有很多弊端(但这是一项有趣的心理锻炼):
bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}
。但是局部变量也将被重新定义,因此事情变得有些失控。
尽管其他一些答案已经涵盖了使用setjmp
和的简单情况longjmp
,但在实际应用中,有两个问题确实很重要。
jmp_buf
将使它们不起作用。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来增加缓冲区,或者类似的东西)。
免责声明:以上代码未经任何测试就被编写。纯粹是为了让您了解如何构造事物。不同的系统和不同的编译器将需要以不同的方式实现线程本地存储。该代码可能同时包含编译错误和逻辑错误-因此,尽管您可以随意选择使用它,但请在使用它之前进行测试;)
这是在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;
}
警告:以下内容不是很好,但确实可以。
#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,那将是安全的一面。
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
只能在失败的系统调用之后使用,而不能在以后的三个调用中使用。
在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标签之后。
如果在Win32上使用C,则可以利用其结构化异常处理(SEH)来模拟try / catch。
如果您在不支持setjmp()
和的平台上使用C,longjmp()
请看一下pjsip库的异常处理,它确实提供了自己的实现
也许不是主要的语言(不幸的是),但是在APL中,有⎕EA操作(代表执行替代)。
用法:'Y'⎕EA'X'其中X和Y是作为字符串或函数名称提供的代码段。
如果X发生错误,则将执行Y(通常是错误处理)。