这个C函数应该总是返回false,但它不会

Dim*_*ski 311 c gcc

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

考虑以下C函数:

在f1.c

#include <stdbool.h>

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

这应该总是返回falsevar3 == 3000.该main函数如下所示:

main.c中

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

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

由于f1()应该总是返回false,人们会期望程序只能在屏幕上打印一个false.但是在编译并运行它之后,还会显示执行:

$ gcc main.c f1.c -o test
$ ./test
false
executed
Run Code Online (Sandbox Code Playgroud)

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

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

Lun*_*din 391

如其他答案所述,问题是您使用gcc没有编译器选项集.如果你这样做,它默认为所谓的"gnu90",这是1990年旧的撤销C90标准的非标准实现.

在旧的C90标准中,C语言存在一个主要缺陷:如果在使用函数之前未声明原型,则默认为int func ()(其中( )表示"接受任何参数").这会更改函数的调用约定func,但不会更改实际的函数定义.由于大小boolint不同,您的代码在调用函数时会调用未定义的行为.

随着C99标准的发布,这种危险的无意义行为在1999年得到了修复.隐含的功能声明被禁止.

不幸的是,GCC版本5.xx仍然默认使用旧的C标准.可能没有理由为什么你想要将代码编译为标准C以外的任何东西.所以你必须明确告诉GCC它应该将你的代码编译为现代C代码,而不是25年以上的非标准GNU垃圾.

通过始终将程序编译为以下内容来解决问题:

gcc -std=c11 -pedantic-errors -Wall -Wextra
Run Code Online (Sandbox Code Playgroud)
  • -std=c11 告诉它根据(当前)C标准(非正式地称为C11)进行编译的半心半意.
  • -pedantic-errors 告诉它全心全意地执行上述操作,并在编写违反C标准的错误代码时给出编译器错误.
  • -Wall 意味着给我一些可能有好处的额外警告.
  • -Wextra 意味着给我一些额外的警告,可能会有好处.

  • 这个答案是完全正确的,但对于更复杂的程序,`-std = gnu11`比`-std = c11`更有可能按预期工作,因为以下任何一个或全部:需要超出C11的库函数(POSIX,X /打开等,在"gnu"扩展模式下可用,但在严格一致性模式下被抑制; 系统标头中隐藏在扩展模式中的错误,例如假设非标准typedef的可用性; 无意中使用三字符(在"gnu"模式下禁用此标准错误). (17认同)
  • @Lundin相反,我提到的第二个问题(严格一致性模式暴露的系统头中的错误)是*普遍存在的*; 我已经进行了广泛的系统测试,并且没有*没有广泛使用的操作系统,至少没有一个这样的错误(两年前,无论如何).仅需要C11功能的C程序,没有进一步的添加,也是我的经验中的例外而不是规则. (7认同)
  • @joop如果你使用标准的C`bool` /`_Bool`那么你可以用"C++ - esque"的方式编写你的C代码,你假设所有的比较和逻辑运算符都返回像C++一样的`bool`,即使由于历史原因,它们在C中返回一个`int`.这具有很大的优势,您可以使用静态分析工具检查所有此类表达式的类型安全性,并在编译时公开所有类型的错误.它也是一种以自我记录代码的形式表达意图的方式.而且不太重要的是,它还节省了几个字节的RAM. (6认同)
  • 请注意,C99中的大多数新内容都来自25岁以上的GNU垃圾. (6认同)
  • 出于类似的原因,虽然我通常鼓励使用高警告级别,但我不能支持使用警告 - 错误模式.`-pedantic-errors`比`-Werror'麻烦少,但是既可以又确实导致程序无法在原始作者测试中未包含的操作系统上编译,即使没有实际问题. (5认同)
  • @zwol:@Lundin:我认为你们两个正在讨论一些正交问题:开发你自己的代码和只是想要别人的草率代码之间存在差异,这些代码会对GNU-isms的编译做出假设.我想说新的东西,我们都应该使用`-std = c11`,如果有必要,定义`_POSIX_C_SOURCE`或`_XOPEN_SOURCE`或根据需要包含所需的其他宏来从头文件中包含默认情况下未包含的东西 - 但这绝不会使有时默认情况下工作没有时间"修复"以使用`-std = c*`的事实无效. (4认同)
  • @Lundin我用C编写的原因是..我不想用C++编写.或者Java.或XML.并且:如果指向Bool的指针是可能的,那么Bool不能比普通的旧char更紧凑.和类型安全是一个红鲱鱼,在这里恕我直言(见原始问题) (3认同)
  • 我不喜欢这被称为胡说八道.确实,这是危险的,而且确实gcc的默认值是没有标准的,但在那之前它是在每个C标准版本中.因为许多GNU代码不能编译为C99但是编译为ANSI C,所以gcc默认为旧版本的年龄和年龄.此外,它直到x64才变得非常危险. (3认同)
  • @zwol我怀疑这对Linux程序员来说是个问题.我敢打赌,大多数C程序员都不会在乎编写一些凌乱的,非标准的Linux代码.我一生中从未见过偶然的三字谜虫.无论如何,海湾合作委员会警告使用三卦. (2认同)
  • @zwol桌面操作系统之外还有各种编程.例如,Linux PC中的所有计算机固件(硬盘驱动器,图形卡,BIOS,声卡,DVD等)都可能用C语言编写. (2认同)
  • @Lundin您是否认真地声称这些程序都是严格遵循C11编写的,并且那些(半独立式)实现通常没有在严格一致性模式下使用它们而暴露的错误?因为我不相信纳秒. (2认同)
  • @zwol可能不是严格的C11,但它们可以用严格的C90或严格的C99编写.无论如何,他们肯定不会使用POSIX,也可能不会使用非标准的GCC扩展. (2认同)
  • 您的"危险的废话行为"更好地称为"与当时存在的C代码库的向后兼容性"; 如果它没有*以这种方式工作,C89/C90将完全失败. (2认同)
  • @jcast C 最初设计时并不是“向后兼容”。如果该语言从一开始就设计得正确,就不需要这样做。行与行之间发生的所有隐式转换可能是该语言的最大缺陷。 (2认同)

dbu*_*ush 139

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

如果intbool具有不同的大小,则会导致未定义的行为.例如,在我的机器上,int是4个字节,bool是一个字节.由于函数被定义为返回bool,因此它返回时会在堆栈上放置一个字节.但是,由于它被隐式声明int从main.c 返回,因此调用函数将尝试从堆栈中读取4个字节.

gcc中的默认编译器选项不会告诉您它正在执行此操作.但如果你编译-Wall -Wextra,你会得到这个:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’
Run Code Online (Sandbox Code Playgroud)

要解决此问题,请f1在main.c之前添加声明main:

bool f1(void);
Run Code Online (Sandbox Code Playgroud)

请注意,参数列表显式设置为void,它告诉编译器函数不带参数,而不是空参数列表,这意味着参数数量未知.f1还应更改f1.c中的定义以反映这一点.

  • 我在项目中曾经做过的事情(当我仍然使用GCC时)是在GCC的选项中添加了-Werror-implicit-function-declaration`,这样,这个选项就不再溜走了。更好的选择是-Werror将所有警告变为错误。强制您修复所有警告出现时的情况。 (2认同)
  • 您也不应该使用空括号,因为这样做是一个过时的功能.这意味着他们可以在下一版C标准中禁止使用此类代码. (2认同)

Owe*_*wen 35

我认为看看Lundin的优秀答案中提到的大小不匹配实际发生的地方很有意思.

如果使用编译--save-temps,您将获得可以查看的汇编文件.下面是其中的一部分f1()做了== 0比较,并返回其值:

cmpl    $0, -4(%rbp)
sete    %al
Run Code Online (Sandbox Code Playgroud)

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

现在main():

call    f1
testl   %eax, %eax
je  .L2
Run Code Online (Sandbox Code Playgroud)

这种检查是否整体%eax是零,因为它认为它是测试一个int.

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

call    f1
testb   %al, %al
je  .L2
Run Code Online (Sandbox Code Playgroud)

这就是我们想要的.


jda*_*nay 27

请使用如下命令编译:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

有了这样的信息,您应该知道如何纠正它.

编辑:在阅读(现已删除)注释后,我尝试编译没有标志的代码.好吧,这导致链接器错误没有编译器警告而不是编译器错误.而那些链接器错误更难以理解,所以即使-std-gnu99没有必要,请尽量使用至少-Wall -Werror它会为你节省很多痛苦.