如何解决索引一个 `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]
书写方式,但这使它看起来像 foo
,unsigned long
正在被索引。如果是这样,为什么?循环的其余部分似乎是典型的打印数组的所有元素,所以一切似乎都表明这是真的。对 unsigned char *
的强制转换使我更加困惑。我尝试专门在谷歌上搜索有关将整数类型强制转换为 char *
的问题,但在一些关于将 int
强制转换为 char
、itoa()
等的无用搜索结果之后,我的研究陷入了困境。{ {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) 提升为输入int
。printf
然后在内部将此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 举报,一经查实,本站将立刻删除。