当前位置: 首页 > 知识库问答 >
问题:

使用offsetof访问结构成员

从光启
2023-03-14

我有以下代码:

#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;
}

最后一行执行对< code>x.b的间接访问。

这段代码是根据任何C标准定义的吗?

我知道:

  • *(字符*)ptr = 42; 已定义,但仅定义了实现。
  • ptr == (空*)

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


共有2个答案

罗飞宇
2023-03-14

我认为这是完全合法的。事实上,我刚刚在我正在读的一本书中遇到了类似的技巧(这並不重要)。

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

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

首先,< code>x被解引用为指向struct的指针,但是如果我们使用它的原始类型进行指针运算,那么每次我们增加< code >

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

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

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

因为我们已经到达了所需对象的开头,所以不需要结果的类型为char*。现在,结果将被强制转换为一个通用指针void*ptr,稍后将被强制转化为int*>,然后取消对它的引用,以便我们访问

由于 b 是一个 int,我们最终有一个计算结果为 int 类型的 *(int*),因此我们遵循上面的“与对象的有效类型兼容的类型”子句(或其他子句之一,如果我错了,请纠正我)下的标准。

周翰池
2023-03-14
匿名用户

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

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

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

所以 (字符 *)

从结构开始到结构成员的偏移量(以字节为单位)[7.19p3]

所以 4 实际上是从 x 的开头到 x.b 的偏移量。因此,x 的字节 4 是 x.b 的最低字节,这就是 ptr 所指的;换句话说,ptr 是指向 x.b 的指针,但类型为 char *。当我们将其转换回 int * 时,我们有一个指向 x.b 的指针,其类型为 int *,与我们从表达式中得到的完全相同

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

但是,我认为我们也可以间接地推导出来。希望我们同意上面的 ptr 是指向 x.b 的最低寻址字节的指针。现在,通过上面引用的6.3.2.3 p7的同一段话,将指针指向x.b并将其转换为char *,如(char *)

然后我们看一下6.3.2.3第7页的前几句话:

指向对象类型的指针可能会转换为指向其他对象类型的指示器。如果生成的指针没有针对引用的类型正确对齐,则行为是未定义的。否则,当再次转换回来时,结果应与原始指针相等。

这里没有对齐问题,因为< code>char的对齐要求最弱(6.2.8 p6)。所以转换< code>(char *)

但是< code>ptr和< code>(char *)是同一个指针

显然<代码> *

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

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

嗯,x。b有一个声明的类型,即int。因此它的有效类型是int

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

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

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

[此处没有更多相关选项]

我们通过左值表达式 *(整数 *)ptr 访问 x.b,其类型为 int并且 int 与 6.2.7p1 的 int 兼容:

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

这种可能更熟悉的相同技术的一个例子是按字节html" target="_blank">索引到数组中。如果我们有

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

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

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

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

 类似资料:
  • 访问结构成员或类成员时,使用成员访问运算符(member access operator),包括圆点运算符(.)和箭头运算符(—>)。圆点运算符通过对象的变量名或对象的引用访问结构和类成员。例如,要打印 timeObject 结构的 hour 成员,用下列语句: cout << timeobject.hour; 要打印timeRef引用的结构的hour成员,用下列语句: cout << timeR

  • 反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如下表所示。 反射值对象的成员访问方法 方  法 备  注 Field(i int) Value 根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机 NumField() int 返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机 FieldByNa

  • 在这两个示例中,通过偏移其他成员的指针来访问结构成员是否会导致未定义/未指定/实现定义的行为? C11§6.7.2.1第14段似乎表明,这应该是实施定义: 结构或联合对象的每个非位字段成员都以适合其类型的实现定义方式对齐。 后来又说: 结构对象中可能有未命名的填充,但在其开头没有。 但是,如下所示的代码似乎相当常见: 该标准似乎保证 与 和< code >( 原始应用程序正在考虑从一个结构字段到另

  • 我试图理解在将值存储到结构或联合的成员中时,类型双关是如何工作的。 标准N1570指定 当值存储在结构或联合类型的对象(包括成员对象)中时,与任何填充字节相对应的对象表示的字节采用未指定的值。 所以我把它解释为如果我们有一个对象要存储到一个成员中,这样对象的大小等于,与填充相关的字节将具有未指定的值(即使我们定义了原始对象中的字节)。这是一个例子: 在我的机器。 打印的行为是否为这样的程序定义得很

  • 问题内容: 我想在结构上定义一个方法来验证http请求。但是我在访问结构域时遇到一些问题。 有我的代码。 运行此代码时,得到以下结果 有什么方法可以访问Validate2()方法上的Validate()方法上的结构字段? 问题答案: 您不能从内部结构访问外部结构字段。仅内部字段来自外部。您可以做的是:

  • 我目前使用的日志记录系统使用一个标记值来标识它将存储的参数。我们正在使用的格式如下:标签+时间+值。 不要忘记结构的每个字段可能具有不同的类型。