#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
#ifndef container_of
#define container_of(ptr, type, member) ({ \
const typeof(((type*)0)->member)* __mptr = (ptr); \
(type*)((char*)__mptr - offsetof(type, member)); })
#endif
首先分析 offsetof宏
offsetof 用于计算 TYPE 结构体中 MENBER 成员的偏移位置
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
该宏最值得注意的就是 ((TYPE*)0)->MEMBER) 这个写法。这里是直接把0地址 强制转换为 TYPE* 指针,指向的空间为TYPE模子扣出来的空间,第一眼看,很多人都会奇怪,这么明晃晃的直接使用0地址,难道不会导致程序崩溃吗,很多人会以为 在0地址处有一个 TYPE类型的结构体,但是这是不可能的,0地址是给操作系统使用的。
所以 编译器到底是怎么解读这段代码的呢?
1 编译器在编译的时候就已经清楚的知道了结构体成员变量的偏移位置了。
2 编译器通过结构体成员变量首地址与偏移量定位成员变量
以上两点就是编译所做的事情,直接这样讲,有时候我自己都有点似懂非懂,直接看代码吧:
struct ST
{
int i; // 0
int j; // 4
char c; // 8
};
struct ST s = {0};
struct ST* pst = &s;
/*
以下取成员变量地址的操作的本质其实就是 根据结构体的首元素地址加上编译器知道的成员变量的偏移地址得到的。
*/
int* pi = &(pst->i); // 0 pst首地址 + i成员的偏移量
int* pj = &(pst->j); // 4 pst首地址 + j成员的偏移量
char* pc = &(pst->c); // 8 pst首地址 + c成员的偏移量
这就是编译器所做的事情,只是做指针运算,并没有做内存访问!!!
所以,就算这里的 pst指针 指向的是0地址,也没什么大不了的,因为只是做指针运算,并不是访问内存。 而用NULL作为参数 可以很方便的直接得到 成员变量在结构体中偏移位置!
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
可以看成
(unsigned int) &((struct ST*)0 -> i)
即得到变量i 的偏移量。
实验:
#include <stdio.h
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
struct ST
{
int i; // 0
int j; // 4
char c; // 8
};
void func(struct ST* pst)
{
int* pi = &(pst->i); // 0
int* pj = &(pst->j); // 4
char* pc = &(pst->c); // 8
printf("pst = %p\n", pst);
printf("pi = %p\n", pi);
printf("pj = %p\n", pj);
printf("pc = %p\n", pc);
}
int main()
{
struct ST s = {0};
func(&s);
//传入空指针
func(NULL);
printf("offset i: %d\n", offsetof(struct ST, i));
printf("offset j: %d\n", offsetof(struct ST, j));
printf("offset c: %d\n", offsetof(struct ST, c));
return 0;
}
可以发现 传入空指针,程序并没有崩溃,原因就是 本质上,以上程序,编译器只是做了地址的指针运算,即加上偏移量,而并没有访问地址中的数据。
再分析 container_of
#ifndef container_of
#define container_of(ptr, type, member) ({ \
const typeof(((type*)0)->member)* __mptr = (ptr); \
(type*)((char*)__mptr - offsetof(type, member)); })
#endif
关于 ({}) 这个写法是什么意思?
将花括号{}中的代码块封装到圆括号中(),表示留下代码段的最后一个语句的值。代码段中的 a b 都是局部变量,这是与逗号表达式不同的地方。
关于 typeof
所以
#ifndef container_of
#define container_of(ptr, type, member) ({ \
const typeof(((type*)0)->member)* __mptr = (ptr); \
(type*)((char*)__mptr - offsetof(type, member)); })
#endif
可以这样分析:
1
#define container_of(ptr, type, member)
== (type*)((char*)__mptr - offsetof(type, member));
2
__mptr == ptr,表示member成员的真是地址
3
offsetof(type, member)); 表示 member成员在结构体type中的偏移量
4
做减法就可以得到 member成员所在结构体的首地址
可以简单理解为:
#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
#endif
但是 Linux 为什么在中间加上了
const typeof(((type*)0)->member)* __mptr = (ptr); \
原因在于,宏是预处理器处理的,不能做类型检查的,这行代码就是用于做类型检查
typeof(((type*)0)->member) 用于获取 成员变量 member的类型,即保证 container_of宏 参数1地址类型必须和 member类型一致,即保证传入的地址是member的地址,否则就是参数传入错误!!
所以这里也就是引入 GNU C 编译器扩展语法 ({}) 的原因了。这里为了增加代码的安全性,为了有一点点的类型安全检查,所以设计container_of 宏的时候 加上了 这条指针定义语句:
const typeof(((type*)0)->member)* __mptr = (ptr);
而指针定义是不可能存在于逗号表达式中的,所以使用了 GNU C 编译器扩展语法 ({})
注意:
typeof 和 ({}) 只能用于 GNU C 编译器环境中。如果不是 GNU C 编译器。可以考虑使用改写版,即不做类型检查:
#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
#endif
实验:
#include <stdio.h>
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
#ifndef container_of
#define container_of(ptr, type, member) ({ \
const typeof(((type*)0)->member)* __mptr = (ptr); \
(type*)((char*)__mptr - offsetof(type, member)); })
#endif
#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
#endif
struct ST
{
int i; // 0
int j; // 4
char c; // 8
};
void method_1()
{
int a = 0;
int b = 0;
int r = (
a = 1,
b = 2,
a + b
);
printf("r = %d\n", r);
}
void method_2()
{
int r = ( {
int a = 1;
int b = 2;
a + b;
} );
printf("r = %d\n", r);
}
void type_of()
{
int i = 100;
typeof(i) j = i;
const typeof(j)* p = &j;
printf("sizeof(j) = %d\n", sizeof(j));
printf("j = %d\n", j);
printf("*p = %d\n", *p);
}
int main()
{
// method_1();
// method_2();
// type_of();
struct ST s = {0};
char* pc = &s.c;
int e = 0;
int* pe = &e;
struct ST* pst = container_of(pc, struct ST, c);
printf("&s = %p\n", &s);
printf("pst = %p\n", pst);
return 0;
}