当 long 变量的地址存储在 char 指针中时会发生什么?

What happens when address of a long variable is store in char pointer?

我想了解当我将不同数据类型的指针存储到 char 指针时会发生什么。除了这两行的原因,我什么都明白:

…
char *cc;
long l;
…
cc = &l;
printf("\nc: %ld, cc: %u", *cc, cc);

正在打印:

c: 4294967211, cc: 591272048

而不是这个->

c: 171 (0xAB), cc: 591272048?

代码:

#include <stdio.h>    
int main()
{
  char c = 65, *cc;
  int i = 0x12345678;
  long l = 0x12345AB;
  float f = 3.14;

  cc = &c;
  printf("c: %c, cc: %u", *cc, cc);
  cc = &i;
  printf("\nc: %d, cc: %u", *cc, cc);
  cc = &l;
  printf("\nc: %ld, cc: %u", *cc, cc);
  cc = &f;
  printf("\nc: %f, cc: %u", *cc, cc);
}

打印:

c: A, cc: 591272063
c: 120, cc: 591272056
c: 4294967211, cc: 591272048
c: 0.000000, cc: 4294967235

您正在调用未定义的行为,并得到...未定义的结果!如果在您的目标架构上,就会发生这种情况:

  • char 类型已签名
  • int 类型是 32 位长
  • long 类型是 64 位长
  • 字节序是小字节序
  • 负数使用 2 的补码表示

然后在执行的时候

cc = &l;
printf("\nc: %ld, cc: %u", *cc, cc);

*ccl的最后一个字节,是0xAB(或-85)它被提升为有符号整数0xFFFFFFAB。在那之前你已经有很多未指定的行为......

但是当你在 printf 中使用 %ld 时,当你只传递一个被提升为 int 的 char 值时,程序会尝试读取一个小字节序 long。最高 32 位在这里恰好为 0,但那部分是 UB,任何事情都可能发生。但是作为一个 long,0xFFFFFFAB 确实是 4 294 967 211

但是当您调用 UB 时,您不能依赖它...

TL/DR:您的代码调用了未定义的行为,结果可能是任何东西。

cc = &i; 设置 cc 指向 i 的第一个字节。这个赋值违反了C标准的一个约束,所以编译器需要发出诊断信息,但它可能会接受这个程序。

printf("\nc: %d, cc: %u", *cc, cc);中,*cci第一个字节存储的值。您的 C 实现使用 eight-bit 字节并将 int 的字节存储在 little-endian 顺序中,这意味着 low-value 字节位于最低内存地址。 0x12345678的最低字节是7816,也就是12010,所以printf为此打印“120” .然后%u指令printf打印一个unsigned int,但是你传给它cc,这是一个指针。 C 标准未定义此行为。 printf 显然在这种情况下做了它能做的。您应该使用 %p 作为转换规范并使用 (void *) cc 作为参数来打印指针。

cc = &l;cc 设置为指向 l 的第一个字节,与上面的 i 一样。

printf("\nc: %ld, cc: %u", *cc, cc);中,*ccl的第一个字节,即AB16。在您的 C 实现中,char 是有符号和二进制补码,因此 AB16 的位被解释为 −85。这会自动提供给值为 −85 的 int,用 FFFFFFAB16 的位表示。然而,%ld 指示 printf 期待一个 long int,而你只传递了一个 int。这里可能发生的事情是,大约 32 个相邻的零位(可能在用于传递位 FFFFFFAB16 的同一寄存器中)可能已与 32 位 FFFFFFAB16 一起使用以形成位 00000000FFFFFFAB16,并且 printf 将其用作long int。作为 long int,这些位代表 4,294,967,211,因此 printf 打印出“4294967211”。

cc = &f;cc 设置为指向 f 的第一个字节,如上。

printf("\nc: %f, cc: %u", *cc, cc);中,*ccf的第一个字节。它是一些 char 值,再次提升为 int,但 %f 指示 printf 期望 double。在您的 C 实现中,double 个参数和 int 个参数可以在不同的处理器寄存器中传递。因此,当 printf 从它期望 double 的位置获取位时,它根本不会获取您传递的所有 int 的任何位。它确实得到的位可能全为零,在常见的 floating-point 编码方案中表示零,因此 printf 打印出“0.000000”。

同样在 printf 的输出中,我们看到“cc: 4294967235”。这个值 2,294,967,235 不同于我们看到的地址打印的其他值,例如 591,272,048。这样做的原因是,在 printf 试图为 %f 获取一个 double 值之后,它又试图为 %u 获取一个 unsigned 参数。它不是从传递第二个整数参数的地方获取它,就像在前面的 printf 调用中那样,而是从传递第一个整数参数的地方获取它。因此,它得到了为 *cc 传递的 int 值,而不是为 cc.

传递的地址

这个程序有很多问题;它包含多种形式的未定义行为。我建议使用这些设置开始编译:What compiler options are recommended for beginners learning C?

首先,这些指针赋值中的大多数在 C 中都是无效的 - 您不能将指向一种类型的指针赋给另一种 non-compatible C 中类型的指针(参见 C17 6.5.16.1 和 6.5。 4).您必须使用显式转换:
cc = (char*)&i;.

其次,在 printf 中使用一个特定的转换说明符但按不同类型传递变量是未定义的行为。同样,您必须使用 %p 打印指针,并且在打印之前应将指针转换为 void*

您应该始终将 \n 放在每个 printf 调用的末尾,因为在线缓冲系统中,\n 通常用于刷新标准输出并使内容显示在屏幕上。

在纠正了大部分无效 C/undefined behavior/bugs 的情况后,除了仍然使用错误的转换说明符之外,代码如下所示:

#include <stdio.h>

int main (void)
{
  char c = 65, *cc;
  int i = 0x12345678;
  long l = 0x12345AB;
  float f = 3.14;

  cc = &c;
  printf("c: %c, cc: %p\n",  *cc, (void*)cc);
  cc = (char*)&i;
  printf("c: %d, cc: %p\n",  *cc, (void*)cc);
  cc = (char*)&l;
  printf("c: %ld, cc: %p\n", *cc, (void*)cc);
  cc = (char*)&f;
  printf("c: %f, cc: %p\n",  *cc, (void*)cc);
}

这段代码仍然会调用未定义的行为,因此不能保证您会得到任何特定的结果。当我 运行 它在 gcc x86_64 Linux 上时,我得到与你相同的输出。

如果我们试图推断编译器在这里做什么,那么在所有情况下,您传递给 printf 的字符都会隐式提升为 int。这是因为 printf 是一个“可变参数函数”,并且这些函数带有隐式参数提升的特殊规则,称为“默认参数提升”。这意味着您不妨输入 (int)*cc,您会得到相同的结果。

在此处的小端计算机上,字符指针指向较大类型的最低有效字节。即0x78、0xAB等。

碰巧的是,char 类型对于像这样进行 hardware-related 编程是有问题的,因为它具有 implementation-defined 符号性。这意味着编译器可以让它的范围为 -128 到 127(2 的补码)或 0 到 255。在你的情况下,它选择了前者。通过 char* 读取时,像 0xAB 这样的常量将被视为负值 -85。

然后在传递给 printf 时提升到 int,这个负值得到“符号扩展”——编译器试图保留十进制值 -85。但由于它现在以 32 位 int 存储,因此该值的二进制表示形式为 0xFFFFFFAB。如果我们尝试用 %d 打印它,我们会得到 -85。如果转换为无符号整数,我们将得到 4294967211.

但在 %ld 和 8 字节数据的情况下,值 4294967211 适合一个。隐式提升总是转到类型 int。所以打印出一个正值。如果您将 (long)*cc 显式转换为 printf,那么它将像之前那样进行符号扩展,并打印 -85.

如您所知,在处理原始数据时使用 char 或任何有符号类型是个坏主意。最好使用 stdint.h 中的无符号整数类型。即 uint8_tuint64_t.