31

昨天,有人给我看了这段代码:

#include <stdio.h>

int main(void)
{
    unsigned long foo = 506097522914230528;
    for (int i = 0; i < sizeof(unsigned long); ++i)
        printf("%u ", *(((unsigned char *) &foo) + i));
    putchar('\n');

    return 0;
}

这导致:

0 1 2 3 4 5 6 7

我很困惑,主要是for循环中的行。据我所知,似乎&foo被转换为 anunsigned char *然后被i. 我认为这*(((unsigned char *) &foo) + i)是一种更冗长的写作方式,((unsigned char *) &foo)[i]但这看起来像是正在被索引。如果是这样,为什么?循环的其余部分似乎是典型的打印数组的所有元素,所以一切似乎都表明这是真的。演员阵容让我更加困惑。我尝试在 google 上专门搜索有关将整数类型转换为的内容,但在一些关于转换为、等的无用搜索结果专门打印出来后,我的研究陷入了困境foounsigned longunsigned char *char *intcharitoa()5060975229142305280 1 2 3 4 5 6 7,但其他数字似乎在输出中显示了自己唯一的 8 个数字,并且更大的数字似乎填充了更多的零。

4

2 回答 2

39

作为前言,这个程序不一定会像它在问题中那样运行,因为它表现出实现定义的行为。除此之外,稍微调整程序也会导致未定义的行为。最后有更多关于这方面的信息。

函数的第一行main定义了unsigned long fooas 506097522914230528。起初这似乎令人困惑,但在十六进制中它看起来像这样:0x0706050403020100.

该数字由以下字节组成:0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00. 现在,您可能可以看到它与输出的关系。如果您仍然对它如何转换为输出感到困惑,请查看 for 循环。

for (int i = 0; i < sizeof(unsigned long); ++i)
        printf("%u ", *(((unsigned char *) &foo) + i));

假设 along是 8 字节长,这个循环运行 8 次(记住,两个十六进制数字足以显示一个字节的所有可能值,并且由于十六进制数字中有 16 位,结果是 8,所以 for 循环运行八次)。现在真正令人困惑的部分是第二行。这样想:正如我之前提到的,两个十六进制数字可以显示一个字节的所有可能值,对吧?因此,如果我们可以分离出这个数字的最后两位,我们将得到一个字节值 7!现在,假设long实际上是一个如下所示的数组

{00, 01, 02, 03, 04, 05, 06, 07}

我们得到foowith的地址&foo,将其转换为 anunsigned char *以隔离两位数,然后使用指针算法基本上得到foo[i]iffoo是一个八字节数组。正如我在我的问题中提到的,这可能看起来不那么令人困惑,因为((unsigned char *) &foo)[i].


一点警告:这个程序表现出实现定义的行为。这意味着该程序不一定会以相同的方式工作/为所有 C 实现提供相同的输出。不仅在某些实现中是长 32 位,而且当我们声明 时unsigned long,它存储字节的方式/顺序of 0x0706050403020100(AKA endianness ) 也是实现定义的。感谢@philipxy 首先指出了实现定义的行为。这种类型的双关语会导致@Ruslan 指出的另一个问题,即如果将转换为除/long以外的任何内容,则 C 的严格别名规则char *unsigned char *开始发挥作用,您将获得未定义的行为(链接的信用也转到@Ruslan)。评论部分中关于这两点的更多详细信息。

于 2021-02-16T14:49:20.780 回答
11

已经有一个答案解释了代码的作用,但是由于这篇文章由于某种原因引起了很多奇怪的关注,并且由于错误的原因被反复关闭,这里有一些关于代码做什么、C 保证什么以及它做什么的更多见解不保证:


  • unsigned long foo = 506097522914230528;. 这个整数常量是 506 * 10^15 大。那个可能适合也可能不适合unsigned long,取决于long您的系统上是 4 字节还是 8 字节(实现定义)。

    在 4 字节的情况下long,这将被截断为0x03020100 1)

    在 8 字节的情况下long,它可以处理高达 18.44 * 10^18 的数字,因此该值将适合。

  • ((unsigned char *) &foo)是有效的指针转换和明确定义的行为。C17 6.3.2.3/7 做出以下保证:

    指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐引用的类型,则行为未定义。否则,当再次转换回来时,结果将等于原始指针。

    由于我们有一个指向字符的指针,因此对对齐的关注并不适用。

    如果我们继续阅读 6.3.2.3/7:

    当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续增量,直到对象的大小,产生指向对象剩余字节的指针。

    这是一个特殊规则,允许我们通过字符类型检查 C 中的任何类型。连续递增是由 apointer++还是由指针算术pointer + i完成并不重要。只要我们一直指向被检查的对象,这就i < sizeof(unsigned long)确保了。这是定义明确的行为。

  • 提到的另一个特殊规则“严格别名”包含字符的类似例外。它与 6.3.2.3/7 规则同步。具体来说,“严格别名”允许(C17 6.5/7):

    对象的存储值只能由具有以下类型之一的左值表达式访问:
    ...

    • 一种字符类型。

    在这种情况下,“存储的对象”unsigned long通常只能这样访问。但是,当unsigned char*取消引用时,*我们将其作为字符类型访问。这是上述严格别名规则的例外情况所允许的。

    作为旁注,反过来,unsigned char arr[sizeof(long)]通过*(unsigned long*)arr左值访问访问数组是严格的别名违规和未定义的行为。但这里不是这种情况。

  • 严格来说,使用%u打印字符是不正确的,因为printf那时需要一个unsigned int. 然而,由于printf它是一个可变参数函数,它带有一些奇怪的隐式提升规则,使得这段代码定义良好。该unsigned char值将由默认参数promotions 2)提升为类型intprintf然后在内部将其重新解释intunsigned int. 它不能是负值,因为我们从unsigned char. 转换3)定义明确且可移植。

  • 所以我们一一得到字节值。十六进制表示是07 06 05 04 03 02 01 00但如何存储在unsigned long特定于 CPU/实现定义的行为中。这又是一个非常常见的常见问题解答,请参阅什么是 CPU 字节序?其中包含与此代码非常相似的示例。

    在 little endian 上会打印1 2...,在 big endian 上会打印7 6...


1)见无符号整数转换规则 C17 6.3.1.3/2。
2) C17 6.5.2.2/6。
3) C17 6.3.1.3/1“当一个整数类型的值转换为_Bool以外的其他整数类型时,如果该值可以用新类型表示,则保持不变。”

于 2021-02-18T15:36:12.847 回答