GCC 中在非 void 函数中不写 return 的情况

被同学的同学给逼死了……

问题提出

今天同学给我说,他们计导课上有个人在声明返回值为 int 的函数中不写 return 也能得到正确的结果。瞬间感觉整个人的三观都被颠覆了……询问清楚环境是 Dev C++ with TDM-GCC 之后,在电脑上配置好后开始实验。

一顿瞎搞

代码如下(来自万恶的谭浩强的《C 程序设计》),一个简单的比较大小的程序:

#include <stdio.h>

int max(int x, int y);
int main()
{
    int a, b, c;
    scanf("%d,%d", &a, &b);
    c = max(a, b);
    printf("max=%d\n", c);
    return 0;
}

int max(int x, int y)
{
    int z;
    if (x > y)
        z = x;
    else
        z = y;
}

这段代码有一个很明显的问题,就是 max 函数没有 return z。理论上这样编译是过不去的。而同学的同学在他们的计导课上提出来的时候,老师一边说着“这绝对过不了”,一边点了 Run。然后意想不到的事情发生了……能运行,而且结果正确。

当时老师就惊呆了。在课上老师最后给的一个初步的定论是“可能不加 return 就默认返回变量 z 了”。真是滑天下之大稽,GCC 怎么可能会这么智能,知道你要返回什么变量。作为一个老师,简直缺乏基本素养。

回宿舍之后我下了个 Dev C++ with TDM-GCC,敲上去,发现确实能通过。但本着严谨的精神,先狠狠地打这老师的脸。

int z; 改成 int z, u, v, w,你 GCC 如果真那么智能我就不信在一沓变量里你还能知道我要 z。结果和预想的一样,通过。纳闷的同时,求助于腹黑猫——毕竟 UESTC 满绩大佬。

然后我们开始考虑,会不会在 stdio.h 里有个内置的 max 函数。于是我去掉了声明,编译不通过。查了下头文件,发现并没有这个函数。腹黑猫那边调试的时候发现在 stl_algobase.h 有这么个函数,怀疑是自动补全了。

检查了一下 Dev C++ 里面的设置,发现也没有设置自动补上 #include。于是事情再次陷入僵局。

在思考这个问题的时候,我想看看在其他编译器中是不是也能编译通过,于是打开了 VS,把代码复制进去,按下 F5,预想中的报错“max 必须要有返回值”。这点看来 MSVC 比较严格。

柳暗花明

突然腹黑猫发过来一张截图。根据截图里面的内容,GCC 和 MSVC(这点好像不太对,参见上面)对于没有 return 的函数会直接返回 eax 寄存器里面的值。

深入 C 语言返回值

我对过于底层的东西了解甚少,正一脸懵逼的时候,腹黑猫又发过来一张截图,是 Stack Overflow 上面的解释(也就是上一张截图中的链接)。

c++ - Why does flowing off the end of a non-void function without returning a value not produce a compiler error? - Stack Overflow

根据 Stack Overflow 回答的说法,尽管在一个不为 void 的函数中不写 return 是理所当然不对的,但在某些系统和编译器上,此时函数返回的是存储在 eax 寄存器中最后一个表达式的值。

如果按照这个解释,那么 GCC 就是这个所谓的“particular complier”。因此在没有书写 return z; 的时候,编译器“自作主张”帮我 return eax; 了。

到这里其实问题已经解决了。但是当我在函数结束前加了一句 z = 23333; 的时候,最终的返回值并不是 23333。这又让我很难受了。

按照腹黑猫所说,赋值语句直接操纵的是内存,因为 z 实质上是个地址,只涉及到一个内存操作的时候不需要用到寄存器。

实践出真知

在知道原因后,我想自己看一下到底是不是这样子(似乎这样没有意义,毕竟我并没到能质疑 Stack Overflow 的水平)。最开始是用 Dev C++ 编译之后送到 OllyDbg 里调试,但这样太痛苦了。查阅之后发现 Dev C++ 其实自带了查看 CPU 的。但不知道为什么在我这里死活用不了。考虑到这是个编译器相关而 IDE 无关的问题,根据网上的建议换用 Code::Blocks 调试。

Code::Blocks 无法对单个源代码调试,所以需要新建一个工程。在 main.cpp 里贴上代码,设置好调试器。首先我给 max 函数结束的地方打上断点。

打开 Disassembly 窗口之后,发现程序运行到断点处的时候的汇编代码如下:

从图中可以看到,z = y;这条语句在执行的时候是操纵了 eax 寄存器的。

那如果我们在后面直接写上 z = 23333; 呢?

可以看到,z = 23333; 是直接对 0x5b25 这个内存地址进行操作的,并不借助 eax 寄存器,因此在返回 eax 寄存器中的值的时候,得到的是 z = 23333; 之前的结果。

如果我们再引入一个变量 t,写成 int t = z + 1; 呢?

eax 寄存器的值被改变了。所以最终我们得到的结果是 4 而不是 3。

从这几个实例中确实能够发现,只有在涉及到多个变量的操作的时候才会改变 eax 中的值。

那么回到最初的问题本身,总结如下:GCC 在编译的时候发现没有写 return 语句,所以就直接将 eax 寄存器中的值返回。由于 max 函数中不涉及其他的变量操作,因此误打误撞得到了正确的结果。

后记

在被问到这个问题之前,其实我从来没有思考过关于返回值的问题。顶多就是知道有些编译器在 main 函数没有返回值的时候会自动 return 0。这次借这个问题,也算是学习了一下。

另外就是,在写这篇文章的时候,我注意到 Code::Blocks 里编译虽然没有报 error,但是报了 warning。

很明显地提示我们在一个非 void 函数中没有返回语句。又去看了下 Stack Overflow,里面指出,如果在使用 GCC 编译的时候指定了 -Wreturn-type,那么就会有这个警告。

但是,在 Dev C++ 里却完全没有警告。

检查了下 Dev C++ 里的设置,发现没有打开  -Wreturn-type,这就是为什么会编译通过。为了避免出现一些奇怪的问题,我们加上这个选项。

加上之后,果然再次编译的时候就会报 Warning,而且直接编译不过。

看来我们学校装 Dev C++ 的人还需要学习一个啊,不加这个坑死萌新。

评论

  1. kk
    2年前
    2022-5-10 20:07:55

    好文

  2. tkk
    3年前
    2020-10-11 14:22:41

    太秀了 :biggrin:

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇