这段代码是否违反了严格别名规则?

16

问题:

  1. 下面的代码是否违反了严格别名规则?也就是说,一台聪明的编译器是否允许打印 00000(或其他一些不好的效果),因为缓冲区首先作为其他类型进行访问,然后通过 int* 访问?

  2. 如果不是,那将只移动 ptr2 的定义和初始化到大括号之前(这样当 ptr1 进入范围时,ptr2 就已经被定义了)会破坏它吗?

  3. 如果不是,那将去掉大括号(这样 ptr1ptr2 将在同一范围内),这会破坏它吗?

  4. 如果是,该如何修复代码?

奖励问题:如果代码没有问题,2 或 3 不会破坏它,如何更改以使其违反严格别名规则(例如,将大括号循环转换为使用 int16_t)?


int i;
void *buf = calloc(5, sizeof(int)); // buf initialized to 0

{
    char *ptr1 = buf;    
    for(i = 0; i < 5*sizeof(int); ++i)
        ptr1[i] = i;
}

int *ptr2 = buf;
for(i = 0; i < 5; ++i)
    printf("%d", ptr2[i]);
寻求确认,需要一份简短的专家答案来解释这段特定代码,最好不要使用大量标准引用。我只需要与此代码相关的部分,不需要对严格别名规则进行长时间的解释。如果答案能明确列出上面编号的问题,那就太好了。另外,假设使用通用CPU,没有整数陷阱值,还假设int是32位的二进制补码。
2个回答

13
不是这样的,这只是因为内存被分配并使用字符类型写入。使用 malloc 分配内存时,该对象没有声明类型,因为它是用 malloc 分配的。 因此,该对象没有任何有效类型。然后代码使用类型 char 访问并修改对象。由于类型是 char,且未复制任何具有有效类型的对象,因此复制不会为此次及随后的访问设置 effective type 为 char,但仅在访问期间将其设置为 char。访问后,该对象不再具有有效类型。然后使用 int 类型来访问和仅读取该对象。由于该对象没有有效类型,它变成了 int,以供读取持续时间使用。访问后,该对象不再具有有效类型。由于 int 很明显与有效类型 int 兼容,因此行为是定义的。(假设读取的值不是 int 的陷阱表示形式。)
如果您使用一个既不是字符类型也不与 int 兼容的非字符类型来访问和修改对象,则行为将是未定义的。假设您的示例是 (假设 sizeof(float)==sizeof(int)):
int i;
void *buf = calloc(5, sizeof(float)); // buf initialized to 0

{
    float *ptr1 = buf;    
    for(i = 0; i < 5*sizeof(float); ++i)
        ptr1[i] = (float)i;
}

int *ptr2 = buf;
for(i = 0; i < 5; ++i)
    printf("%d", ptr2[i]);
通过一个非字符类型的左值存储一个值到没有声明类型的对象中时,该左值的类型成为该访问及后续访问(不修改存储值)期间该对象的有效类型,此时如果写入的值是浮点数,则其有效类型为float。当这些对象被int访问时,由于仅读取而未修改,有效类型仍为float。上次使用float进行的写操作永久将有效类型设置为float,直到下一次写入该对象(在本例中没有发生)。由于类型intfloat不兼容,因此行为未定义。

1
那时只有一个字符被写入对象,这并没有将对象的有效类型设置为它。当读取时,它变成了 int,因为根据“对于没有声明类型的对象的所有其他访问,对象的有效类型仅是用于访问的 lvalue 的类型。” - 2501
1
如果在同一点写入 int 到对象中,而不是读取,类型也会变成 int,因为:如果通过具有非字符类型的 lvalue 存储值到没有声明类型的对象中,则该 lvalue 的类型成为该访问和后续访问的对象的有效类型,这些后续访问不修改存储的值。 - 2501
@2501 我认为第6段中的“作为字符类型数组复制”部分适用于问题中的for循环。第7段中的“一个字符类型”从句是必要的,以避免违反严格别名规则。 - Andrew Henle
1
@hyde 是的,绝对是这样的,自动对象具有声明类型,且其有效类型不能更改。根据引用4,intchar 不兼容。 - 2501
2
@AndrewHenle 我认为“作为字符类型数组复制”是指从另一个对象中整体复制一个值,但这里并非如此(int值是通过字符指针存储的,但表示形式没有被复制)。不幸的是,在标准中没有进一步明确定义(这是众多缺陷之一)。 - davmac
显示剩余5条评论

2
不,这并没有违反严格别名规则。
根据C标准的6.2.5节“类型”,第28段:
“指向void的指针应具有与指向字符类型的指针相同的表示和对齐要求。”(译注:原文是英文)
请注意脚注48。它指的是脚注48:
“48)表示相同的表示和对齐要求意味着可作为函数参数进行交换,作为函数返回值进行交换,以及作为联合成员进行交换。”
因此,您可以使用char *指针(假设您的ptr是指ptr1)轻松访问calloc()分配的内存,而不会出现问题。
虽然那真的是多余的,因为7.22.3节“内存管理函数”,第1段指出:
如果分配成功,返回的指针将被适当地对齐,以便可以将其分配给任何具有基本对齐要求的对象类型的指针,然后用于访问在分配的空间中分配的这种对象或这种对象的数组。因此,您可以安全地通过int指针和char指针访问calloc()分配的内存,甚至是double指针(假设您保持在分配的内存范围内)。

6
“Strict aliasing”和对齐要求以及表示形式不同。 - Johannes Schaub - litb
1
@JohannesSchaub-litb 这个问题所涉及的是和 memset() 做的一样的事情(*memset 函数将 c(转换为 unsigned char)的值复制到指向 s 的对象的前 n 个字符中。*)如果您认为这是错误的,请发布您的答案,该答案还考虑了如何使用 memset() 将任何类型的内存设置为重复的 char 值。您是否在说 memset() 违反了严格别名规则? - Andrew Henle
3
@AndrewHenle 没有人说问题中的代码违反了严格别名规则。然而,你回答中给出的原因是不正确的。拥有相同的对齐方式和表示并不意味着两种类型可以别名。 - davmac
1
如果不是基本原则,那么强制别名规则至少是允许优化的一个重要原因。没有强制别名规则,编译器必须假设几乎任何对任何变量的赋值都可能改变指针指向的非易失性值(并且必须从内存中重新获取它),反之,几乎任何通过指针的赋值都可能改变任何变量的值。强制别名规则允许编译器假设在适用的情况下值不会像这样改变。 - hyde
(即“严格别名规则”主要体现在第6.5段第7款中。) - davmac
显示剩余6条评论

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