This C function should always return false, but it doesn’t
很久以前我在一个论坛上偶然发现了一个有趣的问题,我想知道答案。
考虑以下C函数:
F1.C
1 2 3 4 5 6 7 8 9
| #include <stdbool.h>
bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
} |
这应该总是返回false,因为var3 == 3000。main函数如下:
主C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h>
#include <stdbool.h>
int main ()
{
printf( f1 () == true ?"true
" :"false
");
if( f1 () )
{
printf("executed
");
}
return 0;
} |
由于f1()应该总是返回false,所以我们希望程序在屏幕上只打印一个错误。但在编译和运行之后,还会显示Executed:
1 2 3 4
| $ gcc main.c f1.c -o test
$ ./test
false
executed |
为什么会这样?此代码是否有某种未定义的行为?
注:我是用gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2编译的。
- 其他人提到您需要一个原型,因为您的函数在单独的文件中。但是,即使将EDCOX1与0 EDO复制到与main()相同的文件中,也会得到一些奇怪的结果:在C++中,对于一个空的参数列表,使用EDCOX1对2是正确的,在C中,它被用于一个尚未定义的参数列表的函数(它基本上期望在EDCOX1(3)后面有一个K&;R-风格的参数列表)。为了正确地使用C,您应该将代码更改为bool f1(void)。
- main()可以简化为int main() { puts(f1() == true ?"true" :"false"); puts(f1() ?"true" :"false"); return 0; },这将更好地显示差异。
- @当没有void时,uliwitness关于k&r 1st Ed(1978)的内容是什么?
- @乌利维斯塔K&R第1版中没有true和false,所以根本没有这样的问题。对错和对错都是0和非0。不是吗?我不知道当时是否有原型。
- K&AMP;R第一EDN之前的原型(和C标准)超过十年(1978的书与1989的标准)-事实上,C++(C类)仍然在未来当K&AMP R1出版。此外,在c99之前,没有_Bool型和型头段。
- 嗯,这条线是printf( f1() == true ?"true
" :"false
");行吗?我的意思是,它能确保按预期的顺序执行吗?!哪条规则可以确保这一点?
如其他答案所述,问题是您使用的gcc没有设置编译器选项。如果您这样做,它将默认为所谓的"GNU90",这是1990年撤销的旧C90标准的非标准实现。
在旧的C90标准中,C语言存在一个主要缺陷:如果在使用函数之前没有声明原型,那么它将默认为int func ()(其中( )表示"接受任何参数")。这改变了函数func的调用约定,但并没有改变实际的函数定义。由于bool和int的大小不同,因此在调用函数时,代码会调用未定义的行为。
这种危险的胡说八道的行为是在1999年修正的,随着C99标准的发布。隐式函数声明被禁止。
不幸的是,GCC到5.x.x版仍然默认使用旧的C标准。可能没有理由要将代码编译为标准C之外的任何代码。因此,您必须明确地告诉GCC,它应该将代码编译为现代C代码,而不是25岁以上的非标准GNU垃圾代码。
通过将程序编译为:
1
| gcc -std=c11 -pedantic-errors -Wall -Wextra |
- -std=c11告诉它要半心半意地按照(当前的)C标准(非正式地称为c11)编译。
- -pedantic-errors告诉它要全心全意地做上述工作,并且在编写违反C标准的错误代码时会给编译器带来错误。
- -Wall的意思是给我一些额外的警告,这可能是好事。
- -Wextra的意思是给我一些其他可能有好处的额外警告。
- 这个答案总体上是正确的,但是对于更复杂的程序来说,-std=gnu11比-std=c11更有可能像预期的那样工作,因为其中任何一个或全部:需要C11以外的库功能(posix、x/open等),这些功能在"gnu"扩展模式中可用,但在严格一致性模式中被抑制;系统头中的错误隐藏在扩展模式中,例如假设有非标准typedef可用;无意中使用三角图(在"gnu"模式下禁用此标准错误功能)。
- 出于类似的原因,虽然我通常鼓励使用高警告级别,但我不支持使用警告是错误模式。与-Werror相比,-pedantic-errors不那么麻烦,但两者都会导致程序无法在原始作者测试中未包含的操作系统上编译,即使没有实际问题。
- @ZWOL我怀疑这对除了Linux程序员以外的任何人都是个问题。我敢打赌,大多数C编程人员都不会不关心要编译一些乱七八糟的非标准Linux代码。我这辈子从来没有见过一个偶然的三角学错误。GCC警告无论如何都要使用三角图。
- @相反,我提到的第二个问题(通过严格的一致性模式暴露的系统头中的错误)是普遍存在的;我进行了广泛的系统测试,并且没有广泛使用的操作系统没有至少一个这样的错误(无论如何,从两年前开始)。在我的经验中,只需要C11的功能而不需要进一步添加的C程序也是例外,而不是规则。
- @ZWOL除了桌面操作系统之外,还有各种编程。例如,Linux PC中的所有计算机固件(硬盘、图形卡、BIOS、声卡、DVD等)可能都是用C编写的。
- @Lundin你是认真地声称那些程序都是用严格一致的c11编写的,并且那些(半独立的)实现,作为一个规则,没有通过在严格一致性模式下使用它们暴露的错误吗?因为我一纳秒都不相信。
- 最好的宣传,imho是:避免bool和bool,bool和bool。这是胡说,你不需要它。仅仅是一个普通的int(或char)也可以做到,并且可以减少混乱。
- @ZWOL可能不在严格的c11中,但它们可以用严格的c90或严格的c99编写。无论如何,它们肯定不会使用POSIX,也可能不会使用非标准的GCC扩展。
- 如果你使用标准的C EDOCX1,6 ED/EDOCX1,7,那么你可以用C + eScript的方式编写C代码,在这里你假设所有的比较和逻辑运算符都返回了一个ECDOX1的6度,就像C++一样,尽管他们在历史上返回了一个EDCOX1×9的C,因为历史的原因。这有一个很大的优势,您可以使用静态分析工具检查所有此类表达式的类型安全性,并在编译时公开所有类型的错误。它也是以自我记录代码的形式表达意图的一种方式。更不重要的是,它还节省了几个字节的RAM。
- @伦丁我用C写的原因是……我不想用C++编写。或者Java。或XML。并且:如果指向bool的指针是可能的,则bool不能比普通的旧字符更紧凑地表示。类型安全是一条红鲱鱼,这里是imho(见原始问题)
- @lundin非标准的gcc扩展,也许不是(尽管您可能会惊讶地发现有多少专有编译器实现了gcc扩展的一些子集)。一些编译器扩展是非标准的,几乎可以肯定是的。我的意思是,如果没有其他的东西的话,会有内联装配。在标题中。因此,当内联程序集扩展被禁用时,它无法编译。是的,我曾因这一点(通过VxWorks,IIRC的一些迭代)而被绊倒。
- @ZWOL大多数这样的编译器都有一个诀窍,即使在严格模式下编译时,它们也可以让您使用必需品,例如,如果您执行asm NOP;,它们可能会发出呜呜声,但如果您执行__asm NOP;,它们就不会发出呜呜声。类似于中断。无论如何,这里的重点是GNU扩展和POSIX对于许多应用程序来说都是完全无用的。GNU扩展尤其以非标准解决方案的形式打开了一整袋蠕虫,这些解决方案本来就是未定义的行为(就像一个例子:零长度数组)。
- @伦丁,我明白你的意思。我的观点是,严格的一致性模式比它们的价值更麻烦,因为在严格的一致性模式中,有太多松散的第三方代码漂浮在它们周围,而这不仅仅是Linux或GNU问题,而是普遍存在的。
- 我不喜欢这叫胡说八道。的确,这是危险的,而且确实,GCC的默认值不是标准值,但在那之前,它在每个C标准版本中都存在。由于许多GNU代码不会编译为c99,而是编译为ansi c,所以GCC在各个年龄段都默认使用旧版本。此外,直到x64,它才变得真正危险。
- @ZWOL:@lundin:我认为你们俩正在讨论一些相互正交的问题:开发自己的代码和只想让别人的草率代码编译GNU ISM之间有区别。我想说的是,对于新的东西,我们都应该使用-std=c11,如果必要的话,定义_POSIX_C_SOURCE或_XOPEN_SOURCE,或者其他需要的宏,以包含默认情况下不包含的头文件中所需的东西,但这决不会使默认情况下使一些你没有时间"修复"来工作的事情无效。与-std=c*一起。
- 注意,C99中的大部分新东西都来自于25岁以上的GNU垃圾。
- @哦,是的,GCC的非标准扩展是非常好的,作为一个操场沙盒,用于提议的语言更改。但这并不意味着每个人在开发新程序时都应该使用它们。当你还在学习C的时候,你绝对不应该使用它们。
- 你的"危险的胡说八道行为"被更好地称为"向后兼容当时的C代码库";如果不是那样的话,C89/C90将是一个完全的失败。
- @JCast最初设计C时,它不是"向后兼容性"。如果语言从一开始就设计得很好,就不需要这样做了。所有行与行之间的隐式转换可能是该语言的最大缺陷。
- @Lundin,最确定的是C最初设计时的向后兼容性。我建议你了解一下C的历史。
- @JCAST C在成为标准之前就已经设计好了。您是否建议非原型格式是与bcpl向后兼容所需的格式?我承认我对BCPL几乎一无所知,请告诉我函数原型是如何在那里工作的。
您没有在main.c中为f1()声明原型,因此它隐式定义为int f1(),这意味着它是一个接受未知数量参数并返回int的函数。
如果int和bool的大小不同,将导致行为不明确。例如,在我的机器上,int是4个字节,bool是1个字节。因为函数被定义为返回bool,所以当它返回时,会在堆栈上放置一个字节。但是,由于它被隐式声明为从main.c返回int,调用函数将尝试从堆栈中读取4个字节。
gcc中的默认编译器选项不会告诉您它正在这样做。但是,如果使用-Wall -Wextra编译,您将得到:
1 2
| main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’ |
要解决此问题,请在main.c中的f1前面添加一个声明,该声明位于main之前:
请注意,参数列表显式设置为void,它告诉编译器函数不接受参数,而空参数列表则意味着未知数量的参数。f1.c中的f1定义也应该改变以反映这一点。
- 在我的项目中(当我仍然使用gcc的时候)我曾经做过的事情是将-Werror-implicit-function-declaration添加到gcc的选项中,这样这个选项就不会再被忽略了。一个更好的选择是-Werror把所有的警告变成错误。强制您在所有警告出现时修复它们。
- 您也不应该使用空括号,因为这样做是一个过时的特性。这意味着他们可以在C标准的下一个版本中禁止这种代码。
- @乌利维斯塔啊。对那些只涉足C.的C++者有很好的信息
- 返回值通常不放入堆栈,而是放入寄存器。看欧文的回答。此外,通常不会将一个字节放入堆栈,而是将字大小的倍数放入堆栈。
- 更新版本的gcc(5.x.x)给出了不带额外标志的警告。
我觉得有趣的是,看看Lundin的优秀答案中提到的大小不匹配实际上发生在哪里。
如果使用--save-temps编译,您将获得可以查看的程序集文件。下面是f1()进行== 0比较并返回其值的部分:
1 2
| cmpl $0, -4(%rbp)
sete %al |
返回部分为sete %al。在C的x86调用约定中,返回值4字节或更小(包括int和bool通过寄存器%eax返回。%al是%eax的最低字节。因此,%eax的上3个字节处于不受控制的状态。
现在在main()中:
1 2 3
| call f1
testl %eax, %eax
je .L2 |
这将检查整个EDOCX1[6]是否为零,因为它认为它正在测试一个int。
添加显式函数声明会将main()更改为:
1 2 3
| call f1
testb %al, %al
je .L2 |
这就是我们想要的。
请使用如下命令编译:
1
| gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c |
输出:
1 2 3 4 5 6 7 8
| main. c: In function 'main':
main. c:14:5: error : implicit declaration of function 'f1' [-Werror =impl
icit -function -declaration ]
printf( f1 () == true ?"true
" :"false
");
^
cc1. exe: all warnings being treated as errors |
有了这样的信息,您应该知道如何纠正它。
编辑:在阅读(现在已删除)注释后,我试图编译没有标志的代码。嗯,这导致了链接器错误,没有编译器警告,而不是编译器错误。而那些链接器错误更难理解,所以即使不需要-std-gnu99,请尽可能多地使用-Wall -Werror,这会让你省去很多麻烦。