为什么在没有返回值的情况下流出非void函数的末尾不会产生编译器错误?

Cat*_*kul 157 c c++ gcc g++

自从我多年前意识到这一点,默认情况下这不会产生错误(至少在GCC中),我一直想知道为什么?

我知道您可以发出编译器标志来产生警告,但是它不应该总是出错吗?为什么非void函数没有返回值才有效?

评论中要求的示例:

#include <stdio.h>
int stringSize()
{
}

int main()
{
    char cstring[5];
    printf( "the last char is: %c\n", cstring[stringSize()-1] ); 
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

...编译.

fni*_*eto 144

C99和C++标准不需要函数来返回值.值返回函数中缺少的return语句将0仅在main函数中定义(返回).

基本原理包括检查每个代码路径是否返回一个值是非常困难的,并且可以使用嵌入式汇编程序或其他棘手的方法设置返回值.

来自C++ 11草案:

§6.6.3/ 2

流出函数的末尾[...]会导致值返回函数中的未定义行为.

§3.6.1/ 5

如果控件在main没有遇到return 语句的情况下到达结尾,则效果就是执行

return 0;
Run Code Online (Sandbox Code Playgroud)

请注意,C++ 6.6.3/2中描述的行为在C中是不同的.


如果用-Wreturn-type选项调用它,gcc会给你一个警告.

-Wreturn-type每当使用默认为int的返回类型定义函数时发出警告.还要警告任何return语句没有返回值,返回类型不是void的函数(从函数体的末尾掉下来被认为是没有值返回),以及一个函数中带有表达式的return语句return-type无效.

-Wall启用此警告.


就像好奇心一样,看看这段代码的作用:

#include <iostream>

int foo() {
   int a = 5;
   int b = a + 1;
}

int main() { std::cout << foo() << std::endl; } // may print 6
Run Code Online (Sandbox Code Playgroud)

此代码具有正式的未定义行为,并且在实践中它依赖于约定体系结构.在一个特定的系统上,使用一个特定的编译器,返回值是最后一个表达式求值的结果,存储在该eax系统的处理器的寄存器中.

  • 我会谨慎地将未定义的行为称为"允许",但不可否认的是,将其称为"禁止"也是错误的.不是错误而不需要诊断与"允许"不完全相同.至少,你的回答有点像你说它可以做,但很大程度上不是. (10认同)
  • @Catskul,你为什么要买这个论点?如果不是很简单,那么识别函数的退出点并确保它们都返回一个值(以及声明的返回类型的值)是不可行的? (4认同)
  • @Catskul,是的,不.静态类型和/或编译语言会做很多你可能会认为"过于昂贵"的事情,但因为它们只在编译时执行一次,所以它们的开销可以忽略不计.即使这样说,我也不明白为什么识别函数的出口点需要超线性:你只需遍历函数的AST并寻找返回或退出调用.这是线性时间,这显然是有效的. (3认同)
  • 我认为这更好,尽管仍然倾向于向语言新手建议,如果您在应该返回值的函数中省略“return”语句,您最终将得到一个语义已定义的程序。确实,C++“不需要”“return”语句,因为它“不需要”因遗漏而出错,但 C++_确实_需要它才能使您的程序具有明确定义的语义...这就是我们努力的目标为了。 (2认同)
  • @LightnessRacesinOrbit:如果一个带有返回值的函数有时会立即返回一个值,有时会调用另一个总是通过`throw`或`longjmp`退出的函数,编译器在调用非返回函数后需要一个无法访问的`return`功能?不需要它的情况并不常见,即使在这种情况下包括它的要求可能也不会是繁重的,但是不要求它的决定是合理的. (2认同)
  • @supercat:在这种情况下,超级智​​能编译器不会发出警告或错误,但是 - 再次 - 对于一般情况,这基本上是无法计算的,所以你坚持一般的经验法则。但是,如果您知道永远不会到达您的函数结束,那么您离传统函数处理的语义还很远,是的,您可以继续这样做并知道它是安全的。坦率地说,在这一点上,您比 C++ 低了一层,因此,无论如何,它的所有保证都没有实际意义。 (2认同)

Joh*_*ica 42

默认情况下,gcc不会检查所有代码路径是否返回值,因为通常无法执行此操作.它假设你知道你在做什么.考虑使用枚举的常见示例:

Color getColor(Suit suit) {
    switch (suit) {
        case HEARTS: case DIAMONDS: return RED;
        case SPADES: case CLUBS:    return BLACK;
    }

    // Error, no return?
}
Run Code Online (Sandbox Code Playgroud)

程序员知道,除非有bug,否则此方法总是会返回一种颜色.gcc相信你知道你在做什么,所以它不会强迫你在函数的底部放回一个.

另一方面,javac尝试验证所有代码路径都返回一个值,如果它不能证明它们都有效,则会抛出错误.Java语言规范强制要求此错误.请注意,有时它是错误的,你必须输入一个不必要的return语句.

char getChoice() {
    int ch = read();

    if (ch == -1 || ch == 'q') {
        System.exit(0);
    }
    else {
        return (char) ch;
    }

    // Cannot reach here, but still an error.
}
Run Code Online (Sandbox Code Playgroud)

这是一个哲学上的差异.C和C++是比Java或C#更宽松和更信任的语言,因此较新语言中的一些错误是C/C++中的警告,默认情况下会忽略或关闭某些警告.

  • 在第一个中,它没有给你信任覆盖所有枚举情况(你需要一个默认情况或切换后的返回),而在第二个它不知道`System.exit()`永远不会回来 (3认同)
  • 如果javac实际上检查了代码路径,难道不是您看不到这一点吗? (2认同)
  • 对于javac(一个强大的编译器)来说,知道`System.exit()`永远不会返回似乎是直截了当的.我查了一下(http://java.sun.com/j2se/1.4.2/docs/api/java/lang/System.html#exit%28int%29),文档只是说它"永远不会回来".我想知道这意味着什么...... (2认同)

AnT*_*AnT 13

你的意思是,为什么流出一个值返回函数的末尾(即没有显式退出return)不是错误?

首先,在C中,当执行代码实际使用返回值时,函数是否返回有意义的内容才是关键.也许当你知道大多数时候你不会使用它时,语言不想强迫你返回任何东西.

其次,显然语言规范不希望强制编译器作者检测和验证存在显式的所有可能的控制路径return(尽管在许多情况下这并不难).此外,一些控制路径可能会导致非返回函数 - 编译器通常不知道的特征.这样的路径可能成为恼人的误报的来源.

另请注意,C和C++在这种情况下对行为的定义不同.在C++中,只是在值返回函数的末尾流出始终是未定义的行为(无论调用代码是否使用了函数的结果).在C中,仅当调用代码尝试使用返回的值时,才会导致未定义的行为.

  • @Chris Lutz:是的,`main`在这方面很特别. (3认同)

Kei*_*son 7

C 和 C++ 有不同的规则。


C 中的语言规则是,如果达到}返回非值的函数的结束并且void调用者尝试使用该值,则行为是未定义的。只要调用者不使用该值,从函数末尾脱落就具有明确定义的行为。

可能需要所有可能的控制路径来执行return在离开函数之前执行语句,但 C 传统上并不要求编译器进行此类代码分析。(许多编译器无论如何都会进行这种分析,并在适当的情况下发出警告。)

允许从非功能的末端脱落的主要原因void是历史性的。K&R C(Kernighan 和 Ritchie 的书 1978 年第一版中描述的版本,在 1989 年 ANSI 和 1990 年 ISO C 标准之前)没有关键字void或类型。在 1999 年 ISO C 标准之前,C 具有“隐式int”规则,这意味着您可以声明或定义一个没有显式返回类型的函数,并且它将返回一个int结果。

在 K&R C 中,如果您想要一个不返回结果的函数,您可以在没有显式返回类型的情况下定义它,并且简单地不返回值:

#include <stdio.h>

do_something() {
    printf("Not returning a value\n");
}

int main() {
    do_something();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

该函数实际上会返回一些垃圾int值,调用者会悄悄地忽略这些值。

在现代 C 语言中,你可以这样写:

#include <stdio.h>

void do_something(void) {
    printf("Not returning a value\n");
}

int main(void) {
    do_something();
}
Run Code Online (Sandbox Code Playgroud)

这保证了调用者不能尝试使用返回值。从 C89/C90 开始,该语言仍然支持旧样式以避免破坏现有代码。int当C99 中删除隐式规则时,对非void函数无法返回值的要求没有改变(并且大多数 C99 及更高版本的编译器仍然int默认支持隐式规则,可能会带有警告,因此旧的 K&R C 代码仍然可以进行编译)。


在 C++ 中,从构造函数、析构函数、函数以外的函数末尾流出void,或main会导致未定义的行为,无论调用者尝试对结果执行什么操作。


Yak*_*ont 5

在C / C ++中,合法的做法是不要从声称要返回某些内容的函数中返回。有许多用例,例如调用exit(-1),或调用它或引发异常的函数。

即使您要求不这样做,编译器也不会拒绝合法的C ++,即使它导致了UB。特别是,您不要求生成任何警告。(默认情况下,Gcc仍会启用某些功能,但是添加后,这些功能似乎与新功能保持一致,而不是针对旧功能的新警告)

更改默认的no-arg gcc以发出一些警告可能是对现有脚本或make系统的重大更改。精心设计的-Wall警告或者处理警告,或者切换单个警告。

学习使用C ++工具链是学习成为C ++程序员的障碍,但是C ++工具链通常由专家编写并为专家编写。

  • 例如,将[`-Wduplicated-cond`](https://gcc.gnu.org/ml/gcc-patches/2015-10/msg00245.html)的一部分破坏了GCC引导程序。在* most *代码中似乎合适的某些警告并非在所有代码中都适用。这就是为什么默认情况下不启用它们。 (2认同)