此C函数应始终返回false,但不返回


316

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

考虑以下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函数如下所示:

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,因此可以期望程序在屏幕上仅打印一个错误。但是编译并运行它之后,也会显示执行

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

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

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


9
其他人提到您需要原型,因为您的功能位于单独的文件中。但是,即使您将复制f1()到的文件复制到同一个文件中main(),也会有些奇怪:虽然在C ++中()用于空参数列表是正确的,但在C中用于具有尚未定义的参数列表的函数(它基本上期望在))之后有K&R样式的参数列表。要使用正确的C语言,您应该将代码更改为bool f1(void)
uliwitness '16

1
main()可以简化成int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }-这将显示出差异更好。
Palec

@uliwitness关于K&R第一版。(1978)什么时候没有void
Ho1 2016年

@uliwitness有不truefalse在K&R第1版,所以有不是在所有这样的问题。它的真假值为0,非零。是不是 我不知道当时是否有原型。
Ho1 2016年

1
K&R 1st Edn在原型(和C标准)之前已经有十多年的历史了(本书为1978年,标准为1989年)–实际上,当K&R1发行时,C ++(带有类的C)仍在未来。另外,在C99之前,没有_Bool类型,也没有<stdbool.h>标头。
乔纳森·莱夫勒

Answers:


396

如其他答案中所述,问题是您gcc未设置任何编译器选项。如果执行此操作,则默认为“ gnu90”,这是从1990年撤消的旧C90标准的非标准实现。

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

随着C99标准的发布,这种危险的废话行为在1999年得到了解决。隐式函数声明被禁止。

不幸的是,默认情况下,版本5.xx之前的GCC仍使用旧的C标准。可能没有理由将标准C之外的任何代码都编译为其他代码。因此,您必须明确告诉GCC它应将您的代码编译为现代C代码,而不是使用25年以上的非标准GNU废话。 。

通过始终将程序编译为以下方式解决此问题:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 告诉它做出全心全意的尝试,以按照(当前)C标准(非正式地称为C11)进行编译。
  • -pedantic-errors 告诉它全心全意地执行上述操作,并在编写违反C标准的错误代码时给出编译器错误。
  • -Wall 意味着给我一些可能有好处的额外警告。
  • -Wextra 意味着再给我一些其他的警告,可能会有所帮助。

19
这个答案总体上是正确的,但是由于以下任何或所有原因,-std=gnu11比起更复杂的程序,它更有可能按预期工作-std=c11:需要“ gnu”中可用的C11(POSIX,X / Open等)以外的库功能扩展模式,但在严格一致性模式下被抑制;隐藏在扩展模式下的系统头中的错误,例如,假定非标准typedef可用;意外使用三字组合(在“ gnu”模式下禁用了此标准功能)。
zwol 2016年

5
出于类似的原因,尽管我通常鼓励使用较高的警告级别,但我不支持使用警告为错误模式。 -pedantic-errors这样做虽然没有什么麻烦,-Werror但是即使没有实际问题,两者都可以并且确实会导致程序无法在原始作者的测试中未包含的操作系统上编译。
zwol 2016年

7
@Lundin相反,我提到的第二个问题(严格遵循模式公开的系统头文件中的错误)无处不在;我已经做了广泛,系统的测试,有没有广泛使用的操作系统不具有至少一个这样的错误(如两年前,反正)。仅凭C11的功能而无其他增加的C程序也是例外,而不是我的经验。
zwol 2016年

6
@joop如果使用标准C bool/,_Bool则可以以“ C ++式”的方式编写C代码,在这种情况下,由于历史原因,您假定所有比较和逻辑运算符bool在C ++中都返回一个like,即使它们在C ++中都返回了like int。这具有很大的优势,您可以使用静态分析工具来检查所有此类表达式的类型安全性,并在编译时公开各种错误。这也是一种以自我记录代码形式表达意图的方法。同样重要的是,它还节省了几个字节的RAM。
伦丁

7
请注意,C99中的大多数新内容都来自具有25年以上历史的GNU废话。
Shahbaz

141

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

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

gcc中的默认编译器选项不会告诉您它正在执行此操作。但是,如果您使用进行编译-Wall -Wextra,则会得到以下信息:

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

要解决此问题,请f1在main.c中的之前添加一个声明main

bool f1(void);

请注意,参数列表已显式设置为void,这告诉编译器该函数不接受任何参数,而空参数列表则意味着未知数量的参数。f1f1.c中的定义也应更改以反映这一点。


2
我在项目中曾经做过的事情(当我仍然使用GCC时)被添加-Werror-implicit-function-declaration到GCC的选项中,这样,这个选项就不再溜走了。更好的选择是-Werror将所有警告变为错误。强制您修复所有警告,当它们出现时。
uliwitness '16

2
您也不应使用空括号,因为这样做是过时的功能。意味着他们可以在下一个C标准版本中禁止此类代码。
伦丁

1
@uliwitness啊。对于那些由C ++来仅谁广泛涉猎C.良好的信息
SeldomNeedy

返回值通常不放入堆栈,而是放入寄存器。参见欧文的答案。另外,通常您永远不会将一个字节放入堆栈中,而是将字长乘以一倍。
rsanchez '16

较新版本的GCC(5.xx)会发出该警告,而没有额外的标志。
2016年

36

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

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

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

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

现在在main()

call    f1
testl   %eax, %eax
je  .L2

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

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

call    f1
testb   %al, %al
je  .L2

这就是我们想要的。


27

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

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它,这将为您节省很多麻烦。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.