C - 两个指针之间的转换行为

Ric*_*ant 9 c pointers strict-aliasing language-lawyer

2020 年 12 月 11 日更新:感谢@“一些程序员老兄”在评论中提出的建议。我的根本问题是我们的团队正在实现一个动态类型存储引擎。我们分配了多个16 对齐的char array[PAGE_SIZE] 缓冲区来存储动态类型的数据(没有固定的结构)。出于效率原因,我们不能执行字节编码或分配额外的空间来使用memcpy.

既然已经确定了对齐方式(即16),剩下的就是使用指针的强制转换来访问指定类型的对象,例如:

int main() {
    // simulate our 16-aligned malloc
    _Alignas(16) char buf[4096];

    // store some dynamic data:
    *((unsigned long *) buf) = 0xff07;
    *(((double *) buf) + 2) = 1.618;
}
Run Code Online (Sandbox Code Playgroud)

但是我们的团队对这个操作是否是未定义的行为存在争议。


我已经阅读了许多类似的问题,例如

但是这些和我对C标准的解释不同,我想知道是不是我的误解。

主要的混淆是关于C11的第6.3.2.3 #7节:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐 68) 引用类型,则行为未定义。

68) 通常,“正确对齐”的概念是可传递的:如果指向类型 A 的指针与指向类型 B 的指针正确对齐,而指向类型 C 的指针又正确对齐,则指向类型的指针对于指向类型 C 的指针,A 已正确对齐。

这里的结果指针是指Pointer Object还是Pointer Value

在我看来,我认为答案是Pointer Object,但更多的答案似乎表明Pointer Value


解释A:指针对象

我的想法如下: 指针本身就是一个对象。根据6.2.5 #28,不同的指针可能有不同的表示和对齐要求。因此,根据6.3.2.3 #7,只要两个指针具有相同的对齐方式,它们就可以安全地转换而不会出现未定义的行为,但不能保证它们可以被取消引用。在程序中表达这个想法:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    if (_Alignof(char *) == _Alignof(int *)) {
        // cast safely, because they have the same alignment requirement?
        int *pi = (int *) pc; 
        printf("pi: %p\n", pi);
    } else {
        printf("char * and int * don't have the same alignment.\n");
    }
}
Run Code Online (Sandbox Code Playgroud)

解释 B:指针值

但是,如果 C11 标准谈论的是引用类型的Pointer Value而不是Pointer Object。上面代码的对齐检查是没有意义的。在程序中表达这个想法:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    
    /*
     * undefined behavior, because:
     * align of char is 1
     * align of int is 4
     * 
     * and we don't know whether the `value` of pc is 4-aligned.
     */
    int *pi = (int *) pc;
    printf("pi: %p\n", pi);
}
Run Code Online (Sandbox Code Playgroud)

哪个解释是正确的?

dbu*_*ush 6

解释B是正确的。该标准讨论的是指向对象的指针,而不是对象本身。“结果指针”指的是转换的结果,而转换不会产生左值,因此它指的是转换后的指针值。

以代码在你的例子,假设一个int必须在4字节边界上对齐,也就是说,它的地址必须是4的倍数。如果地址buf0x1001然后转换那个地址int *是无效的,因为指针值没有正确对齐。如果地址buf0x1000然后将其转换成int *有效。

更新:

您添加的代码解决了对齐问题,因此在这方面没问题。然而,它有一个不同的问题:它违反了严格的别名。

您定义的数组包含类型为 的对象char。通过将地址转换为不同的类型并随后取消引用转换后的类型,您将一种类型的对象作为另一种类型的对象进行访问。这是 C 标准不允许的。

虽然标准中没有使用术语“严格别名”,但在第 6.5 节第 6 和第 7 段中描述了该概念:

6访问其存储值的对象的有效类型是该对象的声明类型(如果有)。87)如果一个值通过一个类型不是字符类型的左值存储到一个没有声明类型的对象中,那么左值的类型成为该访问和后续访问的有效类型修改存储的值。如果使用memcpy 或将值复制到没有声明类型的对象中memmove,或者被复制为字符类型的数组,那么该访问的修改对象的有效类型以及不修改该值的后续访问的有效类型是复制该值的对象的有效类型,如果它有一个. 对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

7对象的存储值只能由具有以下类型之一的左值表达式访问:88)

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 一种类型,它是与对象的有效类型相对应的有符号或无符号类型,
  • 一种类型,它是与对象有效类型的限定版本相对应的有符号或无符号类型,
  • 在其成员中包含上述类型之一的聚合或联合类型(递归地包括子聚合或包含联合的成员),或
  • 一种字符类型。

...

87 ) 分配的对象没有声明类型。

88 ) 此列表的目的是指定对象可以或不可以别名的情况。

在您的示例中,您正在对象之上编写 anunsigned long和 a 。这两种类型都不满足第 7 段的条件。doublechar

除此之外,这里的指针算法无效:

 *(((double *) buf) + 2) = 1.618;
Run Code Online (Sandbox Code Playgroud)

当您将其buf视为数组double时,它不是。至少,您需要直接执行必要的算术buf并在最后转换结果。

那么为什么这是char数组的问题而不是由 返回的缓冲区的问题malloc?因为从返回的内存malloc没有有效的类型,直到你存储在它的东西,这是第6段和脚注87描述。

因此,从标准的严格角度来看,您所做的是未定义的行为。但是根据您的编译器,您可以禁用严格别名,因此这将起作用。如果您使用的是 gcc,则需要传递-fno-strict-aliasing标志