在C代码中处理错误

在C库中以一致的方式处理错误时,你认为什么是“最佳实践”?

有两种方法我一直在想:

总是返回错误代码。 一个典型的函数看起来像这样:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize); 

总是提供一个错误指针的方法:

 int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError); 

在使用第一种方法时,可以像这样编写代码,将error handling检查直接放在函数调用上:

 int size; if(getObjectSize(h, &size) != MYAPI_SUCCESS) { // Error handling } 

这看起来比这里的error handling代码更好。

 MYAPIError error; int size; size = getObjectSize(h, &error); if(error != MYAPI_SUCCESS) { // Error handling } 

不过,我认为使用返回值来返回数据会使代码更具可读性。很明显,在第二个示例中写入了sizevariables。

你有什么想法,为什么我应该喜欢任何这些方法,或者混合使用或使用其他的东西? 我不是全局错误状态的粉丝,因为它倾向于使multithreading使用库方式更痛苦。

编辑:关于这一点的C ++具体想法也将是有趣的听到,只要他们不涉及例外,因为它不是我目前的select…

我喜欢错误作为返回值的方式。 如果你正在deviseAPI,你想尽可能无痛地使用你的库,想想这些增加:

  • 将所有可能的错误状态存储在一个typedef的枚举中,并在你的lib中使用它。 不要只是返回整数,甚至更糟糕,混合整数或不同枚举返回代码。

  • 提供将错误转换为人类可读的function。 可以很简单。 只是错误枚举,const char * out。

  • 我知道这个想法使得multithreading使用有点困难,但是如果应用程序员可以设置一个全局的错误callback函数,那将会很好。 这样他们将能够在寻找错误会话中将一个断点放入callback中。

希望它有帮助。

我已经使用了两种方法,他们都适合我。 无论我使用哪一种,我总是试图应用这个原则:

如果唯一可能的错误是编程错误,请不要返回错误代码,请在函数内部使用断言。

validationinput的断言清楚地传达了函数期望的内容,而太多的错误检查会掩盖程序逻辑。 决定如何处理各种错误情况可能会使devise复杂化。 为什么要弄清楚functionX应该如何处理一个空指针呢?如果你可以坚持让程序员从不通过一个空指针?

每当我创build一个库时,我都会使用第一种方法。 使用typedef'ed枚举作为返回码有几个优点。

  • 如果函数返回一个更复杂的输出,如数组,它的长度不需要创build任意结构来返回。

     rc = func(..., int **return_array, size_t *array_length); 
  • 它允许简单,标准化的error handling。

     if ((rc = func(...)) != API_SUCCESS) { /* Error Handling */ } 
  • 它允许在库函数中进行简单的error handling。

     /* Check for valid arguments */ if (NULL == return_array || NULL == array_length) return API_INVALID_ARGS; 
  • 使用typedef的枚举也允许枚举名在debugging器中可见。 这使得debugging更容易,而不需要经常咨询一个头文件。 有一个函数将这个枚举转换成一个string也很有帮助。

不pipe使用什么方法,最重要的问题是一致的。 这适用于函数和参数命名,参数sorting和error handling。

使用setjmp 。

http://en.wikipedia.org/wiki/Setjmp.h

http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

 #include <setjmp.h> #include <stdio.h> jmp_buf x; void f() { longjmp(x,5); // throw 5; } int main() { // output of this program is 5. int i = 0; if ( (i = setjmp(x)) == 0 )// try{ { f(); } // } --> end of try{ else // catch(i){ { switch( i ) { case 1: case 2: default: fprintf( stdout, "error code = %d\n", i); break; } } // } --> end of catch(i){ return 0; } 

 #include <stdio.h> #include <setjmp.h> #define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){ #define CATCH } else { #define ETRY } }while(0) #define THROW longjmp(ex_buf__, 1) int main(int argc, char** argv) { TRY { printf("In Try Statement\n"); THROW; printf("I do not appear\n"); } CATCH { printf("Got Exception!\n"); } ETRY; return 0; } 

我个人更喜欢前一种方法(返回一个错误指示器)。

在必要时,返回结果应该只是表明发生了错误,而使用另一个函数来找出确切的错误。

在你的getSize()例子中,我认为大小必须总是为零或正数,所以返回一个负值的结果可以表示一个错误,就像UNIX系统调用一样。

我想不出任何我使用过的库,后一种方法将错误对象作为指针传入。 stdio等所有去与返回值。

过去我做了很多C编程。 我真的很喜欢错误代码的返回值。 但有几个可能的陷阱:

  • 重复的错误号码,这可以用全局errors.h文件来解决。
  • 忘记检查错误代码,这应该用一个线索和长的debugging时间来解决。 但最后你会学习(或者你会知道别人会做debugging)。

UNIX方法与您的第二个build议非常相似。 返回结果或单个“出错”值。 例如,open会在成功时返回文件描述符,在失败时返回-1。 如果失败,它还会设置errno ,一个外部全局整数来指示发生了哪个故障。

对于它的价值,Cocoa也采取了类似的方法。 许多方法返回BOOL,并且获取一个NSError **参数,以便在失败时设置错误并返回NO。 然后error handling如下所示:

 NSError *error = nil; if ([myThing doThingError: &error] == NO) { // error handling } 

这是你的两个select之间的某处:-)。

CMU的CERT有一组很好的幻灯片 ,并提供了何时使用每种常见C(和C ++)error handling技术的build议。 最好的幻灯片之一就是这个决策树:

错误处理决策树

我会亲自改变这个stream动车的两件事。

首先,我会澄清,有时对象应该使用返回值来指示错误。 如果一个函数只从一个对象中提取数据而不改变对象,那么对象本身的完整性就没有危险,并且使用返回值指出错误更为合适。

其次,在C ++中使用exception并不总是合适的。 例外是好的,因为它们可以减less专用于error handling的源代码数量,它们大多不会影响函数签名,并且它们可以非常灵活地将它们传递给调用堆栈的数据。 另一方面,例外可能不是正确的select,原因如下:

  1. C ++exception有非常特殊的语义。 如果你不想要那些语义,那么C ++exception是一个不好的select。 抛出后必须立即处理exception,并且devise偏向于需要将调用堆栈展开几个级别的情况。

  2. 抛出exception的C ++函数不能被包装为不抛出exception,至less在不付出全部exception成本的情况下也是如此。 返回错误代码的函数可以被打包,抛出C ++exception,使它们更加灵活。 C ++的new通过提供一个不抛出的变体来获得这个权利。

  3. C ++exception相对比较昂贵,但是这个缺点主要是为了明智地使用exception而被夸大了。 程序根本不应该在性能受到关注的代码path上抛出exception。 程序可以报告错误并退出的速度并不重要。

  4. 有时C ++exception不可用。 要么他们真的不能在一个人的C ++实现,或者一个人的代码准则禁止他们。


由于原来的问题是关于一个multithreading的情况下,我认为当地的错误指示器技术( SirDarius的答案中描述了什么)在原来的答案中被低估了。 它是线程安全的,不会强制错误被调用者立即处理,并可以捆绑描述错误的任意数据。 缺点是它必须由一个对象(或者我认为是外部联系的)来维护,而且比返回代码更容易被忽略。

当我编写程序的时候,在初始化的时候,我通常会分离一个线程来处理错误,并初始化一个特殊的错误结构,包括一个锁。 然后,当我检测到一个错误时,通过返回值,我将exception信息input到结构中,并发送一个SIGIO到exception处理线程,然后看看我是否不能继续执行。 如果我不行,我会发送一个SIGURG到exception线程,从而优雅地停止程序。

第一种方法是更好的恕我直言:

  • 这样写function就容易多了。 当你在函数中间发现一个错误时,你只是返回一个错误值。 在第二种方法中,您需要将错误值分配给其中一个参数,然后返回一些内容….但是,您将返回什么 – 您没有正确的值,并且不返回错误值。
  • 它更受欢迎,所以它会更容易理解,维护

我最近也在思考这个问题,并且写了一些C语言的macros,它们使用纯粹的本地返回值来模拟try-catch-finally语义 。 希望你觉得它有用。

这是一个我认为很有趣的方法,同时需要一些纪律。

这假定句柄typesvariables是操作所有API函数的实例。

这个想法是,句柄后面的结构将以前的错误存储为具有必要数据(代码,消息…)的结构,并且向用户提供返回指向该错误对象的指针的函数。 每个操作都会更新指向的对象,以便用户可以检查其状态,而无需调用函数。 与errno模式相反,错误代码不是全局的,只要每个句柄被正确使用,这些方法都是线程安全的。

例:

 MyHandle * h = MyApiCreateHandle(); /* first call checks for pointer nullity, since we cannot retrieve error code on a NULL pointer */ if (h == NULL) return 0; /* from here h is a valid handle */ /* get a pointer to the error struct that will be updated with each call */ MyApiError * err = MyApiGetError(h); MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext"); /* we want to know what can go wrong */ if (err->code != MyApi_ERROR_OK) { fprintf(stderr, "(%d) %s\n", err->code, err->message); MyApiDestroy(h); return 0; } MyApiRecord record; /* here the API could refuse to execute the operation if the previous one yielded an error, and eventually close the file descriptor itself if the error is not recoverable */ MyApiReadFileRecord(h, &record, sizeof(record)); /* we want to know what can go wrong, here using a macro checking for failure */ if (MyApi_FAILED(err)) { fprintf(stderr, "(%d) %s\n", err->code, err->message); MyApiDestroy(h); return 0; } 

返回错误代码是C中error handling的常用方法

但是最近我们也尝试了传出的错误指针方法。

它比退货价值法有一些优点:

  • 您可以使用返回值更有意义的目的。

  • 必须写出错误参数提醒您处理错误或传播它。 (你永远不会忘记检查fclose的返回值,不是吗?)

  • 如果使用错误指针,则可以在调用函数时将其传递。 如果任何function设置它,值不会丢失。

  • 通过在错误variables上设置数据断点,您可以捕捉错误发生的位置。 通过设置一个条件断点,你也可以捕获特定的错误。

  • 无论您是否处理所有错误,都可以更轻松地自动化检查。 代码约定可能会强制你将错误指针调用为err并且它必须是最后一个参数。 所以脚本可以匹配stringerr); 然后检查if if (*err是否在后面,实际上我们做了一个名为CER (check err return)和CEG (check err goto)的macros,所以当我们只想返回的时候不需要input错误,并可以减less视觉混乱。

不是我们代码中的所有函数都有这个传出参数。 这个传出参数的东西是用于你通常会抛出exception的情况。

除了已经说过的之外,在返回错误代码之前,当返回错误时,触发断言或类似的诊断,因为这将使跟踪更容易。 我这样做的方式是在发行版中有一个自定义的断言,但是只有在软件处于诊断模式时才会被触发,并且可以select以静默方式报告日志文件或在屏幕上暂停。

我个人将错误代码以no_error作为负整数返回为零,但它确实会给您带来以下可能的错误

 if (MyFunc()) DoSomething(); 

另一种方法是将失败始终返回为零,并使用LastError()函数提供实际错误的详细信息。

我绝对更喜欢第一个解决scheme:

 int size; if(getObjectSize(h, &size) != MYAPI_SUCCESS) { // Error handling } 

我会稍微修改它,以:

 int size; MYAPIError rc; rc = getObjectSize(h, &size) if ( rc != MYAPI_SUCCESS) { // Error handling } 

另外,即使当前的function范围允许你这样做,你也永远不会知道函数的实现将来会走哪条路,所以我决不会混合合法的返回值和错误。

如果我们已经在谈论error handling,我会build议goto Error; 作为error handling代码,除非可以调用一些undo函数来正确处理error handling。

编辑:如果您只需要访问最后一个错误,并且不在multithreading环境中工作。

你只能返回true / false(或者如果你在C中工作并且不支持boolvariables的话,可以使用某种#define),并且有一个全局的错误缓冲区来保存最后的错误:

 int getObjectSize(MYAPIHandle h, int* returnedSize); MYAPI_ERROR LastError; MYAPI_ERROR* getLastError() {return LastError;}; #define FUNC_SUCCESS 1 #define FUNC_FAIL 0 if(getObjectSize(h, &size) != FUNC_SUCCESS ) { MYAPI_ERROR* error = getLastError(); // error handling } 

第二种方法可以让编译器产生更多的优化代码,因为当一个variables的地址被传递给一个函数时,编译器在随后调用其他函数的时候不能在寄存器中保存它的值。 完成代码通常只在调用之后使用一次,而从调用返回的“真实”数据可能会更频繁地使用

我更喜欢使用以下技术在C中进行error handling:

 struct lnode *insert(char *data, int len, struct lnode *list) { struct lnode *p, *q; uint8_t good; struct { uint8_t alloc_node : 1; uint8_t alloc_str : 1; } cleanup = { 0, 0 }; // allocate node. p = (struct lnode *)malloc(sizeof(struct lnode)); good = cleanup.alloc_node = (p != NULL); // good? then allocate str if (good) { p->str = (char *)malloc(sizeof(char)*len); good = cleanup.alloc_str = (p->str != NULL); } // good? copy data if(good) { memcpy ( p->str, data, len ); } // still good? insert in list if(good) { if(NULL == list) { p->next = NULL; list = p; } else { q = list; while(q->next != NULL && good) { // duplicate found--not good good = (strcmp(q->str,p->str) != 0); q = q->next; } if (good) { p->next = q->next; q->next = p; } } } // not-good? cleanup. if(!good) { if(cleanup.alloc_str) free(p->str); if(cleanup.alloc_node) free(p); } // good? return list or else return NULL return (good ? list : NULL); } 

资料来源: http : //blog.staila.com/?p = 114

你可以做什么,而不是返回你的错误,从而禁止你返回数据与你的函数,使用你的返回types的包装

 typedef struct { enum {SUCCESS, ERROR} status; union { int errCode; MyType value; } ret; } MyTypeWrapper; 

然后,在被调用函数中:

 MyTypeWrapper MYAPIFunction(MYAPIHandle h) { MyTypeWrapper wrapper; // [...] // If there is an error somewhere: wrapper.status = ERROR; wrapper.ret.errCode = MY_ERROR_CODE; // Everything went well: wrapper.status = SUCCESS; wrapper.ret.value = myProcessedData; return wrapper; } 

请注意,使用下面的方法,包装将有MyType的大小加上一个字节(在大多数编译器上),这是非常有利可图的; 当你调用你的函数(在你提交的两个方法中都是returnedSizereturnedError时,你不必在栈上推另一个参数