在初始化C中的多个资源的函数中处理错误(清理和中止)的一些好方法是什么?

Dmi*_*tri 3 c error-handling readability

首先,如果有人可以重新提出问题以使其更清楚,请执行.

C编程中常见的情况是要按特定顺序初始化/分配多个资源.每个资源都是后续资源初始化的先决条件.如果其中一个步骤失败,则必须取消分配先前步骤的剩余资源.理想的伪代码(利用神奇的通用pure-unobtainium clean_up_and_abort()函数)看起来大致如下:

err=alloc_a()
if(err)
    clean_up_and_abort()

err=alloc_b()
if(err)
    clean_up_and_abort()

err=alloc_c()
if(err)
    clean_up_and_abort()

// ...

profit()
Run Code Online (Sandbox Code Playgroud)

我已经看到了几种处理这种方法的方法,所有这些方法似乎都有很大的缺点,至少在人们倾向于考虑"良好实践"方面.

在处理这种情况时,构造代码的最可读和最不容易出错的方法是什么?效率是优选的,但为了便于阅读,可以牺牲合理的效率.请列出优点和缺点.欢迎讨论多种方法的答案.

我们的目标是希望最终得到一套解决这个问题的几种首选方法.

我将从我已经看过的一些方法开始,请对它们进行评论并添加其他方法.

Dmi*_*tri 5

到目前为止我见过的三种最常见的方法:

1:嵌套的if语句(没有SESE纯粹主义者的多次返回).由于存在一系列先决条件,这种情况会迅速失控.IMO,即使在简单的情况下,这是一个可读性灾难,并没有真正的优势.我包括它因为我看到人们经常这样做.

uint32_t init_function() {
    uint32_t erra, errb, errc, status;
    A *a;
    B *b;
    C *c;

    erra = alloc_a(&a);
    if(erra) {
        status = INIT_FAIL_A;
    } else {

        errb = alloc_b(&b);
        if(errb) {
            dealloc_a(&a);
            status = INIT_FAIL_B;
        } else {

            errc = alloc_c();
            if(errc) {
                dealloc_b(&b);
                dealloc_a(&a);
                status = INIT_FAIL_C;
            } else {

                profit(a,b,c);
                status = INIT_SUCCESS;

            }
        }
    }
    // Single return.
    return status;
}
Run Code Online (Sandbox Code Playgroud)

2:多次退货.这是我现在的首选方法.逻辑很容易遵循,但它仍然很危险,因为清理代码必须重复,并且很容易忘记在其中一个清理部分中释放某些内容.

uint32_t init_function() {
    uint32_t err;
    A *a;
    B *b;
    C *c;

    err = alloc_a(&a);
    if(err) {
        return INIT_FAIL_A;
    }

    err = alloc_b(&b);
    if(err) {
        dealloc_a(&a);
        return INIT_FAIL_B;
    }

    err = alloc_c(&c);
    if(err) {
        dealloc_b(&b);
        dealloc_a(&a);
        return INIT_FAIL_C;
    }

    profit(a,b,c);
    return INIT_SUCCESS;
}
Run Code Online (Sandbox Code Playgroud)

3:GOTO.许多人原则上不喜欢goto,但这是在C编程中有效使用goto的标准参数之一.优点是很难忘记清理步骤,也没有复制.

uint32_t init_function() {
    uint32_t status;
    uint32_t err;
    A *a;
    B *b;
    C *c;

    err = alloc_a(&a);
    if(err) {
        status = INIT_FAIL_A;
        goto LBL_FAIL_A;
    }

    err = alloc_b(&b);
    if(err) {
        status = INIT_FAIL_B;
        goto LBL_FAIL_B;
    }

    err = alloc_c(&c);
    if(err) {
        status = INIT_FAIL_C;
        goto LBL_FAIL_C;
    }

    profit(a,b,c);
    status = INIT_SUCCESS;
    goto LBL_SUCCESS;

    LBL_FAIL_C:
    dealloc_b(&b);

    LBL_FAIL_B:
    dealloc_a(&a);

    LBL_FAIL_A:
    LBL_SUCCESS:

    return status;
}
Run Code Online (Sandbox Code Playgroud)

还有什么我没说的吗?

  • 很高兴你提到Goto并提供了一个干净的例子.对我来说,这是最易读和易于维护的,除了我认为单个GOTO标签的错误和一个成功更清晰.在这种情况下,您将解除错误并转移到单个错误标签.嵌套if语句可能变得非常难看,多次返回也可能有问题. (2认同)
  • `goto` 并没有消亡,在许多情况下,在存在多个偶然代码块的情况下,它提供了最有效的分支控制方式。它可以消除代码重复,最重要的是——它是完全可读的。为什么在实践中没有更多地看到它是一个谜。如果您查看标准 C 库函数源代码,您很快就会发现它是您的最爱。 (2认同)

use*_*109 3

4:全局变量,哇哦!!!因为每个人都喜欢全局变量,就像他们喜欢一样goto。但说实话,如果将变量的范围限制在文件范围内(使用 static 关键字),那么情况并没有那么糟糕。旁注:该cleanup函数获取/返回错误代码不变,以便整理init_function.

static A *a = NULL;
static B *b = NULL;
static C *c = NULL;

uint32_t cleanup( uint32_t errcode )
{
    if ( c )
        dealloc_c(&c);
    if ( b )
        dealloc_b(&b);
    if ( a )
        dealloc_a(&a);

    return errcode;
}

uint32_t init_function( void )
{
    if ( alloc_a(&a) != SUCCESS )
        return cleanup(INIT_FAIL_A);

    if ( alloc_b(&b) != SUCCESS )
        return cleanup(INIT_FAIL_B);

    if ( alloc_c(&c) != SUCCESS )
        return cleanup(INIT_FAIL_C);

    profit(a,b,c);
    return INIT_SUCCESS;
}
Run Code Online (Sandbox Code Playgroud)

5:假OOP。对于那些无法接受事实(全局变量在 C 程序中实际上很有用)的人,可以采用 C++ 方法。C++ 获取所有全局变量,将它们放入结构中,并将它们称为“成员”变量。不知怎的,这让每个人都高兴。

技巧是将指向结构的指针作为第一个参数传递给所有函数。C++ 在幕后执行此操作,而在 C 中则必须显式执行此操作。我调用指针that是为了避免与 发生冲突/混淆this

// define a class (uhm, struct) with status, a cleanup method, and other stuff as needed
typedef struct stResources
{
    char *status;
    A *a;
    B *b;
    C *c;
    void (*cleanup)(struct stResources *that);
}
stResources;

// the cleanup method frees all resources, and frees the struct
void cleanup( stResources *that )
{
    if ( that->c )
        dealloc_c( &that->c );
    if ( that->b )
        dealloc_b( &that->b );
    if ( that->a )
        dealloc_a( &that->a );

    free( that );
}

// the init function returns a pointer to the struct, or NULL if the calloc fails
// the status member variable indicates whether initialization succeeded, NULL is success
stResources *init_function( void )
{
    stResources *that = calloc( 1, sizeof(stResources) );

    if ( !that )
        return NULL;

    that->cleanup = cleanup;

    that->status = "Item A is out to lunch";
    if ( alloc_a( &that->a ) != SUCCESS )
        return that;

    that->status = "Item B is never available when you need it";
    if ( alloc_b( &that->b ) != SUCCESS )
        return that;

    that->status = "Item C is being hogged by some other process";
    if ( alloc_c( &that->c ) != SUCCESS )
        return that;

    that->status = NULL;  // NULL is success
    return that;
}

int main( void )
{
    // create the resources
    stResources *resources = init_function();

    // use the resources
    if ( !resources )
        printf( "Buy more memory already\n" );
    else if ( resources->status != NULL )
        printf( "Uhm yeah, so here's the deal: %s\n", resources->status );
    else
        profit( resources->a, resources->b, resources->c );

    // free the resources
    if ( resources )
        resources->cleanup( resources );
}
Run Code Online (Sandbox Code Playgroud)

  • 有关不完整数据类型、封装、数据隐藏、动态链接/后期绑定和动态数据结构的面向对象方法的优秀文章/教程,请参阅 [**ANSI-C 中的面向对象编程**](http:// www.cs.rit.edu/~ats/books/ooc.pdf)。虽然它是用 C 编写的,并且需要相当深入的 C 知识,但消化这些材料所需要的努力是非常值得的。它涵盖了大多数 C 书籍或教程中未包含的许多主题。 (2认同)