为什么offsetof()的实现有效?

时间:2009-04-03 13:43:06

标签: c pointers offsetof

在ANSI C中,offsetof定义如下。

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

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

8 个答案:

答案 0 :(得分:35)

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

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

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

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

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

答案 1 :(得分:8)

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

上面显示的代码对于没有主动检查NULL指针的编译器/环境是典型的,但只有在从NULL指针读取字节时才会失败。

答案 2 :(得分:8)

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

  

标准标题<stddef.h> [...]

中定义了以下类型和宏      

offsetof( type , member-designator )

     

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

     

static type t;

     

然后表达式&(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))

这是一种编译错误,不仅仅是因为标头随编译器一起分发而且无效。

答案 3 :(得分:7)

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

只有当两个指针指向同一个数组中的对象或指向一个超过数组最后一个对象的对象时,才能定义和删除两个指针的结果(7.6.2 Additive Operators,H&amp; S Fifth Edition)< / p>

答案 4 :(得分:2)

它不会出现段错误,因为您没有取消引用它。指针地址用作从另一个数字中减去的数字,不用于解决内存操作。

答案 5 :(得分:2)

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

((st *)(0))指的是类型为NULL的{​​{1}}指针。 st *指的是此对象中成员m的地址。由于此对象的起始地址为&((st *)(0))->m,因此成员m的地址正好是偏移量。

0 (NULL)转换,差值计算以字节为单位的偏移量。根据指针操作,当您在类型char *的两个指针之间产生差异时,结果是操作数包含的两个地址之间表示的类型T *的对象数。

答案 6 :(得分:1)

清单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. 的地址
  5. (size_t)&(((s *)0)->m)将结果转换为适当的数据类型。
  6. 根据定义,结构本身位于地址0.接下来,指向的字段的地址(上面的步骤3)必须是从结构的开头起的偏移量(以字节为单位)。

答案 7 :(得分:0)

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中的结构是复合数据类型(或记录)声明,它定义一个内存块中一个名称下的物理分组的变量列表,从而允许通过单个指针或声明的结构访问不同的变量返回相同地址的名称。

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