很久以前我在论坛上偶然发现一个有趣的问题,我想知道答案。
考虑以下 C 函数:
#include <stdbool.h>
bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
}
自
false
起,这应该始终返回 var3 == 3000
。 main
函数如下所示:
#include <stdio.h>
#include <stdbool.h>
int main()
{
printf( f1() == true ? "true\n" : "false\n");
if( f1() )
{
printf("executed\n");
}
return 0;
}
由于
f1()
应始终返回 false
,因此人们会期望程序仅在屏幕上打印一个 false。但编译运行后,还显示execute:
$ gcc main.c f1.c -o test
$ ./test
false
executed
这是为什么呢?这段代码是否有某种未定义的行为?
注意:我是用
gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2
编译的。
如其他答案中所述,问题是您使用
gcc
时未设置编译器选项。如果这样做,它默认为所谓的“gnu90”,这是 1990 年撤回的旧 C90 标准的非标准实现。
在旧的 C90 标准中,C 语言有一个重大缺陷:如果在使用函数之前没有声明原型,它将默认为
int func ()
(其中 ( )
表示“接受任何参数”)。这改变了函数func
的调用约定,但并没有改变实际的函数定义。由于 bool
和 int
的大小不同,因此您的代码在调用函数时会调用未定义的行为。
随着 1999 年 C99 标准的发布,这种危险的胡言乱语行为得到了修复。隐式函数声明被禁止。
不幸的是,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
意味着给我一些其他可能会更好的额外警告。布什8===> SO是一场狗屎秀。感谢您的搭车。
我认为看看 Lundin 的出色答案中提到的尺寸不匹配实际发生在哪里很有趣。
如果使用
--save-temps
进行编译,您将获得可以查看的汇编文件。这是 f1()
进行 == 0
比较并返回其值的部分:
cmpl $0, -4(%rbp)
sete %al
返回的部分是
sete %al
。在 C 的 x86 调用约定中,4 字节或更小的返回值(包括 int
和 bool
)通过寄存器 %eax
返回。 %al
是%eax
的最低字节。因此,%eax
的高3个字节处于不受控制的状态。
现在在
main()
:
call f1
testl %eax, %eax
je .L2
这会检查 %eax
的
whole是否为零,因为它认为它正在测试 int。
添加显式函数声明将
main()
更改为:
call f1
testb %al, %al
je .L2
这就是我们想要的。
请使用如下命令进行编译:
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
它会为您省去很多麻烦。