使用 uint16_t* 访问 uint32_t 的数组是否会导致未定义的行为?

Does accessing an array of uint32_t with an uint16_t* lead to undefined behavior?

我有以下表面上简单的 C 程序:

#include <stdint.h>
#include <stdio.h>

uint16_t
foo(uint16_t *arr)
{
  unsigned int i;
  uint16_t sum = 0;

  for (i = 0; i < 4; i++) {
    sum += *arr;
    arr++;
  }

  return sum;
}

int main()
{
  uint32_t arr[] = {5, 6, 7, 8};
  printf("sum: %x\n", foo((uint16_t*)arr));

  return 0;
}

我们的想法是迭代一个数组并将其累加为忽略溢出的 16 位字。当使用 gcc 在 x86-64 上编译此代码且未进行优化时,我得到的结果似乎是 0xb (11) 的正确结果,因为它对前 4 个 16 位字(包括 5 和 6)求和:

$ gcc -O0 -o castit castit.c
$ ./castit
sum: b
$ ./castit
sum: b
$

优化后就是另一回事了:

$ gcc -O2 -o castit castit.c
$ ./castit
sum: 5577
$ ./castit
sum: c576
$ ./castit
sum: 1de6

程序生成总和的不确定值。

我假设它现在不是编译器错误,这会让我相信程序中存在一些未定义的行为,但是我不能指出会导致它。

请注意,当函数 foo 被编译为单独链接的模块时,问题就不会出现。

你打破了strict aliasing rule,这确实是UB。那是因为您通过不同类型的指针为 uint32_t 的数组 arr 添加了别名,即 uint16_t 将其传递给 foo(uint16_t*).

唯一可以用作其他类型别名的指针类型是 char*

关于该主题的一些额外阅读 material:http://dbp-consulting.com/tutorials/StrictAliasing.html

K&R定义的C语言并不是为了方便代码生成而设计的,但不一定是高效代码的生成。 ANSI 等。添加了一些规则,旨在通过假设程序员不会做某些事情来允许编译器生成更高效的代码,但这样做的方式使得无法干净有效地实现某些类型的算法。 foo 的 standards-conforming 版本可以接受指向 uint16_tuint32_t 的指针,看起来像:

uint16_t foo(uint16_t *arr)  // Could perhaps use void*
{
  unsigned int i;
  uint16_t *src = arr;
  uint16_t temp;
  uint16_t sum = 0;

  for (i = 0; i < 4; i++) {
    memcpy(&temp, src, sizeof temp);
    sum += temp;
    src++;
  }

  return sum;
}

一些编译器会认识到 memcpy 可以替换为直接将 *src 读取为 uint16_t 的代码,因此尽管上面的代码可能 运行 有效使用 memcpy。然而,在其他一些编译器上,上面的代码会 运行 相当慢。此外,虽然 memcpy 使得在 C89 中可以做任何在没有 pointer-usage 规则的情况下可能发生的事情,但 C99 添加了额外的规则,这使得某些类型的语义基​​本上无法实现。幸运的是,在您的特定情况下,memcpy 将允许以一种允许某些编译器生成高效代码的方式来表达算法。

请注意,许多编译器都包含一个选项,可以通过消除上述对指针使用的限制来编译扩展标准 C 的语言。在 gcc 上,选项 -fno-strict-alias 可用于该目的。使用该选项可能会严重降低某些类型代码的效率,除非程序员采取措施防止这种降低(尽可能使用 restrict 限定符,并将可能别名的内容复制到局部变量),但在许多情况下代码可以这样写 -fno-strict-alias 不会太严重地损害性能。