是的,这是非常明确的定义,并且正是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 开始的偏移x量x.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.b回int *必须恢复指向 的指针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;.
这就是通用例程喜欢qsort和bsearch实现的方式。如果我们尝试使用qsort的数组int,那么在qsort所有指针运算中都以字节为单位,在指向字符类型的指针上完成,偏移量由作为参数传递的对象大小手动缩放(此处为sizeof(int))。当qsort需要比较两个对象时,它将它们转换为const void *并将它们作为参数传递给比较器函数,比较器函数将它们转换回以const int *进行比较。
这一切都很好,显然是该语言的预期功能。所以我认为我们不必怀疑offsetof在当前问题中使用的同样是一个预期的功能。