没有返回语句的C和c++函数

C and C++ functions without a return statement

本文关键字:c++ 函数 返回 语句      更新时间:2023-10-16

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

我写了下面的测试并使用我的编译器(gcc (Homebrew gcc 5.2.0) 5.2.0)

运行它
#include <stdio.h>
int f(int a, int b) {
  int c = a + b;
}
int main() {
   int x = 5, y = 6;
   printf("f(%d,%d) is %dn", x, y, f(x,y)); // f(5,6) is 11
   return 0;
}

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

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

"为什么还允许这样做?"它不是,但一般来说,编译器不能证明您正在做它。考虑这个(当然是极其简化的)示例:

// 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函数的末端仍然是未定义的行为。它可能在某些编译器上工作,但它不是,也从来没有保证过。

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

那么让我们来看看一个完全未优化的拆卸:https://goo.gl/TihXpa

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

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

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

因此,这段令人讨厌的代码是一个完美的例子,它可以在调试构建时按预期工作,但在应用任何优化时却会严重失败。

具有返回类型但没有返回语句的函数的结果是未定义的(c++中main除外,其返回值为0)。这也不是语法错误,因为语法使用的函数的一般形式既适用于返回值函数,也适用于void函数。

这里发生的事情是最后一个表达式的结果被存储在编译器用于函数返回值的相同寄存器中(在x86, EAX或RAX上)。当函数返回时,返回指令不处理这个寄存器,调用代码简单地假定这里的值就是函数的返回值。

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

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

由于编译器供应商在如何处理这类事情上有一定的自由,他们不必特别注意确保有一个有效的返回值,或者确保返回值未被使用。在实践中,很大程度上是偶然的——如果访问了返回值——它经常恰好包含函数中最后一个操作的值。一些程序员偶然发现了他们代码的这种行为——因为简洁和神秘的代码在当时通常被视为一种美德——就利用了它。

后来,即使有问题的编译器供应商试图改变行为(例如,当函数"掉到末尾"时发出错误消息并拒绝代码),他们也会收到开发人员(其中一些人相当直言不讳)关于他们的程序不再工作的错误报告。悲哀的是,编译器供应商屈服于压力。其他编译器供应商也屈服于类似的错误报告(以"gcc和编译器X这样做——你的也应该这样做"的形式),因为这些错误报告中有许多来自大公司或政府机构的开发人员,他们是编译器供应商的付费客户。这就是为什么这些事情会被大多数现代编译器诊断出来(通常作为一个可选的警告,默认禁用,比如gcc的-Wall选项),并给出开发人员期望的行为。

C的历史,以及c++的历史,充斥着许多像这样的模糊特性,这是程序员利用他们早期编译器的一些模糊行为并游说以防止该行为被禁用的结果。

最好的现代实践是打开编译器警告,而不是利用这些特性。然而,仍然有足够的遗留代码——以及出于各种原因不想更新代码库的开发人员(例如,必须提供大量文档来说服监管机构代码仍然有效)停止使用这些特性——编译器仍然支持这些特性。

在计算机体系结构层面:如果在内存中有一个默认位置存储简单计算后的结果,则该内存可能(偶然地)返回正确的答案。

在这里:

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

查看此处类似的主题