【问题标题】:Why does this implementation of offsetof() work?为什么这个 offsetof() 的实现有效?
【发布时间】:2010-10-17 08:53:39
【问题描述】:

在 ANSI C 中,offsetof 定义如下。

#define offsetof(st, m) \
    ((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))

既然我们取消引用 NULL 指针,为什么这不会引发分段错误?或者这是某种编译器破解,它看到只有偏移量的地址被取出,所以它静态计算地址而不实际取消引用它?这段代码也可以移植吗?

【问题讨论】:

  • 这是我在 SO 上看到的第一个抱怨代码有效的问题吗? :-)
  • 有一个 if(0){asm(nop)} 的人把它放在外面会导致失败......
  • ANSI C(实际上是 ISO C)没有为 offsetof 指定这个定义。它只是指定它必须如何表现。实际定义取决于每个实现,并且可以将一个实现更改为另一个。
  • 需要注意的是,符合 MISRA-C:2004 的代码要求不使用 offsetof,因为它很容易导致未定义的行为。

标签: c pointers offsetof


【解决方案1】:

上述代码中没有任何地方被取消引用。当*-> 用于地址值以查找引用值时,会发生取消引用。上面* 的唯一用途是在类型声明中用于强制转换。

上面使用了-> 运算符,但它不用于访问值。相反,它用于获取值的地址。这是一个非宏代码示例,应该让它更清楚一点

SomeType *pSomeType = GetTheValue();
int* pMember = &(pSomeType->SomeIntMember);

第二行实际上不会导致取消引用(取决于实现)。它只是在pSomeType 值内返回SomeIntMember 的地址。

您看到的是任意类型和 char 指针之间的大量转换。 char 的原因是它是 C89 标准中唯一(可能是唯一)具有明确大小的类型之一。大小为 1。通过确保大小为 1,上面的代码可以执行计算值的真实偏移量的邪恶魔法。

【讨论】:

  • 我没有可用的 C 标准,但我想我记得 C90 中的一些内容,即不一定能够使用(不仅是取消引用)任意地址。原因是 8086 和 IBM 370 之类的机器使用段寄存器,并且无法引用它们的整个地址空间。
  • 在 C 标准中,&(pSomeType->SomeIntMember) 中的 -> 确实会导致取消引用。当你声称它不是时,也许你可以澄清你的意思。
  • 这个答案是完全错误的:不仅总体上是错误的,而且我在几乎每一个句子中都看到至少一个错误。
【解决方案2】:

虽然这是offsetof 的典型实现,但标准并没有强制要求,它只是说:

以下类型和宏定义在标准头文件<stddef.h> [...]

offsetof(type,member-designator)

它扩展为一个类型为size_t 的整数常量表达式,其值为 这是结构成员(由 member-designator 指定)的字节偏移量, 从其结构的开头(由 type 指定)。类型和成员代号 应该是给定的

statictypet;

然后表达式&(t.member-designator) 计算为地址常量。 (如果指定的成员是位域,则行为未定义。)

阅读 PJ Plauger 的“标准 C 库”以讨论它以及 <stddef.h> 中的其他项目,这些都是可能(应该?)在适当的语言中的边界线特性,并且可能需要特殊的编译器支持。

它仅具有历史意义,但我在 386/IX 上使用了早期的 ANSI C 编译器(请参阅,我告诉过您具有历史意义,大约在 1990 年),它在 offsetof 的那个版本上崩溃了,但是当我将它修改为:

#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024))

这是某种编译器错误,尤其是因为标头与编译器一起分发并且不起作用。

【讨论】:

  • "...<stddef.h> 这些都是可以(应该?)使用适当语言的边界线功能“ - 我会说他们 是语言本身的一部分,因为即使是独立的实现也需要始终支持它们......
  • 为什么只有static type t 而不仅仅是type t
  • @eigenslacker — 主要是我复制了标准,这就是它所说的。它可能有一些深刻的意义,可能与不能与static 一起使用的 VLA(可变长度数组 - 和可变修改类型)有关。这可能与不完整的类型有关——同上。
【解决方案3】:

在 ANSI C 中,offsetof 不是这样定义的。没有这样定义的原因之一是某些环境确实会抛出空指针异常,或者以其他方式崩溃。因此,ANSI C 将offsetof( ) 的实现留给编译器构建者。

上面显示的代码对于不主动检查 NULL 指针但仅在从 NULL 指针读取字节时失败的编译器/环境来说是典型的。

【讨论】:

  • 为了清楚起见,offsetof() 宏在指针实际上是整数的绝大多数平台上已经非常普遍和广泛地实现,如问题所示,甚至更简单地没有减法.大多数 C 编译器不会主动检查 NULL 指针。使用的表达式 NOT 取消引用 anything --- 它只是通过使用地址(恰好为零)和内部已知偏移量的简单算术加法来计算偏移量的成员。优化后甚至没有执行任何运行时添加。
【解决方案4】:

要回答问题的最后一部分,代码不可移植。

只有当两个指针指向同一个数组中的对象或指向数组的最后一个对象之后的一个对象时,才定义和可移植两个指针相减的结果(7.6.2 加法运算符,H&S 第五版)

【讨论】:

    【解决方案5】:

    它没有段错误,因为您没有取消引用它。指针地址被用作从另一个数字中减去的数字,而不是用于寻址内存操作。

    【讨论】:

      【解决方案6】:

      它计算成员m相对于st类型对象表示的起始地址的偏移量。

      ((st *)(0)) 指的是 NULL 类型的 st * 指针。 &((st *)(0))->m 指的是这个对象中成员 m 的地址。由于这个对象的起始地址是0 (NULL),所以成员m的地址就是偏移量。

      char * 转换并以字节为单位计算偏移量。根据指针运算,当你对两个T *类型的指针进行区分时,结果是操作数所包含的两个地址之间所代表的T类型的对象的数量。

      【讨论】:

      • 肖恩,为什么需要减法?我们不能只返回 (char *)&((st *)(0))->m 吗?
      • 在一些 C 实现中,空指针在内部不由值 0 表示。在这样的实现中,我想要么这个 C 代码将完全失败,因为编译器不知道如何在指针算术中处理空指针,要么它可能由于减法而工作(因为空指针的表示需要被取消)。
      【解决方案7】:

      清单 1:一组有代表性的 offsetof() 宏定义

      // Keil 8051 compiler
      #define offsetof(s,m) (size_t)&(((s *)0)->m)
      
      // Microsoft x86 compiler (version 7)
      #define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)
      
      // Diab Coldfire compiler
      #define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))
      
      typedef struct 
      {
          int     i;
          float   f;
          char    c;
      } SFOO;
      
      int main(void)
      {
        printf("Offset of 'f' is %zu\n", offsetof(SFOO, f));
      }
      

      宏中的各种运算符按顺序进行评估,以便执行以下步骤:

      1. ((s *)0) 接受整数零并将其转换为指向 s 的指针。
      2. ((s *)0)->m 取消引用指向结构成员 m 的指针。
      3. &(((s *)0)->m) 计算 m 的地址。
      4. (size_t)&(((s *)0)->m) 将结果转换为适当的数据类型。

      根据定义,结构本身位于地址 0。因此,指向的字段的地址(上面的步骤 3)必须是从结构开始的偏移量,以字节为单位。

      【讨论】:

        【解决方案8】:

        引用 offsetof 宏的 C 标准:

        C 标准,第 6.6 节,第 9 段

        地址常量是一个空指针,一个指向指定静态存储持续时间对象的左值的指针,或者一个指向函数指示符的指针;它应使用一元& 运算符或转换为指针类型的整数常量显式创建,或通过使用数组或函数类型的表达式隐式创建。数组下标[] 和成员访问.-> 运算符、地址& 和间接* 一元运算符以及指针转换可以用于创建地址常量,但值不能通过使用这些运算符来访问对象。

        宏定义为

        #define offsetof(type, member)  ((size_t)&((type *)0)->member)
        

        并且表达式包含地址常量的创建。

        虽然说真的,结果不是地址常量,因为它不指向静态存储持续时间的对象。但这仍然是一致的,对象的值不应被访问,因此转换为指针类型的整数常量不会被取消引用。

        另外,考虑一下 C 标准中的这句话:

        C 标准,第 7.19 节,第 3 段

        类型和成员指示符应该是给定的

        static type t;

        然后表达式&(t.member-designator) 计算为地址常量。 (如果 指定成员是位域,行为未定义。)

        C 中的 struct 是一种复合数据类型(或记录)声明,它在内存块中定义一个名称下的变量的物理分组列表,允许通过单个指针或声明的 struct 访问不同的变量返回相同地址的名称。

        从编译器的角度来看,结构声明的名称是一个地址,而成员指示符是该地址的偏移量。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-03-26
          • 1970-01-01
          • 2019-07-13
          • 2013-12-17
          • 1970-01-01
          • 2011-06-19
          • 2020-10-19
          • 1970-01-01
          相关资源
          最近更新 更多