11

我有以下代码:

#include <stddef.h>

int main() {
  struct X {
    int a;
    int b;
  } x = {0, 0};

  void *ptr = (char*)&x + offsetof(struct X, b);

  *(int*)ptr = 42;

  return 0;
}

最后一行执行对x.b.

此代码是否根据任何 C 标准定义?

我知道:

  • *(char*)ptr = 42;尽管仅定义了实现,但已定义。
  • ptr == (void*)&x.b

我猜想访问ptrvia指向的数据int*不会违反严格的别名规则,但我不完全确定标准是否能保证这一点。

4

2 回答 2

11

是的,这是非常明确的定义,并且正是offsetof预期的使用方式。您对指向字符类型的指针执行指针运算,以便以字节为单位完成,然后转换回成员的实际类型。

例如,您可以看到 6.3.2.3 p7(所有引用均指向 C17 草案 N2176):

当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续增量,直到对象的大小,产生指向对象剩余字节的指针。

(char *)&x一个指针x转换为指向 的指针也是如此char,因此它指向 的最低寻址字节x。当我们添加offsetof(struct X, b)(比如说它是 4)时,我们有一个指向字节 4 的指针x。现在offsetof(struct X, b)定义为返回

从结构的开头到结构成员的字节偏移量 [7.19p3]

所以 4 实际上是从 to 开始的偏移xx.b。因此字节 4 ofx是 的最低字节x.b,这就是ptr指向;换句话说,ptr是指向 的指针x.b,但类型为char *。当我们将它转​​换回时int *,我们有一个指向x.bwhich 的类型的指针int *,与我们从表达式中得到的完全相同&x.b。所以取消引用这个指针访问x.b.


关于最后一步的评论中出现了一个问题:什么时候ptr被强制转换回int *,我们怎么知道我们确实有一个指向 的指针int x.b?这在标准中不太明确,但我认为这是明显的意图。

但是,我认为我们也可以间接推导出它。希望我们同意ptr上面是一个指向x.b. 现在通过上面引用的 6.3.2.3 p7 的同一段落,获取一个指针x.b并将其转换为char *,如在 中(char *)&x.b,也将产生一个指向 的最低寻址字节的指针x.b。因为它们是指向相同字节的相同类型的指针,所以它们是相同的指针:ptr == (char *)&x.b.

然后我们看6.3.2.3 p7前面几句:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐引用的类型,则行为未定义。否则,当再次转换回来时,结果将等于原始指针。

这里对齐没有问题,因为char对齐要求最弱(6.2.8 p6)。所以转换(char *)&x.bint *必须恢复指向 的指针x.b,即(int *)(char *)&x.b == &x.b

Butptr和 是同一个指针(char *)&x.b,所以我们可以用这个等式替换它们:(int *)ptr == &x.b.

显然*&x.b会产生一个左值指定x.b(6.5.3.2 p4),因此*(int *)ptr.


严格别名(6.5p7)没有问题。首先,确定x.b使用6.5p6的有效类型:

访问其存储值的对象的有效类型是对象的声明类型(如果有)。[然后解释如果它没有声明的类型该怎么办。]

好吧,x.b确实有一个声明的类型,即int. 所以它的有效类型是int

现在看看在严格别名下访问是否合法,参见 6.5p7:

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

— 与对象的有效类型兼容的类型,

[更多选项与此处无关]

我们通过具有类型x.b的左值表达式进行访问。并且与每 6.2.7p1 兼容:*(int *)ptrintintint

如果它们的类型相同,则两种类型具有兼容的类型。[然后它们也可能兼容的其他条件]。


可能更熟悉的相同技术的一个示例是按字节索引到数组中。如果我们有

int arr[100];
*(int *)((char *)arr + (17 * sizeof(int))) = 42;

那么这相当于arr[17] = 42;.

这就是通用例程喜欢qsortbsearch实现的方式。如果我们尝试使用qsort的数组int,那么在qsort所有指针运算中都以字节为单位,在指向字符类型的指针上完成,偏移量由作为参数传递的对象大小手动缩放(此处为sizeof(int))。当qsort需要比较两个对象时,它将它们转换为const void *并将它们作为参数传递给比较器函数,比较器函数将它们转换回以const int *进行比较。

这一切都很好,显然是该语言的预期功能。所以我认为我们不必怀疑offsetof在当前问题中使用的同样是一个预期的功能。

于 2021-11-12T00:31:56.050 回答
2

我相信这是完全合法的;事实上,我刚刚在我正在阅读的一本书中遇到了类似的技术(没关系)。

这就是为什么我认为这是合法的:

void *ptr = (char*)&x + offsetof(struct X, b);

首先,x被解引用为指向结构的指针,但是如果我们使用它的原始类型进行指针运算,每次我们增加&x1 时,值实际上增加了一个等于sizeof(struct X). 由于offsetof返回的值是与结构开头的距离(以字节为单位),因此我们需要将其转换&x为指向字节大小类型的兼容指针,在本例中为char *. 由于 achar总是被定义为 1 个字节,所以当我们将 a 增加char *1 时,我们将提前 1 个字节。这就是为什么在第 6.5 节表达式中特别提到它的原因:

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

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

其结果现在是指向x.b类型开头的指针char *,并且它完全对齐,因此这里没有调用未定义的行为。为什么?因为offsetof返回从开头开始的字节距离,并且我们一直在通过强制转换对指针进行字节运算char *,所以结果应该正好指向b.

由于我们已经到达了我们想要的对象的开头,我们不再需要结果在类型中char *了。结果void * ptr现在将被转换为通用指针,int *稍后在取消引用它以使我们能够访问x.b.

由于b是 a int,并且我们最终有 a*(int*)评估为int类型,因此我们遵循上述"a type compatible with the effective type of the object"条款中的标准(或其他标准之一;如果我错了,请纠正我)。

于 2021-11-13T03:25:11.477 回答