微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决索引一个 `unsigned long` 变量并打印结果

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

#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 被转换为 unsigned char *,然后由 i 添加。我认为 *(((unsigned char *) &foo) + i) 是一种更冗长的 ((unsigned char *) &foo)[i] 书写方式,但这使它看起来像 foounsigned long 正在被索引。如果是这样,为什么?循环的其余部分似乎是典型的打印数组的所有元素,所以一切似乎都表明这是真的。对 unsigned char * 的强制转换使我更加困惑。我尝试专门在谷歌上搜索有关将整数类型强制转换为 char * 的问题,但在一些关于将 int 强制转换为 charitoa() 等的无用搜索结果之后,我的研究陷入了困境。{ {1}} 专门打印出 506097522914230528,但其他数字似乎在输出显示了自己独特的 8 位数字,并且更大的数字似乎填充了更多的零。

解决方法

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

main 函数的第一行将 unsigned long foo 定义为 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));

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

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

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


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

,

已经有一个解释代码的作用的答案,但是由于这篇文章由于某种原因引起了很多奇怪的关注,并且由于错误的原因而反复关闭,这里有一些关于代码的作用、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 中的任何类型。连续递增是由 pointer++ 完成还是由指针算术 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 然后在内部将此 int 重新解释为 unsigned int。它不能是负值,因为我们从 unsigned char 开始。转换3) 定义明确且可移植。

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。