索引一个 `unsigned long` 变量并打印结果

med*_*le1 31 c pointers casting char-pointer implementation-defined-behavior

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

这导致:

0 1 2 3 4 5 6 7
Run Code Online (Sandbox Code Playgroud)

我很困惑,主要是for循环中的行。据我所知,似乎&foo是被强制转换为 anunsigned char *然后被i. 我觉得*(((unsigned char *) &foo) + i)是一个更详细的书写方式((unsigned char *) &foo)[i],但是这使得它看起来像foo,一个unsigned long被索引。如果是这样,为什么?循环的其余部分似乎是典型的打印数组的所有元素,所以一切似乎都表明这是真的。演员unsigned char *阵容让我更加困惑。我试图寻找有关转换的整数类型,以char *对谷歌而言,但我的研究得到了一些后无用的搜索结果停留约铸造intcharitoa()506097522914230528专门打印出0 1 2 3 4 5 6 7,但其他数字似乎在输出中显示了自己独特的 8 个数字,并且更大的数字似乎填充了更多的零。

med*_*le1 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));
Run Code Online (Sandbox Code Playgroud)

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

{00, 01, 02, 03, 04, 05, 06, 07}
Run Code Online (Sandbox Code Playgroud)

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


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

  • 不确定所有这些关于对齐和严格别名的注释的用途。当然,这些都是 unsigned char 以外的类型的问题,但这个示例*确实*使用了 unsigned char,所以在这方面它没问题。该示例确实是实现定义的,因为 unsigned long 可能不是 64 位宽,也可能不是小端,所以如果您要批评它,请在此基础上批评它。 (9认同)
  • 再次强调:只有当实现以某种方式定义了某些“实现定义的行为”时,该程序才有意义。这是一个 C 技术术语,研究一下。它与您的答案相关,因为您的答案声称没有理由该程序做了某件事,并且只有在某些实现定义的情况下才是合理的。如果您认为该语言是根据您的帖子定义的,那么您就错了。当然,代码的作者有这样的期望,这影响了他们编写代码,无论这是否适合他们的期望。 (6认同)
  • 为了获得额外的积分,请尝试将数字更改为 2314886970912564552,并将 printf 格式更改为“%c”。或者也许是 73​​08324466019755382。 (3认同)
  • @AndrewHenle:幸运的是,“_Alignof(char)”保证为“1”,与“sizeof(char)”相同,因此创建甚至取消引用“unsigned char*”到对象总是安全的。另请注意,虽然 ISO C 没有定义创建未对齐指针的行为,但某些实现确实定义了它(例如,因为它们必须不遗余力地破坏此类代码,并且因为某些扩展需要它,[就像英特尔的 SIMD 内在函数一样](//stackoverflow.com/a/57676035))。当然,由于 UB 的原因,未对齐的 `int64_t` 的 *deref* [即使在 x86 上也不安全](//stackoverflow.com/a/47512025)。 (3认同)
  • 为了使该程序有意义(例如在您描述的意义上),某些实现定义的行为必须由实现定义,但您不讨论或识别它。 (2认同)

Lun*_*din 11

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


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

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

    在 8 byte 的情况下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值将被默认参数提升 2)提升为类型intprintf然后在内部将其重新解释intunsigned int. 它不能是负值,因为我们从unsigned char. 转换3)定义明确且可移植。

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

    在小端会打印1 2...,在大端会打印7 6...


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


归档时间:

查看次数:

1182 次

最近记录:

4 年,5 月 前