【问题标题】:Moving printf to different lines gives different outputs? (C)将 printf 移动到不同的行会产生不同的输出? (C)
【发布时间】:2016-02-15 00:17:03
【问题描述】:

在 C 中,当我将此 printf 行:printf("%f\n", 5 / 2); 移动到不同的行时,它的输出会发生变化。有什么想法吗?

代码如下:

#include <stdlib.h>
#include <stdio.h>


int main()
{   

    int a = 65;
    char c = (char)a;
    int m = 3.0/2;

    printf("%c\n", c);              
    printf("%f\n", (float)a);       
    printf("%f\n", 5.0 / 2);        
    printf("%f\n", 5 / 2.0);        
    printf("%f\n", (float)5 / 2);   
    printf("%f\n", 5 / (float)2); 
    printf("%f\n", (float)(5 / 2)); 
    printf("%f\n", 5.0 / 2);        
    printf("%d\n", m);              
    printf("%f\n", 5 / 2);


    system("PAUSE");
    return(0);
}

输出如下:

A
65.000000
2.500000
2.500000
2.500000
2.500000
2.000000
2.500000
1
2.500000

如果我将printf("%f\n", 5 / 2); 移动到第一行之一(在输出 A 的行和输出 65.000000 的行之间),它将打印 0.000000(这是有道理的)而不是现在的 2.500000。 有什么想法吗?

【问题讨论】:

  • printf("%f\n", 5 / 2); 产生未定义的行为。 (整数)除法5 / 2 的结果是一个整数操作数,其值为2。您正在尝试以float 格式打印它(更不用说编译器在将其传递给printf 之前将其扩展为double,因此printf 将尝试从堆栈中读取8 个字节的数据,虽然它只保存sizeof(int) 字节的数据)。
  • 你不能用%f打印整数,就这么简单。
  • 根据 C99 标准关于 printf “如果任何参数不是相应转换规范的正确类型,则行为未定义。” (WG14 N1570,7.21.6.1/p9)。
  • ..所以它与 pintf 语句的含义无关,除了当 printf 尝试将整数解释为双精度时堆栈包含不同的(垃圾)值。
  • 这是一个愚蠢的问题。您是在问“如果我做了一些定义不明确的事情,结果是什么?”但这是未定义行为的本质,​​没有保证的结果。

标签: c casting int printf


【解决方案1】:

您的代码正在调用未定义的行为。

您有义务使用正确的数据说明符来打印printf 中的内容,否则会调用 UB。因此,在不同的地方得到不同的结果并不重要,也不足为奇。

http://en.cppreference.com/w/c/io/fprintf

如果转换规范无效,则行为未定义。

对于 c 也是如此。

当调用未定义的行为时,根据定义,结果是随机的且不可预测,因此要求我们预测它们毫无意义。

【讨论】:

    【解决方案2】:

    正如评论者所指出的,printf("%f\n", 5 / 2); 行只是表现出未定义的行为。但是让我们看看为什么在使用 System V ABI 的 x86-64 架构上可能会得到这样的结果。

    简短的回答是前几个参数是通过寄存器进行通信的。选择取决于参数的类型:整数参数进入“经典”寄存器(ediesi 等),浮点进入 SSE 寄存器(xmm0xmm1 等)。

    因为我们在格式字符串中给出了错误的类型,printf 正在从错误的寄存器中读取参数。


    让我们将您的程序简化为以下内容:

    #include <stdio.h>
    
    int main(void)
    {
        printf("%f\n", 5/2);
        printf("%f\n", 5.0/2);
        printf("%f\n", 5/2);
    
        return 0;
    }
    

    现在让我们来看看main 的反汇编。我们从函数序言开始,它并不太特别:

       push   %rbp
       mov    %rsp,%rbp
       sub    $0x10,%rsp
    

    然后,我们第一次调用printf,其中参数被传递给edi(它获取指向格式字符串的指针)和esi5/2,这是2,由于整数除法):

       mov    $0x2,%esi
       mov    $0x4005e4,%edi
       mov    $0x0,%eax
       callq  4003e0 <printf@plt>
    

    但是,printf 将读取"%f\n" 格式并尝试从xmm0 读取参数。在我的例子中,这个寄存器的值是0,所以这会打印出0.000000

    在第二次调用中,参数显然是一个浮点数,通过xmm0传递:

       movabs $0x4004000000000000,%rax
       mov    %rax,-0x8(%rbp)
       movsd  -0x8(%rbp),%xmm0
       mov    $0x4005e4,%edi
       mov    $0x1,%eax
       callq  4003e0 <printf@plt>
    

    现在,printf 打印出预期的2.500000(您在这里看到的是0x4004000000000000,这是 2.5 的 64 位浮点常量的样子)。我们通过xmm0 传递它,它从xmm0 读取它。

    第三次调用与第一次完全相同:

       mov    $0x2,%esi
       mov    $0x4005e4,%edi
       mov    $0x0,%eax
       callq  4003e0 <printf@plt>
    

    改变的是对printf 的调用没有改变xmm0 中的值。它仍然包含第二次调用之前的常量 2.5,因为我们第三次调用 printf。在第三次调用中,printf 将再次打印2.500000

    (当然,我们的函数以无聊的return 0结束:)

       mov    $0x0,%eax
       leaveq 
       retq   
    

    【讨论】:

    • 编译器对eax 所做的只是一个脚注:在 SysV x86-64 ABI 中,eax 涉及可变参数函数的参数列表,例如作为printf。从理论上讲,printf 可以检测到它的参数有问题(它在 UB 调用中得到 eax = 0,在正确调用中得到 eax = 1)。在实践中,printf 实现不会注意到这一点,因为&lt;stdarg.h&gt; 没有做出这种区分。
    猜你喜欢
    • 1970-01-01
    • 2016-02-01
    • 2014-05-06
    • 2014-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-14
    • 2021-10-07
    相关资源
    最近更新 更多