提问人:mediocrevegetable1 提问时间:2/16/2021 最后编辑:philipxymediocrevegetable1 更新时间:4/3/2021 访问量:1779
索引“无符号长整型”变量并打印结果
Indexing an `unsigned long` variable and printing the result
问:
昨天,有人向我展示了这段代码:
#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
&foo
unsigned char *
i
*(((unsigned char *) &foo) + i)
((unsigned char *) &foo)[i]
foo
unsigned long
unsigned char *
char *
int
char
itoa()
506097522914230528
0 1 2 3 4 5 6 7
答:
作为前言,该程序的运行方式不一定与问题中的运行方式完全相同,因为它表现出实现定义的行为。除此之外,稍微调整程序也会导致未定义的行为。最后将有更多关于这方面的信息。
该函数的第一行将 定义为 。乍一看这似乎令人困惑,但在十六进制中它看起来像这样:.main
unsigned long foo
506097522914230528
0x0706050403020100
此数字由以下字节组成:。到现在为止,您可能可以看到它与输出的关系。如果您仍然对如何将其转换为输出感到困惑,请查看 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!现在,假设实际上是一个数组,如下所示:long
long
{00, 01, 02, 03, 04, 05, 06, 07}
我们得到 with 的地址,将其转换为 an 以隔离两位数,然后使用指针算术基本上得到 if 是一个 8 个字节的数组。正如我在问题中提到的,这可能看起来不那么令人困惑。foo
&foo
unsigned char *
foo[i]
foo
((unsigned char *) &foo)[i]
有点警告:此程序表现出实现定义的行为。这意味着该程序不一定以相同的方式工作/为所有 C 实现提供相同的输出。在某些实现中,不仅有 32 位长,而且当我们声明 时,它存储字节的方式/顺序(又名字节序)也是实现定义的。感谢 @philipxy 首先指出了实现定义的行为。这种类型的双关语会导致@Ruslan指出的另一个问题,即如果将 转换为 / 以外的任何内容,则 C 的严格别名规则会发挥作用,您将获得未定义的行为(链接的功劳也归@Ruslan)。关于这两点的更多细节,请参阅评论部分。unsigned long
0x0706050403020100
long
char *
unsigned char *
评论
%c
_Alignof(char)
1
sizeof(char)
unsigned char*
int64_t
已经有一个答案解释了代码的作用,但是由于某种原因,这篇文章引起了很多奇怪的关注,并且由于错误的原因而反复关闭,这里有一些关于代码的作用,C保证了什么和不保证什么的更多见解:
unsigned long foo = 506097522914230528;
.这个整数常数是 506 * 10^15 大。该值可能适合也可能不适合 ,具体取决于系统上是 4 字节还是 8 字节大(实现定义)。unsigned long
long
如果是 4 字节,这将被截断为 1)。
long
0x03020100
在 8 字节的情况下,它可以处理高达 18.44 * 10^18 的数字,因此该值将适合。
long
((unsigned char *) &foo)
是有效的指针转换和明确定义的行为。C17 6.3.2.3/7 做出以下保证:指向对象类型的指针可以转换为指向其他对象类型的指针。如果生成的指针未与引用类型正确对齐,则行为为 定义。否则,当再次转换回来时,结果应等于 原始指针。
对对齐的担忧不适用,因为我们有一个指向字符的指针。
如果我们继续阅读 6.3.2.3/7:
当指向对象的指针转换为指向字符类型的指针时, 结果指向对象的最低寻址字节。连续递增的 结果,直到对象的大小,都会产生指向对象剩余字节的指针。
这是一个特殊的规则,允许我们通过字符类型检查 C 中的任何类型。连续的增量是由 a 还是指针算术完成的并不重要。只要我们一直指向被检查的物体内,就可以确保。这是明确定义的行为。
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
严格来说,用于打印字符是不正确的,因为这样就需要 .但是,由于是一个可变参数函数,因此它带有一些奇怪的隐式提升规则,使此代码定义良好。该值将通过默认参数 promotions 2) 提升为类型。 然后在内部将其重新解释为 .它不能是负值,因为我们从 .转换3) 定义明确且可移植。
%u
printf
unsigned int
printf
unsigned char
int
printf
int
unsigned int
unsigned char
因此,我们逐个获取字节值。十六进制表示形式是,但它是如何存储在特定于 CPU 的/实现定义的行为中。这又是一个非常常见的 FAQ,请参阅什么是 CPU 字节序?,其中包含与此代码非常相似的示例。
07 06 05 04 03 02 01 00
unsigned 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以外的另一种整数类型时,如果该值可以用新类型表示,则该值保持不变。”
评论
sizeof(unsigned long)
long
0 1 2 3
3 2 1 0
评论
long
char *