在具有一个输入和一个输出的函数中输出错误消息是不好的做法

时间:2013-09-23 16:33:41

标签: c data-structures coding-style

我曾被告知,具有一个输入和一个输出(不完全是一个)的功能在被调用时不应该打印消息。但我不明白。它是为了安全还是仅仅为了惯例?

让我举个例子。如何处理访问具有错误索引的顺序列表中的数据的尝试?

// 1. Give out the error message inside the function directly.
DataType GetData(seqList *L, int index)
{
    if (index < 0  ||  index >= L->length) {
        printf("Error: Access beyond bounds of list.\n");
        // exit(EXIT_FAILURE);
    }
    return L->data[index];
}

// 2. Return a value or use a global variable(like errno) that
//    indicates whether the function performs successfully.
StateType GetData(seqList *L, int index, int *data)
{
    if (index < 0  ||  index >= L->length) {
        return ERROR;
    }
    *data = L->data[index];
    return OK;
}

6 个答案:

答案 0 :(得分:6)

我认为这里有两件事:

  1. 任何可见和意外的副作用(例如写入流)通常都很糟糕,而不仅仅是具有一个输入和一个输出的函数。如果我正在使用列表库,并且它开始静默地将错误消息写入我用于常规输出的同一输出流,我会认为这是一个问题。但是,如果正在为自己的个人用途编写这样的功能,并且你提前知道你想要采取的行动总是要打印一条消息并exit(),那就没问题了。 。只是不要强迫这种行为发生在其他人身上。

  2. 这是如何通知来电者有关错误的一般问题的具体情况。很多时候,函数无法知道对错误的正确响应,因为它没有调用者所执行的上下文。以malloc()为例。绝大多数时候,当malloc()失败时,我只想终止,但是很久以后我可能想通过调用malloc()来故意填充内存直到它失败,然后继续做点别的。在这种情况下,我不希望函数决定是否终止 - 我只是想让它告诉我它失败了,然后将控制权传回给我。

  3. 处理库函数中的错误有许多不同的方法:

    1. 终止 - 如果您自己编写程序,则很好,但对于通用库函数则不好。通常,对于库函数,您将希望让调用者决定在出现错误时要执行的操作,因此函数的作用仅限于通知调用者错误。

    2. 返回错误值 - 有时可以,但有时没有可行的错误值。 atoi()就是一个很好的例子 - 它返回的所有可能值都可以是输入字符串的正确翻译。错误返回的内容无关紧要,无论是0-1还是其他任何内容,都无法区分错误和有效结果,这正是您获取未定义行为的原因所在。遇到一个。从稍微纯粹的观点来看,它在语义上也是有问题的 - 例如,返回数字的平方根的函数是一回事,但是有时的函数返回数字的平方根,但是有时返回错误代码而不是平方根是另一回事。当返回值有两个完全不同的目的时,您可能会失去函数的自我记录简单性。

    3. 让程序处于错误状态,例如设置errno。您仍有一个基本问题,即如果没有可行的返回值,该函数仍然无法告诉您发生了错误。您可以提前将errno设置为0,然后每次都检查它,但这是很多工作,当您开始涉及并发时可能不可行。

    4. 调用错误处理函数 - 这基本上只是通过降压,因为错误函数也必须解决上面的问题,但至少你可以提供自己的。另外,正如R.注释在下面的注释中,除了非常简单的情况,如“总是终止任何错误”,它可能会要求太多的单个全局错误处理函数能够明智地处理可能出现的任何错误你的程序可以恢复正常执行的方式。具有多种错误处理功能并将适当的功能单独传递给每个库函数在技术上是可行的,但几乎不是最佳解决方案。以这种方式使用错误处理函数也很困难,甚至不可能在并发存在的情况下正确使用。

    5. 传入一个参数,如果遇到错误,该参数会被函数修改。从技术上讲是可行的,但为此目的添加一个额外的参数并不是真正可行的。

    6. 抛出异常 - 您的语言必须支持他们这样做,并且它们伴随着各种相关的困难,包括不清楚的结构和程序流程,更复杂的代码等。有些人 - 我不是其中之一 - 认为例外是longjmp()的道德等同物。

    7. 所有可能的方式都有其缺点和优点,因为人类还没有发现报告库函数错误的完美方式。

答案 1 :(得分:2)

通常,您应该确保您具有一致且一致的错误处理策略,这意味着要考虑是否要将错误传递到更高级别或在最初发生的级别处理它。这个决定与函数有多少输入和输出无关。

在一个深度嵌入式系统中,例如,在关键时刻发生内存分配失败,没有必要重新传递该错误(实际上你可能无法做到) - 所有你能做的就是输入一个紧密循环让看门狗重置你。在这种情况下,没有必要保留无效的返回值来指示错误,或者甚至根本没有检查返回值,如果这样做没有意义的话。 (注意我并不主张只是懒得不去检查返回值,这完全是另一回事。)

另一方面,在一个可爱漂亮的GUI应用程序中,您可能希望尽可能优雅地失败并将错误传递到可以处理/重试/适当的任何级别;或者如果没有别的办法可以将其呈现给用户。

答案 2 :(得分:1)

是的,这是一种不好的做法;更糟糕的是,您要将输出发送到stdout而不是stderr。这可能会通过将错误消息与输出混合来破坏数据。

就个人而言,我非常坚信这种“错误处理”是有害的。您无法验证调用者是否传递了L的有效值,因此检查index的有效性是不一致的。函数的文档化接口契约应该只是L必须是指向正确类型对象的有效指针,而index是有效索引(在任何意义上对您的代码都有意义)。如果传递了Lindex的无效值,则这是代码中的错误,而不是运行时可能发生的合法错误。如果您需要帮助调试它,assert中的assert.h宏可能是个好主意;它可以让您在不再需要时轻松关闭支票。

上述原则的一个可能例外是L的值来自程序中的其他数据结构,但index来自一些不受您控制的外部输入。然后,您可以在调用此函数之前执行外部验证步骤,但如果您始终需要验证,则可以像您一样进行集成。但是,在这种情况下,您需要有一种方法向调用者报告失败,而不是向stdout打印无用且有害的消息。因此,您需要保留一个可能的返回值作为错误标记,或者有一个额外的参数,允许您将结果和错误状态都返回给调用者。

答案 3 :(得分:1)

返回对成功条件无效的保留值。例如,NULL

建议不要打印,因为:

  1. 这对于错误的调用代码原因没有帮助。
  2. 您正在写一个可能被更高级代码用于其他内容的流。
  3. 错误可能会更高,因此您可能只是打印误导性错误消息。

  4. 正如其他人所说,一致性如何处理错误情况也是一个重要因素。但请考虑一下:

    • 如果您的代码被用作另一个应用程序的组件,一个不遵循您的打印约定,然后通过打印您不允许客户端代码保持忠实于自己的策略。因此,使用此策略可以将约定强加给所有相关代码。
    • 另一方面,如果您遵循返回保留值的“清洁”解决方案并且客户端代码想要遵循打印约定,则客户端代码可以轻松地适应您返回的内容甚至打印错误,通过制作简单包装你的功能。因此,使用此策略,您可以为代码的用户提供足够的空间来选择最适合他们的策略并忠实于此。

答案 4 :(得分:1)

最好使用perror()来显示错误消息,而不是使用printf()

语法:

void perror(const char *s);

此外,错误消息应该发送到stderr流而不是stdout。

答案 5 :(得分:0)

如果代码只处理一件事,那总是最好的。它更容易理解,更易于使用,并且适用于更多实例。

打印错误消息的GetData函数不适用于可能没有值的情况。即调用代码想要尝试获取值并使用默认值处理错误(如果它不存在)。

由于GetData不知道上下文,因此无法报告错误消息。作为一个更高调用堆栈的例子,我们可以报告你忘了给这个用户一个年龄,而在GetData中,它只知道我们无法得到一些价值。

多线程情况怎么样? GetData似乎可以从多个线程调用它。如果所有线程都需要同时写入,那么在中间推送一个随机位IO会导致争用谁有权访问控制台。