C语言offsetof宏底层如何实现?

99ANYc3cd6
预计阅读时长 20 分钟
位置: 首页 C语言 正文

offsetof 是什么?

offsetof 是一个在 C 标准库中定义的宏,它的全称是 "member offset"(成员偏移量),它的作用是获取一个结构体(struct)或联合体(union)中某个成员变量相对于结构体起始地址的偏移量(以字节为单位)

c语言offsetof实现
(图片来源网络,侵删)

这个偏移量是一个 size_t 类型的无符号整数值,它在内存布局、序列化、网络编程、底层系统编程等领域非常有用。

offsetof 的标准用法

offsetof 的使用非常简单,语法如下:

#include <stddef.h> // 必须包含这个头文件
offset = offsetof(struct_type, member_name);

参数说明:

  • struct_type: 结构体或联合体的类型名。
  • member_name: 结构体或联合体中成员变量的名字。

返回值:

c语言offsetof实现
(图片来源网络,侵删)
  • 返回 member_name 成员在 struct_type 中的起始地址与 struct_type 整体的起始地址之间的字节差。

示例代码

#include <stdio.h>
#include <stddef.h>
// 定义一个结构体
struct Person {
    char name[20];    // 20 bytes
    int age;          // 4 bytes
    double salary;    // 8 bytes
};
int main() {
    // 计算 name 成员的偏移量
    size_t name_offset = offsetof(struct Person, name);
    printf("Offset of 'name': %zu\n", name_offset); // 预期输出: 0
    // 计算 age 成员的偏移量
    size_t age_offset = offsetof(struct Person, age);
    printf("Offset of 'age': %zu\n", age_offset);   // 预期输出: 20
    // 计算 salary 成员的偏移量
    size_t salary_offset = offsetof(struct Person, salary);
    printf("Offset of 'salary': %zu\n", salary_offset); // 预期输出: 24
    // 为了验证,我们也可以手动计算一下
    // sizeof(name) = 20
    // sizeof(age) = 4
    // 20 + 4 = 24, 这与 salary_offset 的结果一致
    return 0;
}

输出结果(在大多数系统上):

Offset of 'name': 0
Offset of 'age': 20
Offset of 'salary': 24

注意:由于内存对齐的存在,sizeof(struct Person) 可能不等于 20 + 4 + 8 = 32,在许多系统上,sizeof(struct Person) 可能是 40,因为在 age (4字节) 和 salary (8字节) 之间可能会有 4 字节的填充,但 offsetof 只关心成员相对于结构体起始位置的偏移,salary 的偏移量仍然是 24。

offsetof 的实现原理(核心)

offsetof 是一个宏,而不是一个函数,这是它的实现能够如此“神奇”的关键,C 标准并没有规定它的具体实现方式,但通常有两种主流的实现方法,第二种是更通用、更强大的方法。

利用 typeof (GCC/Clang 扩展)

在 GCC 和 Clang 编译器中,可以使用 typeof 关键字(C11 标准已引入 _Generic,可以更优雅地实现类似功能)。

c语言offsetof实现
(图片来源网络,侵删)
// 这种方法依赖于编译器扩展,不是标准C,但很直观
#define offsetof(TYPE, MEMBER) \
    ((size_t) &((TYPE *)0)->MEMBER)

工作原理分解:

  1. (TYPE *)0: 将整数 0 强制转换为指向 TYPE 类型的指针,我们得到一个虚拟的、地址为 0 的结构体指针。
  2. ((TYPE *)0)->MEMBER: 通过这个虚拟指针访问它的成员 MEMBER,根据 C 语言的指针运算规则,pointer->member 的地址实际上是 pointer 的地址加上 member 的偏移量。
    • 因为 pointer 的地址是 0&((TYPE *)0)->MEMBER 的计算结果就是 0 + offset_of_MEMBER,也就是 offset_of_MEMBER
  3. (size_t) ...: 将计算出的地址(一个整数)转换为 size_t 类型,以确保返回值的类型正确。

优点:非常简洁,易于理解。 缺点:依赖于 typeof(或类似的编译器特性),不是 100% 的标准 C。


标准 C 宏实现(通用且强大)

这是在标准 C 中实现 offsetof 的最常见、最可靠的方法,它不依赖任何编译器扩展。

// 标准C中 offsetof 的典型实现
#define offsetof(TYPE, MEMBER) \
    ((size_t) &((TYPE *)0)->MEMBER)

Wait, this looks the same as the GCC version! And it is. The magic is that even without typeof, this expression is well-defined by the C standard.

为什么它在标准 C 中是合法的?

关键在于 C 标准对指针算术的特殊规定,即使我们解引用了一个空指针 (TYPE *)0,我们只取其地址 &...,而不实际访问该地址上的值。

C 标准允许对任何指针(包括空指针)进行取地址操作 &,只要我们不试图解引用(dereference)该指针来读取或写入数据。& 操作符本身不触发内存访问。

&((TYPE *)0)->MEMBER 这段代码的执行过程是:

  1. 编译器看到 (TYPE *)0,知道这是一个指向 TYPE 的、地址为 0 的指针。
  2. 编译器知道 MEMBERTYPE 中的偏移量是 X 字节。
  3. 编译器计算 &((TYPE *)0)->MEMBER 的结果,它直接得出一个地址值 X,这个过程完全在编译时完成,是纯粹的地址计算,不涉及任何运行时的内存访问。
  4. (size_t) 将这个地址值 X 转换为无符号整数类型。

这个宏在所有符合标准的 C 编译器中都能正确工作,并且是安全的。

offsetof 的实际应用

offsetof 在很多底层编程场景中都非常有用。

应用 1:手动访问结构体成员

当你只有一个指向结构体的 void* 指针,但想通过成员名字来访问成员时,offsetof 就派上用场了。

#include <stdio.h>
#include <stddef.h>
#include <string.h>
struct Point {
    int x;
    int y;
};
void print_point(void* p) {
    // 使用 offsetof 来计算成员的地址
    int* x_ptr = (int*)((char*)p + offsetof(struct Point, x));
    int* y_ptr = (int*)((char*)p + offsetof(struct Point, y));
    printf("Point is: (%d, %d)\n", *x_ptr, *y_ptr);
}
int main() {
    struct Point my_point = {10, 20};
    print_point(&my_point); // 传递结构体的地址
    return 0;
}

应用 2:处理数据序列化和反序列化

在网络传输或文件存储中,经常需要将结构体数据转换成字节流(序列化),或者从字节流还原成结构体(反序列化)。offsetof 可以帮助你精确定位每个成员在字节流中的位置。

#include <stdio.h>
#include <stddef.h>
#include <string.h>
struct DataPacket {
    char header[4];
    int id;
    float value;
};
// 序列化函数
void serialize(const struct DataPacket* packet, char* buffer) {
    // 使用 memcpy 和 offsetof 来安全地复制数据
    memcpy(buffer + offsetof(struct DataPacket, header), packet->header, sizeof(packet->header));
    memcpy(buffer + offsetof(struct DataPacket, id), &packet->id, sizeof(packet->id));
    memcpy(buffer + offsetof(struct DataPacket, value), &packet->value, sizeof(packet->value));
}
// 反序列化函数
void deserialize(const char* buffer, struct DataPacket* packet) {
    memcpy(packet->header, buffer + offsetof(struct DataPacket, header), sizeof(packet->header));
    memcpy(&packet->id, buffer + offsetof(struct DataPacket, id), sizeof(packet->id));
    memcpy(&packet->value, buffer + offsetof(struct DataPacket, value), sizeof(packet->value));
}
int main() {
    struct DataPacket original = {'T', 'X', 'T', 1, 3.14f};
    char serialized_buffer[sizeof(struct DataPacket)];
    serialize(&original, serialized_buffer);
    printf("Serialized.\n");
    struct DataPacket received;
    deserialize(serialized_buffer, &received);
    printf("Deserialized: id=%d, value=%f\n", received.id, received.value);
    return 0;
}
  • offsetof 是一个宏,用于计算结构体/联合体成员的偏移量。
  • 核心实现((size_t) &((TYPE *)0)->MEMBER),它通过在地址 0 处进行虚拟的指针运算来得到偏移量。
  • 安全性:该宏是安全的,因为它只进行地址计算,不实际访问内存,符合 C 标准。
  • 用途:在需要动态访问结构体成员、处理内存布局、数据序列化等底层编程任务中不可或缺。
  • 使用前提:必须包含 <stddef.h> 头文件。
-- 展开阅读全文 --
头像
织梦如何调用三级栏目下的文章?
« 上一篇 02-25
织梦图片集如何调用新字段?
下一篇 » 02-25

相关文章

取消
微信二维码
支付宝二维码

目录[+]