C代码中的错误处理

时间:2008-12-22 10:46:04

标签: c error-handling

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

我一直在考虑两种方式:

始终返回错误代码。典型的功能如下所示:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

始终提供错误指针方法:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

当使用第一种方法时,可以编写这样的代码,其中错误处理检查直接放在函数调用上:

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

这看起来比错误处理代码更好。

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

但是,我认为使用返回值返回数据会使代码更具可读性。很明显,在第二个示例中,某些内容被写入了size变量。

您对我为什么更喜欢这些方法或者混合使用或使用其他方法有什么想法吗?我不是全局错误状态的粉丝,因为它往往会使库的多线程使用更加痛苦。

编辑: 关于这一点的C ++具体想法也很有意思,只要它们不涉及异常,因为它现在不是我的选择......

23 个答案:

答案 0 :(得分:87)

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

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

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

答案 1 :(得分:65)

我喜欢将错误视为返回值方式。如果您正在设计api,并且想尽可能轻松地使用您的库,请考虑这些新增内容:

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

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

  • 我知道这个想法会让多线程使用变得有点困难,但如果应用程序员可以设置全局错误回调那就太好了。这样他们就可以在追捕错误的会话期间将断点放入回调中。

希望它有所帮助。

答案 2 :(得分:18)

来自CMU的CERT有一个很好的set of slides,建议何时使用每种常见的C(和C ++)错误处理技术。最佳幻灯片之一就是这个决策树:

Error Handling Decision Tree

我个人会改变关于这款跑车的两件事。

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

其次,总是适合在C ++中使用异常。异常是好的,因为它们可以减少用于错误处理的源代码量,它们通常不会影响函数签名,并且它们可以非常灵活地传递callstack的数据。另一方面,由于以下几个原因,异常可能不是正确的选择:

  1. C ++异常具有非常特殊的语义。如果你不想要那些语义,那么C ++异常是一个糟糕的选择。抛出后必须立即处理异常,并且设计支持错误需要将callstack解除几个级别的情况。

  2. 抛出异常的C ++函数以后不会被包装成不抛出异常,至少在没有支付异常的全部代价的情况下也是如此。可以包含返回错误代码的函数以抛出C ++异常,从而使它们更加灵活。 C ++的new通过提供非投掷变体来实现这一目标。

  3. C ++异常相对较为昂贵,但这种缺点主要是对于合理使用异常的程序而言过于夸张。程序根本不应该在性能受到关注的代码路径上抛出异常。程序报告错误并退出的速度并不重要。

  4. 有时C ++异常不可用。要么它们在一个人的C ++实现中根本不可用,要么一个代码指南禁止它们。

  5. 由于最初的问题是关于多线程的上下文,我认为本地错误指示器技术(SirDariusanswer中描述的内容)在原始答案中被低估了。它是线程安全的,不会强制错误被调用者立即处理,并且可以捆绑描述错误的任意数据。缺点是它必须由一个对象持有(或者我想以某种方式与外部相关联)并且可以说比返回代码更容易被忽略。

答案 3 :(得分:16)

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

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

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

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

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
    
  • 使用typedef'ed枚举还允许枚举名在调试器中可见。这样可以更轻松地进行调试,而无需经常查阅头文件。具有将此枚举转换为字符串的功能也很有用。

无论使用何种方法,最重要的问题是保持一致。这适用于函数和参数命名,参数排序和错误处理。

答案 4 :(得分:9)

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

答案 5 :(得分:7)

当我编写程序时,在初始化期间,我通常会分离一个线程以进行错误处理,并初始化一个特殊的错误结构,包括锁定。然后,当我通过返回值检测到错误时,我将异常的信息输入到结构中并将SIGIO发送到异常处理线程,然后查看是否无法继续执行。如果我不能,我将一个SIGURG发送到异常线程,它会正常地停止程序。

答案 6 :(得分:7)

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

如果需要,返回结果应该只表明发生了错误,并使用另一个函数来找出确切的错误。

在你的getSize()示例中,我认为大小必须始终为零或正数,因此返回否定结果可能表示错误,就像UNIX系统调用一样。

我想不出我使用过的任何库,后者使用作为指针传入的错误对象。 stdio等都带有返回值。

答案 7 :(得分:6)

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

  • 重复错误号,可以使用全局errors.h文件解决。
  • 忘记检查错误代码,这应该通过cluebat和长时间的调试来解决。但最后你会学到(或者你会知道别人会做调试)。

答案 8 :(得分:6)

返回错误代码是C中错误处理的常用方法。

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

它比返回值方法有一些优势:

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

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

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

  • 通过在错误变量上设置数据断点,您可以先捕获错误发生的位置。通过设置条件断点,您也可以捕获特定的错误。

  • 它可以更轻松地自动检查您是否处理所有错误。代码约定可能会强制您将错误指针调用为err,并且它必须是最后一个参数。因此,脚本可以匹配字符串err);,然后检查它是否后跟if (*err。实际上在实践中我们创建了一个名为CER的宏(检查错误返回)和CEG(检查错误转到)。因此,当我们只是想要返回错误时,您不需要输出它,并且可以减少视觉混乱。

虽然我们代码中的所有函数都没有这个传出参数。 此传出参数用于通常抛出异常的情况。

答案 9 :(得分:6)

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

对于它的价值,Cocoa也采用了类似的方法。许多方法返回BOOL,并采用NSError **参数,因此失败时它们会设置错误并返回NO。然后错误处理如下:

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

介于两个选项之间: - )。

答案 10 :(得分:5)

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

这假设一个句柄类型变量是操作所有API函数的实例。

这个想法是句柄后面的结构将前一个错误存储为具有必要数据(代码,消息......)的结构,并且为用户提供了一个返回指针tp这个错误对象的函数。每个操作都将更新指向的对象,以便用户无需调用函数即可检查其状态。与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;
}

答案 11 :(得分:4)

我最近也在考虑这个问题,并使用纯粹的本地返回值编写了some macros for C that simulate try-catch-finally semantics。希望你觉得它很有用。

答案 12 :(得分:4)

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

  • 以这种方式编写函数更容易。当您在函数中间发现错误时,只返回错误值。在第二种方法中,您需要将错误值分配给其中一个参数然后返回一些内容....但是您会返回什么 - 您没有正确的值而您没有返回错误值。
  • 它更受欢迎,因此更容易理解,维护

答案 13 :(得分:3)

我绝对更喜欢第一种解决方案:

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

我会略微修改它,以:

int size;
MYAPIError rc;

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

另外,我永远不会将合法的返回值与错误混合,即使当前允许您这样做的功能范围,您也永远不知道将来函数实现的方式。

如果我们已经在讨论错误处理,我会建议将goto Error;作为错误处理代码,除非可以调用某些undo函数来正确处理错误处理。

答案 14 :(得分:3)

我多次遇到这个Q&amp; A,想要提供更全面的答案。我认为考虑这个问题的最佳方法是如何将错误返回给调用者,以及你返回的

如何

有三种方法可以从函数中返回信息:

  1. 返回值
  2. Out Argument(s)
  3. 带外,包括非本地goto(setjmp / longjmp), 文件或全局范围变量,文件系统等。
  4. 返回值

    您只能返回值是单个对象,但是,它可以是任意复杂的。以下是错误返回功能的示例:

      enum error hold_my_beer();
    

    返回值的一个好处是它允许链接调用以减少侵入性错误处理:

      !hold_my_beer() &&
      !hold_my_cigarette() &&
      !hold_my_pants() ||
      abort();
    

    这不仅仅是关于可读性,还可能允许以统一的方式处理这些函数指针的数组。

    Out Argument(s)

    您可以通过参数通过多个对象返回更多内容,但最佳做法确实建议保持参数总数较低(例如,&lt; = 4):

    void look_ma(enum error *e, char *what_broke);
    
    enum error e;
    look_ma(e);
    if(e == FURNITURE) {
      reorder(what_broke);
    } else if(e == SELF) {
      tell_doctor(what_broke);
    }
    

    带外

    使用setjmp()可以定义一个位置以及如何处理int值,并通过longjmp()将控制权转移到该位置。请参阅Practical usage of setjmp and longjmp in C

    什么

    1. 指示器
    2. 对象
    3. 回调
    4. 指示器

      错误指示器仅告诉您存在问题,但没有关于所述问题的性质:

      struct foo *f = foo_init();
      if(!f) {
        /// handle the absence of foo
      }
      

      这是函数传递错误状态的最不强大的方法,但是,如果调用者无法以渐进的方式响应错误,则这是完美的。

      代码

      错误代码告诉调用者问题的性质,并且可以允许适当的响应(来自上面)。它可以是返回值,或者像错误参数上面的look_ma()示例一样。

      对象

      使用错误对象,可以向调用者通知任意复杂问题。例如,错误代码和合适的人类可读消息。它还可以通知调用者多处出错,或处理集合时每个项目出错:

      struct collection friends;
      enum error *e = malloc(c.size * sizeof(enum error));
      ...
      ask_for_favor(friends, reason);
      for(int i = 0; i < c.size; i++) {
         if(reason[i] == NOT_FOUND) find(friends[i]);
      }
      

      您可以根据需要(重新)动态分配错误数组,而不是预先分配错误数组。

      回调

      回调是处理错误的最有效方法,因为您可以告诉函数在出现问题时您希望看到的行为。可以为每个函数添加一个回调参数,或者如果每个结构的实例只需要自定义,则需要:

       struct foo {
          ...
          void (error_handler)(char *);
       };
      
       void default_error_handler(char *message) { 
          assert(f);
          printf("%s", message);
       }
      
       void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
          assert(f);
          f->error_handler = eh;
       }
      
       struct foo *foo_init() {
          struct foo *f = malloc(sizeof(struct foo));
          foo_set_error_handler(f, default_error_handler);
          return f;
       }
      
      
       struct foo *f = foo_init();
       foo_something();
      

      回调的一个有趣的好处是它可以多次调用,或者在没有错误的情况下根本不调用,其中没有开心路径上没有开销。

      然而,有一种控制倒置。调用代码不知道是否调用了回调。因此,也可以使用指标。

答案 15 :(得分:2)

除了已经说过的内容之外,在返回错误代码之前,在返回错误时触发断言或类似的诊断,因为它会使跟踪变得更容易。我这样做的方法是让一个自定义的断言仍然在发布时编译,但只在软件处于诊断模式时被触发,并且可以选择静默报告日志文件或在屏幕上暂停。

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

if (MyFunc())
 DoSomething();

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

答案 16 :(得分:2)

您可以做的不是返回错误,因此禁止您使用函数返回数据,而是使用包装作为返回类型:

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)时,你不必在堆栈上推送另一个参数

答案 17 :(得分:1)

第二种方法允许编译器生成更优化的代码,因为当变量的地址传递给函数时,编译器在后续调用其他函数期间不能将其值保存在寄存器中。完成代码通常仅在调用之后使用一次,而从调用返回的“真实”数据可能会更频繁地使用

答案 18 :(得分:1)

编辑:如果您只需要访问上一个错误,并且您在多线程环境中无法工作。

您只能返回true / false(如果您在C中工作并且不支持bool变量,则返回某种#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
}

答案 19 :(得分:1)

我更喜欢使用以下技术在C中进行错误处理:

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

答案 20 :(得分:1)

这是一个简单的程序,用于演示Nils Pipenbrinck's answer here的前2个项目符号。

他的前2个子弹是:

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

  •   
  • 提供了将错误转换为人类可读的功能。可以很简单。只是错误枚举,const char *退出。

  •   

假设您已经编写了一个名为mymodule的模块。 首先,在mymodule.h中,定义基于枚举的错误代码,并编写一些与这些代码相对应的错误字符串。在这里,我使用的是C字符串数组(char * ),只有当您的第一个基于枚举的错误代码的值为0,并且此后不对数字进行操作时,该方法才有效。 如果您确实使用带有空格或其他起始值的错误代码号,则只需从使用映射的C字符串数组(如下所述)更改为使用带有switch语句的函数,或者/ else if语句从枚举错误代码映射到可打印的C字符串(我不演示)。选择是您的。

mymodule.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,

    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc

    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c包含我的映射函数,用于从枚举错误代码映射到可打印的C字符串:

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c包含一个测试程序,以演示如何调用某些函数并从中打印一些错误代码:

main.c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

输出:

  

在C(或C ++)中基于枚举的错误代码的演示
  来自mymodule_func1()的错误代码= MYMODULE_ERROR_OK
  来自mymodule_func2()的错误代码= MYMODULE_ERROR_INVARG
  来自mymodule_func3()= MYMODULE_ERROR_MYERROR的错误代码

参考文献:

您可以在此处自己运行以下代码:https://onlinegdb.com/ByEbKLupS

答案 21 :(得分:0)

另外还有其他很好的答案,我建议您尝试将错误标记和错误代码分开,以便在每次调用时保存一行,即:

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

当你进行大量的错误检查时,这种小小的简化确实有帮助。

答案 22 :(得分:-3)

如果你想让你的程序崩溃而不知道原因,那就去相信程序员和基本的错误处理。

我认为最好内置某种错误报告,将其称为调试模式,在您想要最佳性能时将其关闭,并在您想要调试问题时将其打开。希望你能再打一次。

会有bug,问题是你想如何日日夜夜寻找它们。