有没有更好的方法来进行C风格的错误处理?

| 我正在尝试通过编写一个简单的解析器/编译器来学习C。到目前为止,这是非常令人启发的经历,但是来自C#的强大背景,我在调整方面遇到了一些问题-尤其是由于缺少异常。 现在,我阅读了“更干净”,更优雅,更难以识别,并且我同意该文章中的每个单词;在我的C#代码中,我尽可能避免抛出异常,但是,现在我面临着一个无法抛出异常的世界,我的错误处理完全淹没了我本来应该清晰易懂的代码逻辑。 目前,我正在编写在出现问题时需要快速失败的代码,并且它也有潜在的深层嵌套-我确定了一种错误处理模式,其中\“ Get \”函数在发生错误时返回NULL,其他功能在失败时返回“ 0”。在这两种情况下,失败的函数都会调用
NS_SetError()
,因此所有调用函数需要做的就是清理并在失败时立即返回。 我的问题是,我脑海中涌现出许多“ 2”条语句-它非常重复并且完全掩盖了底层逻辑。我最终为自己创建了一个简单的宏来尝试改善这种情况,例如:
#define NOT_ERROR(X)    if ((X) < 0) return -1

int NS_Expression(void)
{
    NOT_ERROR(NS_Term());
    NOT_ERROR(Emit(\"MOVE D0, D1\\n\"));

    if (strcmp(current->str, \"+\") == 0)
    {
        NOT_ERROR(NS_Add());
    }
    else if (strcmp(current->str, \"-\") == 0)
    {
        NOT_ERROR(NS_Subtract());
    }
    else
    {
        NS_SetError(\"Expected: operator\");
        return -1;
    }
    return 0;
}
函数
NS_Term
NS_Add
NS_Subtract
的每一个执行
NS_SetError()
并在出现错误的情况下返回
-1
-更好,但仍然感觉我在滥用宏,并且不允许进行任何清理(某些函数,特定的返回指针的“ 9”函数更加复杂,需要运行清理代码)。 总的来说,感觉就像是我遗漏了一些东西-尽管事实证明这种方式的错误处理更容易识别,但是在我的许多函数中,我确实很难确定错误是否得到正确处理: 某些函数在出错时返回
NULL
某些函数在出现错误时返回
< 0
某些功能永远不会产生错误 我的函数执行
NS_SetError()
,但其他许多函数则不执行。 有没有更好的方法来构造函数,或者其他人也有这个问题吗? 还有having9函数(返回指向对象的指针)对错误返回error10是一个好主意,还是只是使我的错误处理混乱?     
已邀请:
一种清理技术是使用while循环,该循环永远不会真正进行迭代。它为您提供了goto,而无需使用goto。
#define NOT_ERROR(x) if ((x) < 0) break;
#define NOT_NULL(x) if ((x) == NULL) break;

// Initialise things that may need to be cleaned up here.
char* somePtr = NULL;

do
{
    NOT_NULL(somePtr = malloc(1024));
    NOT_ERROR(something(somePtr));
    NOT_ERROR(somethingElse(somePtr));
    // etc

    // if you get here everything\'s ok.
    return somePtr;
}
while (0);

// Something went wrong so clean-up.
free(somePtr);
return NULL;
但是,您会失去一定程度的缩进。 编辑:我想补充一点,我没有反对goto的功能,只是对于发问者的用例,他实际上并不需要它。在某些情况下,使用goto击败裤子是其他任何方法,但这不是其中一种。     
这是一个更大的问题,当您必须在每个16英镑的错误之前重复相同的完成代码。在这种情况下,通常使用
goto
int func ()
{
  if (a() < 0) {
    goto failure_a;
  }

  if (b() < 0) {
    goto failure_b;
  }

  if (c() < 0) {
    goto failure_c;
  }

  return SUCCESS;

  failure_c:
  undo_b();

  failure_b:
  undo_a();

  failure_a:
  return FAILURE;
}
您甚至可以围绕它创建自己的宏,以节省一些键入操作,例如:(我还没有测试过):
#define CALL(funcname, ...) \\
  if (funcname(__VA_ARGS__) < 0) { \\ 
    goto failure_ ## funcname; \\
  }
总体而言,与琐碎的处理相比,这是一种更清洁,更少冗余的方法:
int func ()
{
  if (a() < 0) {
    return FAILURE;
  }

  if (b() < 0) {
    undo_a();
    return FAILURE;
  }

  if (c() < 0) {
    undo_b();
    undo_a();
    return FAILURE;
  }

  return SUCCESS;
}
作为一个额外的提示,我经常使用链接来减少代码中的ѭ21的数量:
if (a() < 0 || b() < 0 || c() < 0) {
  return FAILURE;
}
由于ѭ23是短路运算符,因此上述公式将替代三个单独的ѭ21。也考虑在
return
语句中使用链接:
return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;
    
您可能不希望听到此消息,但是使用C进行异常处理的方法是通过
goto
语句。这是它使用语言的原因之一。 另一个原因是“ 17”是状态机实现的自然表达。状态机最能代表什么常见的编程任务?词法分析器。有时看一下
lex
的输出。转到。 因此,在我看来,现在是时候让您开始喜欢这种语言语法元素
goto
了。     
必须至少从两个层次上考虑:函数如何交互以及中断时如何处理。 我看到的大多数大型C框架总是通过引用返回状态和\“ return \\”值(WinAPI和许多C Mac OS API就是这种情况)。您想退还布尔值吗?
StatusCode FooBar(int a, int b, int c, bool* output);
您想返回一个指针?
StatusCode FooBar(int a, int b, int c, char** output);
反正你懂这个意思。 在调用函数方面,我最常看到的模式是使用指向清除标签的goto语句:
    if (statusCode < 0) goto error;

    /* snip */
    return everythingWentWell;

error:
    cleanupResources();
    return somethingWentWrong;
    
除了
goto
,标准C还具有另一种结构来处理出色的流量控制
setjmp/longjmp
。它的优点是,与某些人提出的“ 36”相比,您可以更轻松地突破多重嵌套控制语句,并且“ 17”提供的状态指示可以对出错原因进行编码。 另一个问题只是构造的语法。使用不能无意添加的控制语句不是一个好主意。就你而言
if (bla) NOT_ERROR(X);
else printf(\"wow!\\n\");
从根本上走错了。我会用类似
#define NOT_ERROR(X)          \\
  if ((X) >= 0) { (void)0; }  \\
  else return -1
代替。     
简短的答案是:让您的函数返回不可能是有效值的错误代码-并始终检查返回值。对于返回指针的函数,该值为10。对于返回非负
int
的函数,它是一个负值,通常为
-1
,依此类推... 如果每个可能的返回值也是一个有效值,请使用按引用调用:
int my_atoi(const char *str, int *val)
{
        // convert str to int
        // store the result in *val
        // return 0 on success, -1 (or any other value except 0) otherwise
}
检查每个函数的返回值可能看起来很乏味,但这就是在C中处理错误的方式。请考虑函数nc_dial()。它所做的只是检查其参数的有效性,并通过调用getaddrinfo(),socket(),setsockopt(),bind()/ listen()或connect()建立网络连接,最终释放未使用的资源并更新元数据。这可以在大约15行中完成。但是,由于错误检查,该功能有近100行。但这就是C语言中的方式。一旦习惯了,就可以轻松掩盖头脑中的错误检查。 此外,多个ѭ44没错。相反:这通常是谨慎的程序员的标志。谨慎是好事。 最后要说的是:如果有人在用枪指着你的时候不能证明使用它们的合理性,则不要将宏用于定义值。更具体地说,永远不要在宏中使用控制流语句:这使可怜的家伙感到困惑,可怜的家伙不得不在离开公司5年后维护您的代码。 ѭ45没问题。它简单,干净而且显而易见,以至于您无法做得更好。 一旦您放弃了将控制流隐藏在宏中的趋势,实际上就没有理由感到自己丢失了某些东西。     
goto语句是实现异常样式处理的最简单且可能最干净的方法。如果在宏args中包含比较逻辑,则使用宏可以使阅读起来更容易。如果您组织例程以执行正常(即非错误)工作,并且仅对异常使用goto,则读取该例程非常干净。例如:
/* Exception macro */
#define TRY_EXIT(Cmd)   { if (!(Cmd)) {goto EXIT;} }

/* My memory allocator */
char * MyAlloc(int bytes)
{
    char * pMem = NULL;

    /* Must have a size */
    TRY_EXIT( bytes > 0 );

    /* Allocation must succeed */
    pMem = (char *)malloc(bytes);
    TRY_EXIT( pMem != NULL );

    /* Initialize memory */
    TRY_EXIT( initializeMem(pMem, bytes) != -1 );

    /* Success */
    return (pMem);

EXIT:

    /* Exception: Cleanup and fail */
    if (pMem != NULL)
        free(pMem);

    return (NULL);
}
    
那这个呢?
int NS_Expression(void)
{
    int ok = 1;
    ok = ok && NS_Term();
    ok = ok && Emit(\"MOVE D0, D1\\n\");
    ok = ok && NS_AddSub();
    return ok
}
    
我从来没有想过用
goto
do { } while(0)
这样的错误处理方式-很好,但是考虑了一下,我意识到在很多情况下,我可以通过将函数一分为二来做同样的事情:
int Foo(void)
{
    // Initialise things that may need to be cleaned up here.
    char* somePtr = malloc(1024);
    if (somePtr = NULL)
    {
        return NULL;
    }

    if (FooInner(somePtr) < 0)
    {
        // Something went wrong so clean-up.
        free(somePtr);
        return NULL;
    }

    return somePtr;
}

int FooInner(char* somePtr)
{
    if (something(somePtr) < 0) return -1;
    if (somethingElse(somePtr) < 0) return -1;
    // etc

    // if you get here everything\'s ok.
    return 0;
}
现在确实意味着您获得了一个额外的功能,但无论如何我还是更喜欢许多短功能。 根据飞利浦的建议,我还决定也避免使用控制流宏-只要将它们放在一行中,它就很清楚发生了什么。 至少,让我放心的是,我不只是想念一些东西-其他所有人也都有这个问题! :-)     
使用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;
}
    

要回复问题请先登录注册