这个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 == 3000
。 main
function如下所示:
main.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
,所以程序只能在屏幕上打印一个false 。 但编译并运行后,还会显示执行的内容:
$ 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.xx默认仍然使用旧的C标准。 你可能没有理由把你的代码编译为标准的C,所以你必须明确地告诉GCC它应该把你的代码编译成现代的C代码,而不是25年前的非标准的GNU废话。
通过总是编译你的程序来解决这个问题:
gcc -std=c11 -pedantic-errors -Wall -Wextra
-
-std=c11
告诉它根据(当前)C标准(非正式地被称为C11)做一个半心半意的编译。 -
-pedantic-errors
告诉它全心全意地执行上述操作,并在编写违反C标准的错误代码时给编译器提供错误。 -
-Wall
意味着给我一些额外的警告,可能是好的。 -
-Wextra
意味着给我一些其他额外的警告,可能是好的。
你没有在main.c中声明f1()
的原型,所以它被隐含地定义为int f1()
,这意味着它是一个函数,它接收未知数量的参数并返回一个int
。
如果int
和bool
大小不同,则会导致未定义的行为 。 例如,在我的机器上, 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
的声明:
bool f1(void);
请注意,参数列表显式设置为void
,它告诉编译器该函数不带任何参数,而不是空参数列表,这意味着未知数量的参数。 f1.c中的定义f1
也应该改变以反映这一点。
我觉得很有意思的是,看到伦丁出色的回答中提到的大小不匹配的情况。
如果使用--save-temps
编译,您将获得可以查看的汇编文件。 这里是f1()
进行== 0
比较的部分,并返回它的值:
cmpl $0, -4(%rbp) sete %al
返回部分是sete %al
。 在C的x86调用约定中,通过寄存器%eax
返回4个字节或更小(包括int
和bool
)的返回值。 %al
是%eax
的最低字节。 所以, %eax
的高3字节处于非受控状态。
现在在main()
:
call f1 testl %eax, %eax je .L2
这将检查整个 %eax
是否为零,因为它认为它正在testing一个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
没有必要,请尽量至less使用-Wall -Werror
它会为你节省很多的痛苦。