【问题标题】:weird difference between int pointer and float pointerint指针和float指针之间的奇怪区别
【发布时间】:2011-10-01 02:20:54
【问题描述】:

请在下面查看我的代码

#include <stdio.h>
#include <stddef.h>

typedef struct _node
{
int a;
char *s;
}Node, *nodePtr;

int main(int argc, char *argv[])
{
char *str = "string"; /*str points to satic storage area*/
Node nd;
nodePtr pNode = NULL;
size_t offset_of_s = offsetof(Node,s);

nd.a = 1;
nd.s = str;

pNode = &nd;

    /*Get addr of s, cast it to a different data types pointer, then de-reference it*/

/*this works, print "string"*/
printf("%s\n", *(int*)((char*)pNode + offset_of_s));

/*this sucks, print (null)*/
printf("%s\n", *(float*)((char*)pNode + offset_of_s));

return 0;
} 

我尝试获取Node结构的s成员的地址,cast它为不小于4字节的数据类型(4 strong> 字节是我机器上 pointer 的宽度),然后将指针取消引用作为 printf 的参数。

我确实认为两个 printfs 的结果应该是相同,但第二个显示 "(null)"。

float 和 int 在我的机器上具有相同的字节宽度,是导致这种情况的两种类型的内部不同表示吗?

提前致谢!

【问题讨论】:

    标签: c pointers floating-point int printf


    【解决方案1】:

    您的程序调用未定义的行为,因为 printf() 的参数类型不是 printf 所期望的。没有办法通过查看源代码来预测结果。

    C99-TC3,§7.19.6.1/9

    如果任何参数不是相应转换规范的正确类型,则行为未定义。

    但是,如果您对所观察到的行为的原因感兴趣,那么您的编译器很可能是将浮点值传递给浮点 CPU 寄存器中的 printf() 的编译器之一。 (例如,GNU 和 CLang 就是这样做的)。第二次调用 printf 将取消引用的值放在浮点寄存器中,但printf 看到%s 转换说明符,查看了将传递char* 的寄存器,可能是通用寄存器,在您的情况下恰好为零。

    PS:这是 GCC 4.6.1 在我的 linux 上的成果

    main:
        pushq   %rbx
        leal    .LC0(%rip), %ebx
        movl    $.LC1, %esi
        subq    $16, %rsp
        movl    %ebx, %edx
        movl    $1, %edi
        movq    $.LC0, 8(%rsp)
        xorl    %eax, %eax
        call    __printf_chk
    
        movd    %ebx, %xmm0
        movl    $.LC1, %esi
        movl    $1, %edi
        movl    $1, %eax
        unpcklps    %xmm0, %xmm0
        cvtps2pd    %xmm0, %xmm0 # this is where your value went
        call    __printf_chk     # is NOT gonna read from xmm0!
    
        addq    $16, %rsp
        xorl    %eax, %eax
        popq    %rbx
        ret
    

    clang 2.9 也是如此

        ...
        movl    $.L.str, %ebx
        xorb    %al, %al
        movl    $.L.str1, %edi     # .L.str1 is your format "%s\n"
        movl    $.L.str, %esi      # .L.str  is your static "string"
        callq   printf
    
        movd    %ebx, %xmm0        # your value is in xmm0 again
        cvtss2sd    %xmm0, %xmm0   # promoted to double, but still in xmm0
        movb    $1, %al
        movl    $.L.str1, %edi
        callq   printf             # printf has no idea
    

    【讨论】:

    • +1,在 UB 上是的,但输出 (null) 的原因与浮点寄存器无关——在可变参数函数中,floats 被提升为 @987654330 @ 作为默认参数提升的一部分。提升的double 值当然与原始指针无关;它的前四个字节恰好是 NULL 是一个幸运的巧合。
    • @Adam Rosenfield 添加了程序集输出
    • 好点。但是,可变参数函数中 float 到 double 的提升通常会导致混淆和/或错误,因为人们试图用 printf/scanf 做一些古怪的事情。
    • @cubbi 谢谢你的深入解释,但是如何理解汇编代码,有没有手册让我先看一下?
    【解决方案2】:

    您的期望显然是基于您认为可变参数函数的可变参数以某种特定方式传递给这些函数的信念。这已经非常依赖于实现了,所以从正式的 C 语言来看,你的实验已经没有什么意义了。

    我猜你希望可变参数作为原始内存块复制到某种类型的“可变参数数组”(堆栈帧?),而不管它们的特定于类型的语义如何。出于这个原因,您显然认为应该以与 float 参数完全相同的方式传递 int 参数,因为这两种类型在您的平台上恰好具有相同的大小。

    这个假设是完全没有根据和不正确的。在这种情况下,实际传递给printf 的是所讨论参数的值,并且由于这些值具有完全不同的特定于类型的语义,因此它们可以很容易地以完全不同的方式传递。不用说,您的代码行为未定义的原因不止一个。

    在这种情况下,您需要了解的一个基本问题是,将 float 值作为可变参数函数的可变参数传递是完全不可能的。根据语言规范的要求,所有float 值在传递之前都会自动提升为double 值。 (这同样适用于char 和short 值,它们总是首先提升为int。)考虑到在您的情况下,float 值是通过重新解释指针对象占用的内存获得的,然后提升为double,你观察到的结果毫无意义也就不足为奇了。

    您需要了解的另一件基本事情是,C 语言不允许重新解释由一种类型的对象和另一种类型的对象占用的内存(从某种意义上说,结果行为是未定义的)。不允许将指针对象占用的内存重新解释为 int 对象。这正是你想要做的。即使是您的第一个 printfs,据称“按预期工作”,也只是偶然。

    【讨论】:

    • 如果在 C 中不允许将一种类型的对象占用的内存重新解释为另一种类型的对象,那么强制转换是如何工作的,比如 (char*)pNode 在我的代码中,我只是将指向 Node 的指针重新解释为 char 指针来执行我的指针算术
    • @Tracy:不,你没有。当您执行(char*) pNode 时,您没有重新解释 pNode 为char *。您将pNode转换为char * 类型。 conversion 和 reinterpretation 有很大的不同。转换没问题。重新解释不是。为了将pNode重新解释为char *,您必须这样做:*(char **) &amp;pNode。最后一个例子是重新解释。看到不同?并且通常不允许重新解释。它会导致未定义的行为。
    • @Tracy:另外,C 中有不同级别的“不允许”。有所谓的约束冲突,即编译器错误。还有未定义的行为之类的东西。基本上,一些“不允许”应该被编译器捕获,而另一些“不允许”你应该自己捕获。在您的示例中,如果您将 (char*) pNode 转换替换为 *(char **) &amp;pNode 重新解释的机会是代码仍将像以前一样“工作”。然而, 的行为仍然是未定义的,因此在这个意义上它是“不允许的”。
    • 是 conversion 像 reinterpret_cast 以外的转换,reinterpretation 像 c++ 中的 reinterpret_cast 吗?这是否意味着在具有完整语言支持的 C++ 中允许重新解释?
    【解决方案3】:

    是的。 a float 和 an integer 在二进制中的内部表示有很大不同。

    【讨论】:

      【解决方案4】:

      如果您想要地址,请使用带有 printf() 的“%p”格式说明符。从 K&R2 开始,它就一直在 C 中,也许更早。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-18
        • 2023-04-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多