繁体   English   中英

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

[英]Why does this implementation of offsetof() work?

在 ANSI C 中,offsetof 定义如下。

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

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

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

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

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

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

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

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

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

offsetof( type , member-designator )

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

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))

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

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

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

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

只有当两个指针指向同一数组中的对象或指向数组最后一个对象之后的一个对象时,两个指针相减的结果才被定义和移植(7.6.2 Additive Operators, H&S Fifth Edition)

清单 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 步)必须是从结构开始的偏移量(以字节为单位)。

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM