使用 offsetof 访问结构成员

Using offsetof to access struct member

我有以下代码:

#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 标准定义的吗?

我知道:

我想通过 int* 访问 ptr 指向的数据并没有违反严格的别名规则,但我不完全确定标准是否保证了这一点。

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

你可以看到例如 6.3.2.3 p7(所有引用都是 C17 草案 N2176):

When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

所以(char *)&x是指向x的指针转换为指向char的指针,因此它指向x的最低寻址字节。当我们添加 offsetof(struct X, b)(假设它是 4)时,我们有一个指向 x 的字节 4 的指针。现在 offsetof(struct X, b) 定义为 return

the offset in bytes, to the structure member, from the beginning of its structure [7.19p3]

所以4其实就是x开头到x.b的偏移量。因此 x 的字节 4 是 x.b 的最低字节,这就是 ptr 指向的内容;换句话说,ptr 是指向 x.b 的指针,但类型为 char *。当我们将它转​​换回 int * 时,我们有一个指向 x.b 的指针,它的类型是 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前面的句子:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.

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

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

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


严格别名(6.5p7)没有问题。首先,使用6.5p6:

判断x.b的有效类型

The effective type of an object for an access to its stored value is the declared type of the object, if any. [Then explanations on what to do if it doesn't have a declared type.]

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

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

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

— a type compatible with the effective type of the object,

[more options not relevant here]

我们正在通过类型为 int 的左值表达式 *(int *)ptr 访问 x.b。并且 int 与每个 6.2.7p1 的 int 兼容:

Two types have compatible type if their types are the same. [Then other conditions under which they may also be compatible].


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

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 同样是一个预期的特征。

我相信这是完全合法的;事实上,我刚刚遇到了我正在阅读的一本书中使用的类似技术(这并不重要)。

以下是我认为这是合法的原因:

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

首先,x 被取消引用为指向结构的指针,但如果我们将其原始类型用于指针运算,每次我们将 &x 增加 1,该值实际上增加的量等于sizeof(struct X)。由于 offsetof returns 是一个距离结构开头的字节距离的值,我们需要将 &x 转换为指向字节大小类型的兼容指针,在这种情况下 char *。由于 a char 总是定义为 1 个字节,所以当我们将 a char * 增加 1 时,我们将前进 1 个字节。这就是为什么它在 Section 6.5 Expressions:

中被特别调用的原因

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.

现在的结果是一个指向 char * 类型的 x.b 开始的指针,它是完美对齐的,因此这里没有调用未定义的行为。为什么?因为 offsetof returns 从一开始就以字节为单位的距离,并且我们一直在通过 char * 转换对指针进行逐字节运算,结果应该恰好指向b.

因为我们已经到达了我们想要的对象的开始,所以我们不再需要结果是 char * 类型。结果现在将被转换为通用指针 void * ptr,稍后在取消引用之前转换为 int *,以便我们访问 x.b.

因为 b 是一个 int,而我们最终有一个 *(int*) 计算结果为 int 类型,我们遵循 int 下的标准=32=] 上面的条款(或其他条款之一;如果我错了请纠正我)。