为了账号安全,请及时绑定邮箱和手机立即绑定

有效使用goto进行C中的错误管理?

有效使用goto进行C中的错误管理?

C
慕容3067478 2019-08-06 17:12:00
有效使用goto进行C中的错误管理?这个问题实际上是不久前在programming.reddit.com 上进行有趣讨论的结果。它基本归结为以下代码:int foo(int bar){     int return_value = 0;     if (!do_something( bar )) {         goto error_1;     }     if (!init_stuff( bar )) {         goto error_2;     }     if (!prepare_stuff( bar )) {         goto error_3;     }     return_value = do_the_thing( bar );error_3:     cleanup_3();error_2:     cleanup_2();error_1:     cleanup_1();     return return_value;}goto这里的使用似乎是最好的方法,导致所有可能性中最干净,最有效的代码,或者至少在我看来。在Code Complete中引用Steve McConnell :goto在分配资源,对这些资源执行操作,然后释放资源的例程中很有用。使用goto,您可以清理代码的一部分。goto可以降低忘记在检测到错误的每个位置释放资源的可能性。此方法的另一个支持来自本节中的“ Linux设备驱动程序”一书。你怎么看?这种情况goto在C中是否有效?您是否更喜欢其他方法,这些方法会产生更复杂和/或效率更低的代码,但是要避免goto?
查看完整描述

3 回答

?
有只小跳蛙

TA贡献1824条经验 获得超8个赞

FWIF,我发现你在问题的例子中给出的错误处理习惯比目前为止答案中给出的任何替代方案更具可读性和易于理解。虽然goto通常是一个坏主意,但是当以简单和统一的方式完成时,它对于错误处理是有用的。在这种情况下,即使它是a goto,它也是以明确定义的,或多或少结构化的方式使用的。


查看完整回答
反对 回复 2019-08-06
?
慕容森

TA贡献1853条经验 获得超18个赞

作为一般规则,避免使用goto是一个好主意,但是当Dijkstra第一次写“GOTO Considered Harmful”时流行的滥用行为现在甚至没有超过大多数人的思想。

您概述的是错误处理问题的一般解决方案 - 只要仔细使用它就可以了。

您的特定示例可以简化如下(步骤1):

int foo(int bar){
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }error_2:
    cleanup_2();error_1:
    cleanup_1();
    return return_value;}

继续这个过程:

int foo(int bar){
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;}

我认为,这相当于原始代码。这看起来特别干净,因为原始代码本身非常干净且组织良好。通常,代码片段并不像那样整洁(虽然我接受他们应该的论点); 例如,经常有更多的状态传递给初始化(设置)例程而不是显示,因此更多的状态也传递给清理例程。


查看完整回答
反对 回复 2019-08-06
?
蝴蝶不菲

TA贡献1810条经验 获得超4个赞

我很惊讶没有人提出这个替代方案,所以尽管这个问题已经存在了一段时间我会加入它:解决这个问题的一个好方法是使用变量来跟踪当前状态。这是一种可以使用的技术,无论是否goto用于到达清理代码。像任何编码技术一样,它有优点和缺点,并不适合所有情况,但如果你选择一种风格值得考虑 - 特别是如果你想要避免goto而不是深深嵌套的ifs。

基本思想是,对于可能需要采取的每个清理操作,都有一个变量,从中可以判断清理是否需要进行清理。

我将goto首先显示该版本,因为它更接近原始问题中的代码。

int foo(int bar){
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;}

与其他一些技术相比,这样做的一个优点是,如果初始化函数的顺序发生变化,仍然会发生正确的清理 - 例如,使用switch另一个答案中描述的方法,如果初始化顺序发生变化,那么switch必须非常仔细地编辑,以避免尝试清理一些事实上没有初始化的东西。

现在,有些人可能认为这种方法增加了许多额外的变量 - 事实上在这种情况下确实如此 - 但实际上现有的变量通常已经跟踪或者可以跟踪所需的状态。例如,如果prepare_stuff()实际上是对malloc()或的调用open(),则可以使用保存返回的指针或文件描述符的变量 - 例如:

int fd = -1;....fd = open(...);if (fd == -1) {
    goto cleanup;}...cleanup:if (fd != -1) {
    close(fd);}

现在,如果我们另外使用变量跟踪错误状态,我们可以goto完全避免,并且仍然可以正确清理,而不会让缩进越来越深,我们需要的初始化越多:

int foo(int bar){
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;}

同样,有可能批评这一点:

  • 不是所有那些“如果”伤害表现?不 - 因为在成功的情况下,你必须做所有的检查(否则你没有检查所有的错误情况); 在失败的情况下,大多数编译器会将失败if (oksofar)检查的顺序优化为单个跳转到清理代码(GCC肯定会) - 并且在任何情况下,错误情况通常对性能不太重要。

  • 这不是另外一个变量吗?在这种情况下是,但通常该return_value变量可用于oksofar扮演在这里播放的角色。如果您构建函数以一致的方式返回错误,您甚至可以避免if在每种情况下的第二个:

    int return_value = 0;if (!return_value) {
        return_value = do_something(bar);}if (!return_value) {
        return_value = init_stuff(bar);}if (!return_value) {
        return_value = prepare_stuff(bar);}

    这样编码的一个优点是,一致性意味着原始程序员忘记检查返回值的任何地方都像拇指一样伸出,这样就更容易找到(那一类)错误。

所以 - 这是(还)一种可以用来解决这个问题的风格。正确使用它可以提供非常干净,一致的代码 - 就像任何技术一样,在错误的手中它最终会产生冗长且令人困惑的代码:-)


查看完整回答
反对 回复 2019-08-06
  • 3 回答
  • 0 关注
  • 696 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信