索引“无符号长整型”变量并打印结果

Indexing an `unsigned long` variable and printing the result

提问人:mediocrevegetable1 提问时间:2/16/2021 最后编辑:philipxymediocrevegetable1 更新时间:4/3/2021 访问量:1779

问:

昨天,有人向我展示了这段代码:

#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

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

c 指针 转换 char-pointer 实现定义的行为

评论

25赞 harold 2/16/2021
将506097522914230528转换为十六进制,这将更有意义。
9赞 Fred Larson 2/16/2021
想想小端序。
5赞 mediocrevegetable1 2/16/2021
@harold你是对的,它显示了706050403020100。这是否意味着我通过将它的地址转换为 a 并取消引用来将其视为某种数组?longchar *
5赞 user4815162342 2/16/2021
@mediocrevegetable1宾果游戏!
14赞 Sinatr 2/17/2021
这个问题正在 meta 上讨论

答:

39赞 mediocrevegetable1 2/16/2021 #1

作为前言,该程序的运行方式不一定与问题中的运行方式完全相同,因为它表现出实现定义的行为。除此之外,稍微调整程序也会导致未定义的行为。最后将有更多关于这方面的信息。

该函数的第一行将 定义为 。乍一看这似乎令人困惑,但在十六进制中它看起来像这样:.mainunsigned long foo5060975229142305280x0706050403020100

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

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

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

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

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


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

评论

3赞 Steve Summit 2/16/2021
为了获得额外的积分,请尝试将数字更改为 2314886970912564552,并将 printf 格式更改为 .或者也许是7308324466019755382。%c
2赞 philipxy 2/17/2021
为了使这个程序有意义(例如,在你所描述的意义上),某些实现定义的行为必须由实现定义,但你没有讨论或识别它。
6赞 philipxy 2/17/2021
再说一遍:只有当实现以某种方式定义某些“实现定义的行为”时,程序才有意义。这是一个 C 技术术语,研究一下。它与你的答案有关,因为你的回答没有理由地声称程序做了某件事,并且它只有在某些实现定义的情况下才是合理的。如果你认为语言是按照你的帖子来定义的,那你就错了。当然,代码的作者有这样的期望,这影响了他们编写代码,无论这是否适合他们的期望。
3赞 Peter Cordes 2/17/2021
@AndrewHenle:幸运的是,保证是 ,与 相同,因此创建甚至取消引用对象始终是安全的。另请注意,虽然 ISO C 没有定义创建未对齐指针的行为,但某些实现确实定义了它(例如,因为它们必须不遗余力地破坏此类代码,并且因为某些扩展需要它,例如英特尔的 SIMD 内部函数)。当然,由于 UB,即使在 x86 上deref 对未对齐也是不安全的。_Alignof(char)1sizeof(char)unsigned char*int64_t
9赞 Steve Summit 2/17/2021
不确定所有这些关于对齐和严格别名的评论是做什么用的。当然,这些是无符号字符以外的类型的问题,但此示例确实使用了无符号字符,因此在这方面很好。这个例子确实是实现定义的,因为无符号的 long 可能不是 64 位宽,也可能不是 little-endian,所以如果你要批评它,请在此基础上这样做。
11赞 Lundin 2/18/2021 #2

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


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

    如果是 4 字节,这将被截断为 1)。long0x03020100

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

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

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

    对对齐的担忧不适用,因为我们有一个指向字符的指针。

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

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

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

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

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

    • 字符类型。

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

    顺便说一句,反之亦然,通过左值访问访问数组是一种严格的别名冲突和未定义的行为。但这里的情况并非如此。unsigned char arr[sizeof(long)]*(unsigned long*)arr

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

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

    在小端上它会打印,在大端上它会打印。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以外的另一种整数类型时,如果该值可以用新类型表示,则该值保持不变。”

评论

0赞 mediocrevegetable1 2/19/2021
您好@Lundin,感谢您提供答案,以解决相关来源的一些评论。我没有意识到,当您定义一个值太大的变量时,会发生截断而不是溢出。多亏了 for 循环条件,这应该意味着即使在具有 32 位的系统上,该程序也应该打印 OR(取决于字节序,如您提到的),对吧?sizeof(unsigned long)long0 1 2 33 2 1 0
1赞 Lundin 2/19/2021
@mediocrevegetable1 严格来说,有一些数学模数:“否则,如果新类型是无符号的,则通过重复添加或减去一个比新类型中可以表示的最大值多一个来转换该值,直到该值在新类型的范围内。无符号变量不能溢出,只能环绕。如果你使用了有符号变量,那就另当别论了。