C语言——container_of宏介绍
简介
container_of 是 C 语言中常用的一个宏,通常用于从结构体的成员指针反向获取包含该成员的结构体指针。这个宏的作用非常重要,尤其是在处理嵌入式结构体或者链表时,经常会遇到这种情况:你得到一个结构体成员的指针,但你需要得到包含该成员的整个结构体的指针。
宏定义
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
参数说明:
- ptr: 指向结构体成员的指针;
- type: 包含该成员的结构体类型;
- member: 结构体中成员的名称;
工作原理
- offsetof(type, member) 计算的是成员在结构体中的偏移量;
- (char *)(ptr) 将成员指针转换为 char *,然后减去偏移量,即得到结构体的起始地址;
- 最终, (type *) 将这个地址转换为结构体类型的指针。
问:为什么要使用 (char *)(ptr)?
我们在使用 container_of 宏时,需要将结构体成员的指针转换成结构体的起始地址。要做到这一点,我们需要对结构体成员的偏移量进行运算。
在 C 语言中,结构体的成员是按照它们在结构体中声明的顺序连续排列的,所以结构体的成员有一个相对于结构体起始位置的偏移量。
举个例子:
假设有如下结构体:
struct example {
int a; // 偏移量 0
char b; // 偏移量 4
double c; // 偏移量 8
};
a 占用 4 字节,偏移量为 0;
b 占用 1 字节,偏移量为 4(因为 a 是 4 字节,所以 b 紧接着 a);
c 占用 8 字节,偏移量为 8。
假设我们现在有一个指向 b 成员的指针 ptr,如果我们想通过 ptr 获取到整个 example 结构体的指针,就需要知道 b 成员在结构体中的偏移量。
问:为什么要转换成 (char *)?
C 语言中指针的运算是基于指针类型的。也就是说,指针运算会以指针所指向类型的大小为单位进行。例如:
如果你有一个 int * 类型的指针,指针加 1 会增加 4 个字节(假设 int 是 4 字节);
如果你有一个 char * 类型的指针,指针加 1 会增加 1 个字节(因为 char 占 1 字节);
为了能够进行正确的内存地址计算,必须将 ptr 转换为 char * 类型,因为 char 是 C 中最小的存储单位,指针加 1 就表示内存地址加 1 字节。这使得我们可以通过指针运算精确地控制内存地址。
偏移量的计算:
我们知道 b 在结构体中的偏移量是 4 个字节,假设 ptr 是一个指向 b 的指针(类型为 char * 或 char * 的成员指针)。如果我们想通过 ptr 获取 example 结构体的起始地址,可以通过以下方式计算:
将 ptr 转换为 char * 类型,因为 char 的大小是 1 字节。
使用指针运算,减去偏移量,得到结构体的起始地址。
示例
假设有如下结构体:
struct person {
char name[50];
int age;
};
struct company {
char name[100];
struct person ceo; // 嵌入一个 person 结构体
};
如果你有一个指向 person 结构体成员 ceo 的指针,并且想要获取 company 结构体的指针,可以使用 container_of 宏:
struct person *ceo_ptr = &company_instance.ceo;
struct company *company_ptr = container_of(ceo_ptr, struct company, ceo);
解析:
- ceo_ptr 是指向 company 结构体中 ceo 成员的指针;
- container_of 宏根据 ceo_ptr、结构体类型 struct company 和成员 ceo 的名字,反推回 company 结构体的指针;
应用场景
- 内核编程:在 Linux 内核中,container_of 被广泛使用,尤其是在链表、队列和各种数据结构中。很多时候,内核函数会传递某个结构体的成员指针,而你需要根据该指针获取整个结构体指针;
- 链表操作:链表节点通常是结构体的成员,使用 container_of 可以很方便地从链表节点获取整个结构体的指针;