将uint8_t*转换为uint64_t

3

如何更好地将偏移量为 iuint8_t数组转换为uint64_t类型?建议采用哪种方式,为什么?

uint8_t * bytes = ...
uint64_t const v = ((uint64_t *)(bytes + i))[0];


uint64_t const v = ((uint64_t)(bytes[i+7]) << 56)
                 | ((uint64_t)(bytes[i+6]) << 48)
                 | ((uint64_t)(bytes[i+5]) << 40)
                 | ((uint64_t)(bytes[i+4]) << 32)
                 | ((uint64_t)(bytes[i+3]) << 24)
                 | ((uint64_t)(bytes[i+2]) << 16)
                 | ((uint64_t)(bytes[i+1]) << 8)
                 | ((uint64_t)(bytes[i]));

1
使用 ((uint64_t*)(bytes + i))[0] 时,字节顺序取决于底层系统的大小端。而采用另一种方式,则可以明确选择所需的字节顺序。 - Some programmer dude
@Someprogrammerdude 我忘记提到了,bytes 和系统都是小端字节序,除此之外还有其他的区别吗? - err69
4
此外,一些架构对于uint64_t指针的对齐要求非常严格,因此重新解释uint8_t指针可能会引发异常(例如某些ARM架构)。 - Jack
先将您的 whatever * 转换为 intptr_t(自C99后可选类型),然后再转换为 uint64_t - Neil
3个回答

3

这里有两个主要的区别。

第一,根据C标准,((uint64_t *)(bytes + i))[0]的行为是未定义的(除非关于bytes指向的内容满足某些前提条件)。通常来说,不应该使用uint64_t类型来访问一个字节数组。

当以一种类型定义的内存使用另一种类型进行访问时,称之为别名。C标准只定义了某些别名组合。一些编译器可能支持一些超出标准要求的别名,但使用它们是不可移植的。此外,如果bytes + i没有适当地对齐到uint64_t,则访问可能会导致异常或其他故障。

第二,如果定义了通过别名加载字节(无论是通过标准还是通过编译器扩展),则使用C实现的内存排序解释这些字节。一些C实现将代表整数的字节从低地址到高地址按低位值字节到高位值字节的顺序存储在内存中,而另一些则将它们从高地址到低地址存储。(它们也可以以非连续的顺序存储,尽管这很少见。)因此,以这种方式加载字节将根据C实现使用的顺序从相同的存储在内存中的字节产生不同的值。

但是,加载字节并使用移位操作将始终从相同的存储在内存中的字节产生相同的值,而不管C实现使用的顺序如何。

第一种方法应该避免使用,因为没有必要。如果希望使用C实现的排序来解释这些字节,可以使用以下方法:

uint64_t t;
memcpy(&t, bytes+i, sizeof t);
const uint64_t v = t;

使用memcpy提供了一种便携式的方式来将字节存储到uint64_t中进行别名处理。优秀的编译器会识别这个惯用法,并且如果适用于目标架构(并启用了优化),则会将memcpy优化为从内存加载。

如果希望按照小端序解释字节,就像问题中的代码所示,则可以使用第二种方法。(有时平台可能会提供可以提供更高效代码的例程)


1
您也可以使用memcpy函数。
uint64_t n;
memcpy(&n, bytes + i, sizeof(uint64_t));
const uint64_t v = n;

2
memcpy和shifts的区别在于,在使用memcpy时,字节数组的字节顺序必须直接对应于CPU的字节序。而在使用shifts时,你只需要关心字节数组应该对应的顺序是什么。因此,如果字节数组例如是来自某个串行总线的输入缓冲区,那么在编写shifts时只需关注网络字节序。在使用memcpy时,你需要知道CPU和网络字节序,并且它们也必须匹配。 - Lundin

1
第一个选项存在两个大问题,可以归为未定义行为(任何事情都可能发生):
  • 一个uint8_t*uint8_t数组的对齐方式不一定与uint64_t这样的较大类型所要求的对齐方式相同。简单地强制转换为uint64_t*会导致错误的访问。这可能会引起硬件异常、程序崩溃、代码变慢等,所有这些都取决于特定目标的对齐要求。

  • 它违反了C语言的内部类型系统,编译器知道内存中的每个对象都有一个“有效类型”,并跟踪这个类型。基于这个,编译器可以做出某些关于是否访问了某个内存区域的假设。如果你的代码违反了这些类型规则,就像在这种情况下一样,就会生成错误的机器码。

    这通常被称为严格别名规则,你的强制类型转换后紧接着解引用将是所谓的“严格别名违规”。

第二个选项是正确的代码,因为:

在进行移位或其他形式的位运算时,应使用大整数类型,即unsigned int或更大,具体取决于系统。使用有符号类型或小整数类型可能会导致未定义的行为或意外结果。有关隐式类型提升规则的问题,请参见隐式类型提升规则
如果没有将其强制转换为uint64_t,那么bytes[i + 7] << 56移位操作将涉及从uint8_tint的左操作数的隐式提升,这将是一个错误。因为如果字节的最高有效位(MSB)被设置并且我们移位到/超出符号位,我们会引发未定义的行为-同样,任何事情都可能发生。
当然,在这种特定情况下,我们需要使用64位类型,否则我们将无法移动到56位。移位超出左操作数类型的范围也是未定义的行为。
请注意,选择 bytes[i+7] << 56 或者替代方案 bytes[i+0] << 56 取决于底层的 CPU 字节序。位移操作很好用,因为实际的移位操作会忽略目标类型使用大端还是小端。但在这种情况下,您必须预先知道源数组中哪个字节应对应于最高有效位。如果该数组是基于小端格式构建的,则此代码将起作用,因为数组的最后一个字节被移位到了最高地址。
至于 uint64_t const v = ,在本地范围内使用 const 限定符有点奇怪。它没有危害,但容易引起混淆,在本地范围内并没有增加任何有价值的东西。我建议您将其删除。

1
声明一个对象为“const”可以防止由于拼写错误而意外修改它的错误。此外,它向读者表达了意图,并且在执行时间上没有成本。这甚至可以启用优化,其中编译器可以依赖于不会被调用程序修改的内容。 - Eric Postpischil
@EricPostpischil 在这种情况下,只在本地范围内。而且“编写更多文本以防止拼写错误”的论点是自相矛盾的。世界上没有奇怪的技巧可以拯救那些不知道自己在做什么的人。这与“尤达条件”if(1 == a)和其他类似的荒谬逻辑一样混乱。“我使用聪明的技巧来防止自己写出错误”。好吧,如果你记得在每个==语句中始终写入一个聪明的技巧,你也可以记得仔细检查自己没有意外写入= - Lundin
1
在本地范围内是可以的。 当一个对象被定义为const时,编译器可能会认为它不会被改变,因此编译器会在第二个例程中消除不必要的负载。关于“不知道自己在做什么的人”:打字错误发生在任何人身上,即使是有知识的人也会犯错。使用const并不是一种“聪明的技巧”,而是旨在达到的目的。 - Eric Postpischil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接