没有 return 语句的 C 和 C++ 函数
C and C++ functions without a return statement
在工作中查看代码时,我发现了一些(看似)令人反感的代码,其中一个函数具有 return 类型,但没有 return。我知道代码有效,但假设它只是编译器中的一个错误。
我写了下面的测试 运行 它使用我的编译器 (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 %d\n", x, y, f(x,y)); // f(5,6) is 11
return 0;
}
类似于我在工作中找到的代码,这默认为 returning 函数中执行的最后一个表达式的结果。
我找到了 this 个问题,但对答案不满意。我知道使用 -Wall -Werror
可以避免这种行为,但为什么它是一个选项?为什么这个仍然允许?
At computer architecture level: if there is a default place in memory where results are stored after simple calculations, this memory could be returned (by accident) with the correct answer.
这里是:
For x86 at least, the return value of this function should be in eax register. Anything that was there will be considered to be the return value by the caller.
Because eax is used as return register, it is often used as "scratch" register by calee, because it does not need to be preserved. This means that it's very possible that it will be used as any of local variables. Because both of them are equal at the end, it's more probable that the correct value will be left in eax.
查看 here 个类似主题。
具有 return 类型但没有 return 语句的函数的结果未定义(在具有 main
的 C++ 中除外,其中 return 值为 0) .这也不是语法错误,因为语法对函数使用了通用形式,该函数同时适用于值返回函数和 void 函数。
奇怪的是,最后一个表达式的结果存储在编译器用于 return 值的同一寄存器中(在 x86、EAX 或 RAX 上)功能。当函数 returns 时,return 指令不理会这个寄存器,调用代码只是假设该寄存器的值是函数的 return 值。
"Why is this still allowed?" 不是,但一般来说,编译器无法证明 你正在这样做。考虑这个(当然是极其简化的)例子:
// 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
}
现在第一个示例可以流出结尾,但只要使用函数就永远不会"properly";第二个不能,但编译器不知道这一点。所以编译器会破坏 "working" 和合法的,虽然可能是愚蠢的,代码通过使它成为一个错误。
现在您发布的示例有点不同:在这里,所有 路径都从末尾流出,因此编译器可以拒绝或忽略此函数。然而,它会破坏依赖于编译器特定行为的现实世界代码,就像在您的生产代码中一样,人们不喜欢这样,即使他们错了。
但最后,流出非void
函数的末尾仍然是未定义的行为。它可能适用于某些编译器,但它不是,也从来没有保证。
这不是编译器中的错误,但代码显然格式错误。在 x86 架构上,(R|E)AX 用作累加器寄存器,也用于 return 值。
那么让我们看一下完全未优化的反汇编:https://goo.gl/TihXpa
你会看到f()
的代码确实是用eax来存储加法结果
但是...继续使用-O1
(或任何更激进的优化级别)作为编译器选项(右上角框),看看现在会发生什么。
...我们发现编译器已经正确地意识到该函数没有显式 return 它的结果,它只是变成了一个空操作。
因此,这段令人讨厌的代码是一个完美的例子,它可以作为调试版本按预期工作,但在应用任何优化时会严重失败。
实际上,returned 的值可能是某个机器寄存器中的最后一个值(例如用于计算 c
的值)。但是,根据标准,如果调用者 accesses/users returned 值未定义,则 return 值和结果都是未定义的。
至于为什么允许这种 abusive/ugly 代码结构....首先,在 C
的早期版本中没有 void
关键字,并且 - 如果没有 return 指定了类型 - return 函数的类型是 int
。对于实际上 return 什么都没有的函数,技术是(隐式或显式)将它们定义为 returning int
,而不是 return 任何值,并具有来电者不尝试 access/use return 值。
由于编译器供应商在处理此类事情上有一定的自由度,因此他们不必特别注意确保存在有效的 return 值,或确保 return 值未使用。在实践中,这在很大程度上是偶然的——如果 return 值被访问——它经常恰好包含函数中最后一个操作的值。一些程序员偶然发现了他们代码的这种行为——因为当时简洁和神秘的代码通常被视为某种美德——利用了它。
后来,即使有问题的编译供应商试图改变行为(例如发出错误消息并在函数 "fell off the end" 时拒绝代码),他们也收到了开发人员的错误报告(一些他们非常直言不讳)关于他们的程序不再有效。令人遗憾的是,编译器供应商屈服于压力。其他编译供应商也屈服于类似的错误报告(形式为 "gcc and compiler X does it this way - yours should too"),因为其中许多错误报告来自大公司或政府机构的开发人员,他们为编译器供应商的客户付费。这就是为什么大多数现代编译器会诊断此类事情(通常作为可选警告,默认情况下禁用,例如 gcc 的 -Wall
选项)并给出开发人员预期的行为。
C 的历史以及 C++ 的历史充斥着许多像这样晦涩难懂的特性,这是由于程序员利用其早期编译器的一些晦涩行为并游说以防止行为被禁用。
现代最佳做法是打开编译器警告,而不是利用此类功能。然而,仍然有足够多的遗留代码——以及出于各种原因不想更新代码库的开发人员(例如,必须提供大量文档以说服监管机构代码仍然有效)停止使用此类功能——这些功能仍然受到支持编译器。
在工作中查看代码时,我发现了一些(看似)令人反感的代码,其中一个函数具有 return 类型,但没有 return。我知道代码有效,但假设它只是编译器中的一个错误。
我写了下面的测试 运行 它使用我的编译器 (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 %d\n", x, y, f(x,y)); // f(5,6) is 11
return 0;
}
类似于我在工作中找到的代码,这默认为 returning 函数中执行的最后一个表达式的结果。
我找到了 this 个问题,但对答案不满意。我知道使用 -Wall -Werror
可以避免这种行为,但为什么它是一个选项?为什么这个仍然允许?
At computer architecture level: if there is a default place in memory where results are stored after simple calculations, this memory could be returned (by accident) with the correct answer.
这里是:
For x86 at least, the return value of this function should be in eax register. Anything that was there will be considered to be the return value by the caller.
Because eax is used as return register, it is often used as "scratch" register by calee, because it does not need to be preserved. This means that it's very possible that it will be used as any of local variables. Because both of them are equal at the end, it's more probable that the correct value will be left in eax.
查看 here 个类似主题。
具有 return 类型但没有 return 语句的函数的结果未定义(在具有 main
的 C++ 中除外,其中 return 值为 0) .这也不是语法错误,因为语法对函数使用了通用形式,该函数同时适用于值返回函数和 void 函数。
奇怪的是,最后一个表达式的结果存储在编译器用于 return 值的同一寄存器中(在 x86、EAX 或 RAX 上)功能。当函数 returns 时,return 指令不理会这个寄存器,调用代码只是假设该寄存器的值是函数的 return 值。
"Why is this still allowed?" 不是,但一般来说,编译器无法证明 你正在这样做。考虑这个(当然是极其简化的)例子:
// 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
}
现在第一个示例可以流出结尾,但只要使用函数就永远不会"properly";第二个不能,但编译器不知道这一点。所以编译器会破坏 "working" 和合法的,虽然可能是愚蠢的,代码通过使它成为一个错误。
现在您发布的示例有点不同:在这里,所有 路径都从末尾流出,因此编译器可以拒绝或忽略此函数。然而,它会破坏依赖于编译器特定行为的现实世界代码,就像在您的生产代码中一样,人们不喜欢这样,即使他们错了。
但最后,流出非void
函数的末尾仍然是未定义的行为。它可能适用于某些编译器,但它不是,也从来没有保证。
这不是编译器中的错误,但代码显然格式错误。在 x86 架构上,(R|E)AX 用作累加器寄存器,也用于 return 值。
那么让我们看一下完全未优化的反汇编:https://goo.gl/TihXpa
你会看到
f()
的代码确实是用eax来存储加法结果但是...继续使用
-O1
(或任何更激进的优化级别)作为编译器选项(右上角框),看看现在会发生什么。
...我们发现编译器已经正确地意识到该函数没有显式 return 它的结果,它只是变成了一个空操作。
因此,这段令人讨厌的代码是一个完美的例子,它可以作为调试版本按预期工作,但在应用任何优化时会严重失败。
实际上,returned 的值可能是某个机器寄存器中的最后一个值(例如用于计算 c
的值)。但是,根据标准,如果调用者 accesses/users returned 值未定义,则 return 值和结果都是未定义的。
至于为什么允许这种 abusive/ugly 代码结构....首先,在 C
的早期版本中没有 void
关键字,并且 - 如果没有 return 指定了类型 - return 函数的类型是 int
。对于实际上 return 什么都没有的函数,技术是(隐式或显式)将它们定义为 returning int
,而不是 return 任何值,并具有来电者不尝试 access/use return 值。
由于编译器供应商在处理此类事情上有一定的自由度,因此他们不必特别注意确保存在有效的 return 值,或确保 return 值未使用。在实践中,这在很大程度上是偶然的——如果 return 值被访问——它经常恰好包含函数中最后一个操作的值。一些程序员偶然发现了他们代码的这种行为——因为当时简洁和神秘的代码通常被视为某种美德——利用了它。
后来,即使有问题的编译供应商试图改变行为(例如发出错误消息并在函数 "fell off the end" 时拒绝代码),他们也收到了开发人员的错误报告(一些他们非常直言不讳)关于他们的程序不再有效。令人遗憾的是,编译器供应商屈服于压力。其他编译供应商也屈服于类似的错误报告(形式为 "gcc and compiler X does it this way - yours should too"),因为其中许多错误报告来自大公司或政府机构的开发人员,他们为编译器供应商的客户付费。这就是为什么大多数现代编译器会诊断此类事情(通常作为可选警告,默认情况下禁用,例如 gcc 的 -Wall
选项)并给出开发人员预期的行为。
C 的历史以及 C++ 的历史充斥着许多像这样晦涩难懂的特性,这是由于程序员利用其早期编译器的一些晦涩行为并游说以防止行为被禁用。
现代最佳做法是打开编译器警告,而不是利用此类功能。然而,仍然有足够多的遗留代码——以及出于各种原因不想更新代码库的开发人员(例如,必须提供大量文档以说服监管机构代码仍然有效)停止使用此类功能——这些功能仍然受到支持编译器。