这个C函数应该总是返回false,但它没有

很久以前,我在一个论坛上偶然发现了一个有趣的问题,我想知道答案。

考虑下面的C函数:

f1.c

#include <stdbool.h>


bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
}

这应该总是返回false,因为var3 == 3000main函数看起来像这样:

c

#include <stdio.h>
#include <stdbool.h>


int main()
{
printf( f1() == true ? "true\n" : "false\n");
if( f1() )
{
printf("executed\n");
}
return 0;
}

由于f1()应该总是返回false,因此程序只能在屏幕上打印一个。但是在编译和运行它之后,执行也会显示出来:

$ gcc main.c f1.c -o test
$ ./test
false
executed

为什么呢?这段代码是否有某种未定义的行为?

注意:我用gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2编译了它。

20940 次浏览

在main.c中没有为f1()声明原型,因此它被隐式定义为int f1(),这意味着它是一个接受未知数量参数并返回int的函数。

如果intbool的大小不同,这将导致undefined behavior。例如,在我的机器上,int是4个字节,而bool是一个字节。由于函数是定义返回bool,所以返回时将一个字节放在堆栈上。然而,由于从main.c返回int隐式声明,调用函数将尝试从堆栈中读取4个字节。

gcc中的默认编译器选项不会告诉你它正在这样做。但是如果你用-Wall -Wextra编译,你会得到这个:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

为了解决这个问题,在main.c中添加f1声明,在main之前:

bool f1(void);

注意,实参列表显式设置为void,这告诉编译器函数不接受实参,而不是一个空形参列表,这意味着参数数量未知。在f1.c中的定义f1也应该被改变以反映这一点。

请使用如下命令进行编译:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

输出:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
printf( f1() == true ? "true\n" : "false\n");
^
cc1.exe: all warnings being treated as errors

有了这样的消息,你应该知道如何纠正它。

编辑:在阅读了一条评论(现在已删除)后,我试图在没有标记的情况下编译您的代码。好吧,这导致我链接错误没有编译器警告,而不是编译器错误。而且这些链接错误更难以理解,所以即使-std-gnu99不是必要的,也请尽量至少使用-Wall -Werror,这将为你省去很多麻烦。

正如在其他回答中提到的,问题是你使用gcc时没有设置编译器选项。如果您这样做,它默认为所谓的“gnu90”,这是旧的、从1990年撤回的C90标准的非标准实现。

在旧的C90标准中,C语言有一个重大缺陷:如果你在使用函数之前没有声明一个原型,它将默认为int func ()(其中( )表示“接受任何参数”)。这将改变函数func的调用约定,但不会改变实际的函数定义。由于boolint的大小不同,所以在调用函数时,代码会调用未定义的行为。

随着C99标准的发布,这种危险的无意义行为在1999年得到了修正。隐式函数声明被禁止。

不幸的是,GCC升级到版本5.x。x默认情况下仍然使用旧的C标准。您可能没有理由希望将代码编译为标准C以外的任何代码。因此,您必须明确地告诉GCC,它应该将您的代码编译为现代C代码,而不是一些25年以上的非标准GNU垃圾代码。

通过始终编译你的程序来解决这个问题:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11告诉它尝试按照(当前)C标准(非正式地称为C11)进行半心半意的编译。
  • -pedantic-errors告诉它全心全意地做上面的事情,并在你编写违反C标准的不正确代码时给出编译器错误。
  • -Wall的意思是给我一些额外的警告,这可能是好的。
  • -Wextra的意思是给我一些额外的警告,这可能是好的。

我认为,看看伦丁出色的答案中提到的尺寸不匹配实际上发生在哪里是很有趣的。

如果你用--save-temps编译,你会得到你可以查看的程序集文件。下面是f1()执行== 0比较并返回其值的部分:

cmpl    $0, -4(%rbp)
sete    %al

返回部分是sete %al。在C的x86调用约定中,4字节或更小的返回值(包括intbool)通过寄存器%eax返回。%al%eax的最低字节。因此,%eax的前3个字节处于未受控状态。

现在在main()中:

call    f1
testl   %eax, %eax
je  .L2

它检查%eax整个是否为零,因为它认为它在测试一个int型。

添加显式函数声明将main()更改为:

call    f1
testb   %al, %al
je  .L2

这就是我们想要的。