为什么 > 运算符不能正常工作?

Why does the > operator not work correctly?

在此代码中:

#include <stdio.h>

int main()
{
    int a;
    int b;

    scanf("%d", &a);
    printf("\n");
    scanf("%d", &b);

    if ((float)a/b > 0.6)
    {
        printf("congratulations");
    }
    
    return 0;
}

当我输入 6 作为 a 和 10 作为 b 时,打印了“恭喜”,但我认为不应该打印,因为在这种情况下 a/b 是 0.6。

您的程序的行为取决于 floatdouble 类型的实现:

(float)a/b 可能使用 float 算术计算并产生不同于 0.6 的结果,其类型为 double。两者都可能是值 六十 的近似值,使用基数为 2 的浮点系统无法精确表示。在您的系统上,前者大于后者,因此会打印 congratulations,但在不同的系统上,行为可能会有所不同,尤其是在类型 floatdouble 使用相同的系统实现的系统上表示。

这里有一个更详细的说明:

#include <stdio.h>

int main() {
    int a, b;

    printf("Enter a and b: ");
    if (scanf("%d%d", &a, &b) != 2)
        return 1;

    printf("\na=%d, b=%d\n", a, b);

    if ((float)a/b > 0.6)   printf("(float)a/b > 0.6\n");
    if ((float)a/b == 0.6)  printf("(float)a/b == 0.6\n");
    if ((float)a/b < 0.6)   printf("(float)a/b < 0.6\n");
    if ((float)a/b > 0.6F)  printf("(float)a/b > 0.6F\n");
    if ((float)a/b == 0.6F) printf("(float)a/b == 0.6F\n");
    if ((float)a/b < 0.6F)  printf("(float)a/b < 0.6F\n");
    if ((double)a/b > 0.6)  printf("(double)a/b > 0.6\n");
    if ((double)a/b == 0.6) printf("(double)a/b == 0.6\n");
    if ((double)a/b < 0.6)  printf("(double)a/b < 0.6\n");

    // printing the values (converted to double when passed to printf)
    printf(" (float)a/b -> %.18g  (%#a)\n", (float)a/b, (float)a/b);
    printf("(double)a/b -> %.18g  (%#a)\n", (double)a/b, (double)a/b);
    printf("       0.6F -> %.18g  (%#a)\n", 0.6F, 0.6F);
    printf("       0.6  -> %.18g  (%#a)\n", 0.6, 0.6);

    return 0;
}

输出:

Enter a and b: 6 10

a=6, b=10
(float)a/b > 0.6
(float)a/b == 0.6F
(double)a/b == 0.6
 (float)a/b -> 0.60000002384185791  (0x1.333334p-1)
(double)a/b -> 0.599999999999999978  (0x1.3333333333333p-1)
       0.6F -> 0.60000002384185791  (0x1.333334p-1)
       0.6  -> 0.599999999999999978  (0x1.3333333333333p-1)

如你所见,(float)a/b0.6F完全相同,类型为float的常量,(double)a/b与[=17=相同]. C 标准不保证这一点,但除法的结果和常量都会产生最接近目标类型精确值的近似值。如您所见,0.6F 实际上大于六个十,而 0.6 更小。

另请注意编译器使用 -Weverything:

生成的许多有用警告
clang -O3 -funsigned-char -Weverything -Wno-padded -Wno-shorten-64-to-32 -Wno-missing-prototypes -Wno-vla -Wno-missing-noreturn -Wno-sign-co
nversion -Wno-unused-parameter -Wwrite-strings  -g -lm -lcurses -o sixtens sixtens.c
sixtens.c:12:17: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    if ((float)a/b > 0.6)   printf("(float)a/b > 0.6\n");
        ~~~~~~~~^~ ~
sixtens.c:13:20: warning: comparing floating point with == or != is unsafe [-Wfloat-equal]
    if ((float)a/b == 0.6)  printf("(float)a/b == 0.6\n");
        ~~~~~~~~~~ ^  ~~~
sixtens.c:13:17: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    if ((float)a/b == 0.6)  printf("(float)a/b == 0.6\n");
        ~~~~~~~~^~ ~~
sixtens.c:14:17: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    if ((float)a/b < 0.6)   printf("(float)a/b < 0.6\n");
        ~~~~~~~~^~ ~
sixtens.c:16:20: warning: comparing floating point with == or != is unsafe [-Wfloat-equal]
    if ((float)a/b == 0.6F) printf("(float)a/b == 0.6F\n");
        ~~~~~~~~~~ ^  ~~~~
sixtens.c:19:21: warning: comparing floating point with == or != is unsafe [-Wfloat-equal]
    if ((double)a/b == 0.6) printf("(double)a/b == 0.6\n");
        ~~~~~~~~~~~ ^  ~~~
sixtens.c:22:53: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    printf(" (float)a/b -> %.18g  (%#a)\n", (float)a/b, (float)a/b);
    ~~~~~~                                  ~~~~~~~~^~
sixtens.c:22:65: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    printf(" (float)a/b -> %.18g  (%#a)\n", (float)a/b, (float)a/b);
    ~~~~~~                                              ~~~~~~~~^~
sixtens.c:24:45: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    printf("       0.6F -> %.18g  (%#a)\n", 0.6F, 0.6F);
    ~~~~~~                                  ^~~~
sixtens.c:24:51: warning: implicit conversion increases floating-point precision: 'float' to 'double' [-Wdouble-promotion]
    printf("       0.6F -> %.18g  (%#a)\n", 0.6F, 0.6F);
    ~~~~~~                                        ^~~~
10 warnings generated.

作为 Brian Kernighan 和 P.J。 Plauger 曾经说过,*用浮点数做算术就像移动沙堆。每做一次,就丢掉一点沙子,捡起一点泥土。

在 David Goldberg 1991 年的 ACM 论文中了解更多相关信息 What Every Computer Scientist Should Know About Floating-Point Arithmetic

让我们稍微更改一下您的程序。一起来吧

if ((float)a/b > 0.6666666666666)

假设您输入 a=2 和 b=3。在这种情况下,很容易想象 a/b 可能会出现 0.6666666666667,它比 0.6666666666666 大(稍微大一点),所以程序可能会打印“恭喜”。

现在,在您原来的程序中,您认为这不是问题。 0.6是一个很好的偶数,6/10正好等于0.6,应该没有问题吧?

错了!

0.6 是一个十进制的“漂亮的偶数”,基数 10。但是你的计算机使用二进制,base 2.

众所周知,小数的1/3是无限循环的分数,0.33333333333333...。不太为人所知的是,在二进制中,1/10 是一个无限重复的分数,0.00011001100110011011...。所以 6/10 是 0.10011001100110011011,它也是无限重复的。

因为 1/10 是一个无限重复的二进制分数,事实证明我们可以写的几乎所有“漂亮的偶数”小数——比如 1.23 或 4.56 或 7.8910——都是 not 二进制中的精确分数。所以我们的程序中总是会遇到这些“舍入错误”!一方面,解决方案非常简单:只需调整您的想法,想象 所有 个分数是“不均匀”的分数,例如 1/3 = 0.33333333333333.

脚注:在二进制中,唯一“精确”的分数是涉及 1/2 次幂的分数,这并不奇怪。所以二进制的1/2正好是0.1,1/4就是0.01,21/32就是0.10101.