被同学的同学给逼死了……
问题提出
今天同学给我说,他们计导课上有个人在声明返回值为 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
寄存器里面的值。
我对过于底层的东西了解甚少,正一脸懵逼的时候,腹黑猫又发过来一张截图,是 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++ 的人还需要学习一个啊,不加这个坑死萌新。
好文
太秀了 :biggrin: