C 代码中的错误处理

当在 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 + + 在这方面的具体想法也很有趣,只要他们不涉及异常,因为这不是我目前的一个选项..。

131770 次浏览

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

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

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

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

我以前做过很多 C 语言编程。我真的很欣赏错误代码返回值。但它也有几个可能的缺陷:

  • 重复的错误编号,这可以通过一个全局 errors.h 文件来解决。
  • 忘记检查错误代码,这个问题应该通过一个俱乐部和长时间的调试来解决。但是最终您将学习(或者您将知道其他人将进行调试)。

我喜欢这个错误作为返回值的方式。如果您正在设计 API,并且希望尽可能轻松地使用库,请考虑以下添加内容:

  • 将所有可能的错误状态存储在一个 typedef 的枚举中,并在库中使用它。不要仅仅返回 int 或者更糟糕的,用返回代码混合 int 或者不同的枚举。

  • 提供一个函数,将错误转换为人类可读的内容。可以很简单。只需输入错误枚举,输出常量字符。

  • 我知道这个想法使得多线程使用有点困难,但是如果应用程序员能够设置一个全局错误回调就更好了。这样,他们就可以在错误搜索会话期间将断点放入回调中。

希望能有帮助。

我当然更喜欢第一种解决方案:

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函数来正确处理错误处理。

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

  • 这样写函数更容易。当您注意到函数中间有错误时,您只需返回一个错误值。在第二种方法中,您需要将错误值赋给其中一个参数,然后返回一些... ..。但是你会返回什么-你没有正确的值,也不会返回错误值。
  • 它更受欢迎,所以更容易理解和维护

UNIX 方法与您的第二个建议最为相似。返回结果或单个“ it went wrong”值。例如,open 将在成功时返回文件描述符,失败时返回 -1。出现故障时,它还设置 errno,这是一个外部全局整数,表明发生了 哪个故障。

值得一提的是,Cocoa 也采用了类似的方法。许多方法返回 BOOL,并接受 NSError **参数,因此在失败时,它们设置错误并返回 NO。然后错误处理看起来像:

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

这是介于你的两个选择之间的某个地方: ——。

除了上述内容之外,在返回错误代码之前,在返回错误时启动一个断言或类似的诊断,因为这将使跟踪更加容易。我这样做的方式是有一个定制的断言,仍然在发布时进行编译,但只有在软件处于诊断模式时才会触发,并且可以选择静默地向日志文件报告或在屏幕上暂停。

我个人将错误代码作为负整数返回,将 没错作为零返回,但是它可能会给您留下以下错误

if (MyFunc())
DoSomething();

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

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

你只能返回 true/false (如果你在 C 语言中工作,并且不支持 bool 变量,那么你可以返回某种类型的 # Definition) ,并且有一个全局错误缓冲区来保存最后一个错误:

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
}

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

这两种方法我都用过,它们对我都很有效。无论我使用哪种方法,我总是尝试应用这个原则:

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

验证输入的断言清楚地传达了函数的期望,而过多的错误检查会模糊程序逻辑。决定如何处理各种各样的错误情况可能会使设计变得非常复杂。如果可以坚持让程序员永远不要传递空指针,那么为什么要弄清楚 function X 应该如何处理空指针呢?

每次创建库时,我都使用第一种方法。使用类型定义的枚举作为返回代码有几个好处。

  • 如果函数返回更复杂的输出,比如数组及其长度,则不需要创建任意结构来返回。

    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 枚举还允许枚举名称在调试器中可见。这使得调试更加容易,而不需要不断查阅头文件。有一个将这个枚举转换为字符串的函数也很有帮助。

不管使用什么方法,最重要的问题是保持一致性。这适用于函数和参数命名、参数排序和错误处理。

我最近也在思考这个问题,并使用纯本地返回值编写了 模拟 try-catch-finally 语义的一些 C 宏。希望对你有用。

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

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

我更喜欢用 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

这里有一个方法,我认为是有趣的,但需要一些纪律。

这假定句柄类型变量是操作所有 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 语言中错误处理的常用方法。

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

与回报价值方法相比,它有一些优势:

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

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

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

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

  • 这使得自动检查是否处理所有错误变得更加容易。代码约定可能强制您将错误指针调用为 err,并且它必须是最后一个参数。因此脚本可以匹配字符串 err);,然后检查它后面是否跟着 if (*err。实际上,我们制作了一个宏,叫做 CER(check err return)和 CEG(check err goto)。因此,当我们只想返回错误时,不需要总是键入它,这样可以减少视觉上的混乱。

但是,并非我们的代码中的所有函数都具有这个输出参数。 这个输出参数用于通常抛出异常的情况。

CMU 的 CERT 提供了一个很好的 一组幻灯片,其中推荐了何时使用每种常见的 C (和 C + +)错误处理技术。最好的幻灯片之一是这个决策树:

Error Handling Decision Tree

我个人会改变这辆花车的两个特点。

首先,我要说明的是,有时候对象应该使用返回值来指示错误。如果一个函数只从一个对象中提取数据,而不改变对象,那么对象本身的完整性就不会受到威胁,使用返回值指示错误更合适。

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

  1. C + + 异常具有非常特殊的语义。如果您不想要这些语义,那么 C + + 异常是一个糟糕的选择。必须在抛出异常后立即处理异常,并且设计倾向于在错误需要将调用堆栈解除几个级别的情况下处理异常。

  2. 抛出异常的 C + + 函数以后不能包装成不抛出异常,至少不能在不支付异常的全部成本的情况下这样做。返回错误代码的函数可以包装为抛出 C + + 异常,从而使它们更加灵活。C + + 的 new通过提供一个非抛出变体来实现这一点。

  3. C + + 异常的开销相对较大,但是对于合理使用异常的程序来说,这个缺点被夸大了。程序不应该在代码路径上抛出异常,因为性能是一个问题。程序报告错误和退出的速度有多快并不重要。

  4. 有时 C + + 异常不可用。要么它们在 C + + 实现中实际上是不可用的,要么代码指南禁止它们。


由于最初的问题是关于多线程上下文的,我认为在最初的答案中,本地错误指示器技术(大流士先生回答中描述的技术)没有得到充分的重视。它是线程安全的,不会强制调用方立即处理错误,并且可以绑定描述错误的任意数据。缺点是它必须由一个对象持有(或者我认为以某种方式与外部关联) ,并且可以说比返回代码更容易被忽略。

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

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)。

除此之外,我建议你尝试分离错误标志和错误代码,以便在每次调用中保存一行,比如:

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

当您进行大量的错误检查时,这个小小的简化真的很有帮助。

我遇到过很多次这样的问答环节,我想提供一个更全面的答案。我认为考虑这个问题的最佳方式是 怎么做将错误返回给调用者,而 什么返回。

怎么做到的

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

  1. 返回值
  2. 争议
  3. 带外,包括非本地 goto (setjmp/longjmp) , 文件或全局作用域变量、文件系统等。

返回值

您只能返回一个值(对象) ; 但是,它可以是任意复杂的值。下面是一个错误返回函数的例子:

  enum error hold_my_beer(void);

返回值的一个好处是,它允许链接调用,从而减少错误处理的干扰:

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

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

争议

您可以通过多个参数通过多个对象返回多个参数,但是最佳实践确实建议将参数总数保持在较低的水平(比如 < = 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);
}

这迫使调用方传入对象,这可能会使它更有可能被检查。如果您有一组调用所有返回错误,并且您决定为每个返回错误分配一个新变量,那么它将在调用者中添加一些混乱。

乐队外

最著名的例子可能是(线程本地) errno 变量,被调用的函数设置该变量。被调用方很容易不检查这个变量,而且如果函数很复杂(例如,函数的两个部分返回相同的错误代码) ,那么只能检查一个变量。

使用 setjmp ()可以定义位置以及如何处理 int 值,并通过 longjmp ()将控制权转移到该位置。参见 Setjmp 和 longjmp 在 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]);
}

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

复试

回调是处理错误的最强大的方法,因为您可以告诉函数在出错时希望看到什么行为发生。可以向每个函数添加一个回调参数,或者如果每个结构的实例只需要自定义 u,则如下所示:

 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();

回调的一个有趣的好处是,可以多次调用它,或者在没有错误的情况下完全不调用它,因为在快乐路径上没有开销。

然而,还有一个控制反转。调用代码不知道是否调用了回调。因此,使用指示器也可能是有意义的。

这里有一个简单的程序来演示 这里是尼尔斯 · 皮彭布林克的回答的前两个子弹。

他的前两颗子弹是:

  • 将所有可能的错误状态存储在一个 typedef 的枚举中,并在库中使用它。不要仅仅返回 int 或者更糟糕的,用返回代码混合 int 或者不同的枚举。

  • 提供一个函数,将错误转换为人类可读的内容。可以很简单。只需输入错误枚举,输出常量字符。

假设您已经编写了一个名为 mymodule的模块。在这里,我使用一个 C 字符串数组(char *) ,只有当第一个基于枚举的错误代码的值为0时,这个数组才能正常工作,此后不再处理这些数字。选择权在你。

我的模块

/// @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);

C 包含我的映射函数,可以从枚举错误代码映射到可打印的 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;
}

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 _ fun1() = MYMODULE _ ERROR _ OK 中的 err 代码
MYMODULE _ fun2() = MYMODULE _ ERROR _ INVARG 中的 err 代码
() = MYMODULE _ ERROR _ MYERROR 的 err 代码

参考文献:

  1. 您可以在这里自己运行这个代码: https://onlinegdb.com/ByEbKLupS
  2. 我经常引用我的答案来查看这种类型的错误处理: STM32如何获得最后的重置状态

我已经看到 C 语言中函数在错误报告中使用的五种主要方法:

  • 没有错误代码报告或没有返回值的返回值
  • 返回只是错误代码的值
  • 返回一个有效值或错误代码值
  • 返回一个值,该值指示以某种方式获取可能包含错误上下文信息的错误代码的错误
  • 函数参数,该参数返回一个带有错误代码(可能带有错误上下文信息)的值

除了功能错误返回机制的选择之外,还要考虑错误代码助记符,并确保错误代码助记符不与正在使用的任何其他错误代码助记符冲突。通常,这需要使用三个字母前缀的方法来命名用 #defineenumconst static int定义它们的助记符。参见此讨论 “ static const”vs“ # Definition”vs“ enum”

一旦检测到错误,会有几个不同的结果,这可能是考虑函数如何提供错误代码和错误信息的一个因素。这些结果实际上分为两个阵营,可恢复的错误和不可恢复的错误:

  • 记录系统状态,然后终止
  • 等待并重试失败的操作
  • 通知人类并请求援助
  • 以降级状态继续执行

根据错误的上下文,错误类型可能使用多个这些结果。例如,由于文件不存在而打开失败的文件可能会以不同的文件名重试,或者通知用户请求帮助,或者在降级状态下继续执行。

五个主要途径的详情

有些函数不提供错误代码。这些函数要么不能失败,要么如果失败,它们就无声无息地失败。这类函数的一个例子是各种 is字符测试函数,例如 isdigit(),它指示字符值是否为数字。字符值是或不是数字或字母字符。与 strcmp()函数类似,比较两个字符串将得到一个值,该值指示如果两个字符串不相同,那么在排序序列中哪一个字符串高于另一个字符串。

在某些情况下,不需要错误代码,因为指示失败的值是有效的结果。例如,标准库中的 strchr()函数返回一个指针,指向在要扫描的字符串中找到的搜索字符,或者如果没有找到的话返回 NULL。在这种情况下,找不到字符是一个有效和有用的指示器。使用 strchr()的函数可能要求所搜索的字符不在字符串中才能成功,并且查找字符是一个错误条件。

其他函数不返回错误代码,而是通过外部机制报告错误。这被标准库中的大多数数学库函数所使用,这些函数要求用户将 errno设置为零,调用函数,然后检查 errno的值是否仍然为零。许多数学函数的输出值范围不允许使用特殊的返回值来指示错误,而且它们的接口中没有错误报告参数。

某些函数执行操作并返回一个错误代码值,其中一个可能的错误代码值表示成功,其余的值表示错误代码。例如,如果成功,函数可能返回一个值0,或者返回一个正或负的非零值,指示一个错误,返回的值是错误代码。

有些函数可以执行操作,如果成功,则返回来自一系列有效值的值,或者返回来自一系列指示错误代码的无效值的值。一种简单的方法是对有效值使用正值(0,1,2,...) ,对允许检查(如 if(status < 0) return error;)的错误代码使用负值。

有些函数返回一个有效值或一个无效值,指示需要通过某种方式获取错误代码的附加步骤的错误。例如,fopen()函数返回指向 FILE对象的指针,或者返回无效的指针值 NULL,并将 errno设置为指示失败原因的错误代码。许多返回 HANDLE值以引用资源的 Windows API 函数也可能返回 INVALID_HANDLE_VALUE值,而函数 GetLastError()用于获取错误代码。OPOS 控制对象标准要求 OPOS 控制对象提供两个函数 GetResultCode()GetResultCodeExtended(),以便在 COM 对象方法调用失败时检索错误状态信息。

在其他 API 中也使用了同样的方法,这些 API 使用一个句柄或对资源的引用,其中有一系列有效值,其中一个或多个值超出了这个范围,用于指示错误。然后提供一种机制来获取额外的错误信息,例如错误代码。

对于返回布尔值 true表示函数成功的函数和返回错误值 false的函数,也使用了类似的方法。然后程序员必须检查其他数据以确定错误代码,如使用 WindowsAPI 的 GetLastError()

有些函数有一个指针参数,其中包含被调用的函数的内存区域的地址,该函数被调用来提供错误代码或错误信息。这种方法的真正亮点在于,除了简单的错误代码之外,还有其他有助于指出错误的错误上下文信息。例如,JSON 字符串解析函数不仅可以返回错误代码,还可以返回指向解析失败的 JSON 字符串位置的指针。

我还看到函数返回一个错误指示符,比如带有错误信息参数的布尔值。我记得在某些情况下,错误信息参数可能是 NULL,表明调用者不想知道故障的具体情况。

在我的经验中,这种返回错误代码或错误信息的方法似乎并不常见,不过由于某种原因,我认为我在 Windows API 中不时地看到过它的使用,或者可能与 XML 解析器一起使用。

多线程的注意事项

当使用通过一种机制进行额外的错误代码访问的方法时,如在检查全局(如 errno)或使用函数(如 GetLastError())时,存在跨多个线程共享全局的问题。

现代编译器和库通过使用线程本地存储来处理这个问题,以确保每个线程都有其自己的存储,而其他线程不共享这些存储。但是仍然存在多个函数共享相同的线程本地存储位置以获取状态信息的问题,这可能需要一些适应性。例如,一个使用多个文件的函数可能需要解决这样一个问题,即所有可能失败的 fopen()调用在同一个线程中共享一个 errno

如果 API 使用某种类型的句柄或引用,那么可以将错误代码存储设置为特定于句柄。可以将 fopen()函数封装在另一个函数中,该函数执行 fopen(),然后设置一个 API 控制块,其中既包含 fopen()返回的 FILE *,也包含 errno的值。

我更喜欢这种方式

我倾向于将错误代码作为函数返回值返回,这样我可以在调用时检查它,或者将它保存到以后。在大多数情况下,错误是需要立即处理的,这就是为什么我更喜欢这种方法。

我对函数使用的一种方法是让函数返回一个简单的 struct,它包含两个成员、一个状态代码和返回值。例如:

struct FuncRet {
short  sStatus;  // status or error code
double dValue;   // calculated value
};


struct FuncRet Func(double dInput)
{
struct FuncRet = {0, 0};  // sStatus == 0 indicates success


// calculate return value FuncRet.dValue and set
// status code FuncRet.sStatus in the event of an error.


return FuncRet;
}




//  ... source code before using our function.


{
struct FuncRet s;


if ((s = Func(aDble)).sStatus == 0) {
// do things with the valid value s.dValue
} else {
// error so deal with the error reported in s.sStatus
}
}

这允许我立即检查错误。由于返回的数据很复杂,许多函数最终返回状态而不返回实际值。函数可以修改一个或多个参数,但是函数不返回除状态代码以外的值。