没有return语句的C和C ++函数

在查看工作代码时,我发现了一些(看似)令人反感的代码,其中函数具有返回类型,但没有返回。 我知道代码有效,但认为它只是编译器中的一个错误。

我编写了以下测试并使用我的编译器运行它(gcc(Homebrew gcc 5.2.0)5.2.0)

#include  int f(int a, int b) { int c = a + b; } int main() { int x = 5, y = 6; printf("f(%d,%d) is %d\n", x, y, f(x,y)); // f(5,6) is 11 return 0; } 

类似于我在工作中找到的代码,这默认返回函数中执行的最后一个表达式的结果。

我发现了这个问题,但对答案不满意。 我知道使用-Wall -Werror可以避免这种行为,但为什么它是一个选项呢? 为什么这仍然允许?

“为什么这仍然允许?” 它不是,但一般来说,编译器无法certificate你正在这样做。 考虑这个(当然非常简化)示例:

 // Input is always true because logic reason int fun (bool b) { if (b) { return 7; } } 

甚至这个:

 int fun (bool b) { if (b) { return 7; } // Defined in a different translation unit, will always call exit() foo(); // Now we can never get here, but the compiler cannot know } 

现在第一个例子可以流出结尾,但只要函数“正确”使用它就永远不会; 而第二个不能,但编译器不能知道这一点。 所以编译器会通过使这个错误来打破“工作”和合法,尽管可能是愚蠢的代码。

现在您发布的示例有点不同:这里, 所有路径都流出结束,因此编译器可以拒绝或忽略此函数。 然而,它会破坏依赖于生产代码中的编译器特定行为的真实世界代码,并且人们不喜欢它,即使它们是错误的。

但最终,流出非void函数的结尾仍然是未定义的行为。 它可能适用于某些编译器,但它不是,也绝不保证。

具有返回类型但没有返回语句的函数的结果是未定义的(除了在C ++中使用main ,其中返回值为0)。 它也不是语法错误,因为语法使用了一个函数的通用forms,它既适用于值返回函数,也适用于void函数。

这里发生的可能性是,最后一个表达式的结果存储在编译器用于函数返回值的同一寄存器(在x86,EAX或RAX上)中。 当函数返回时,返回指令单独留下该寄存器,并且调用代码只是假设该值具有该函数的返回值。

这不是编译器中的错误,但代码显然很糟糕。 在x86架构上,(R | E)AX用作累加器寄存器,也用作返回值。

让我们来看看完全未经优化的反汇编: https : //goo.gl/TihXpa

  • 您将看到f()的代码确实使用eax来存储添加结果。

  • 但是 ……继续使用-O1 (或任何更积极的优化级别)作为编译器选项(右上方框),看看现在发生了什么。

…我们发现编译器已经正确地意识到函数没有明确地返回它的结果,它只是变成了无操作。

因此,这段令人讨厌的代码是一个完美的例子,可以像调试版本一样工作,但在应用任何优化时都会失败。

在计算机体系结构级别:如果内存中存在默认位置,在简单计算后存储结果,则可以使用正确答案返回(偶然)此内存。

这里是:

至少对于x86,此函数的返回值应该在eax寄存器中。 那里的任何东西都会被调用者视为返回值。
因为eax用作返回寄存器,所以它通常被calee用作“scratch”寄存器,因为它不需要保留。 这意味着它很可能被用作任何局部变量。 因为它们两者在最后是相等的,所以更有可能将正确的值保留在eax中。

在这里查看类似的主题。

实际上,返回的值可能是某些机器寄存器中的最后一个值(例如,用于计算c )。 但是,根据标准,如果调用者访问/用户返回值,则返回值和结果都是未定义的。

至于为什么允许这种滥用/丑陋的代码构造……首先,在C早期版本中没有void关键字,并且 – 如果没有指定返回类型 – 函数的返回类型是int 。 对于实际上不打算返回任何内容的函数,该技术是(隐式或显式地)将它们定义为返回int ,不返回任何值,并让调用者不尝试访问/使用返回值。

由于编译器供应商在如何处理此类事情方面有一些自由,因此他们无需特别注意确保存在有效的返回值,或确保返回值未被使用。 在实践中,很大程度上是偶然的 – 如果访问了返回值 – 它经常碰巧包含函数中最后一个操作的值。 一些程序员偶然发现了他们的代码的这种行为 – 因为当时的代码通常被视为一种美德 – 使用它。

稍后,即使有问题的编译供应商试图改变行为(例如发出错误消息并在函数“落到最后”时拒绝代码),他们也会收到开发人员的错误报告(其中一些人声音很大) )关于他们的程序不再工作。 令人遗憾的是,编译器供应商屈服于压力。 其他编译供应商也屈服于类似的错误报告(forms为“gcc和编译器X这样做 – 你也应该这样做”),因为许多错误报告来自大公司或政府机构的开发人员,他们为编译厂商。 这就是大多数现代编译器诊断出这些事情的原因(通常作为可选警告,默认情况下禁用,例如gcc的-Wall选项)并给出开发人员期望的行为。

C和C ++的历史上充斥着许多这样模糊不清的特征,这些特征是程序员利用他们早期编译器的一些模糊行为和游说来防止行为被禁用的结果。

最好的现代做法是打开编译器警告,而不是利用这些function。 但是,仍然有足够的遗留代码 – 开发人员不希望因各种原因更新代码库(例如必须提供大量文档来说服监管机构代码仍然有效)才能停止使用这些function – 这些function仍然受到支持由编译器。