无意间在腾讯课堂上看到有个老师讲解linux内核链表,开始就讲这两个宏。
这篇文章主要是为了记录对这两个宏的使用和理解。
测试环境:
- win10 64bit 家庭版
- gcc version 6.3.0 (MinGW.org GCC-6.3.0-1)
为了描述方便, 示例代码中会使用这样的结构:
typedef struct Node {
double d;
int i;
} Node;
下面正式开始
宏定义概述
offsetof:
宏定义:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
功能: 获取结构体中成员变量MEMBER相对于结构体的地址偏移量
container_of:
宏定义:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
功能: 获取结构体成员所在的结构体变量的地址
为了有一个比较深刻的理解, 用代码逐步演示.
offsetof
-
(TYPE *)0
理解
这写法比较少见, 因为平时写得最多的可能是这样:#include <stdio.h> int main() { // 地址:低 ---------------> 高 // 小端存储(16进制): 45, 44, 43, 42 int i = 0x42434445; int* pi = &i; char* pc = (char*)pi; // 类型转换 // 61ff24, 61ff24 printf("%x, %x\n", pi, pc); // E, D, C, B printf("%c, %c, %c, %c\n", *pc, *(pc + 1), *(pc + 2), *(pc + 3)); return 0; }
上面
(char*)pi
是将一个int*
转成char*
, 什么意思呢?
就是将pi
处开始的内存用char
类型来解释,i
变量占了4个字节的内存,
其值按小端存储分别为(16进制): 45, 44, 43, 42, 此时pc
的值就是45所在的内存地址,
即*pc的值为'E'(ASCII为45)
, 从运行结果也可以看出.再来看
(TYPE *)0
, 假想0是某一个int类型指针p的值, 这不就是将p转换成TYPE类型的指针吗?
有人可能说, 0地址处不是操作系统占用了吗? 这样会不会报错?
不会, 因为只做了转换, 并没有向0地址处进行读写操作.
那么,(TYPE *)0
相当于0地址处存了一个TYPE类型的结构体, &((TYPE *)0)->MEMBER就是求MEMBER成员的地址.
再回头看看offsetof宏, 就是为了求出变量MEMBER相对于结构体的地址偏移量. -
offsetof使用示例:
Node n = { .d = 3.14, .i = 6, }; Node* pNode = NULL; // Node中: d在第一个, i在d后面定义, 成员i的偏移为一个double的长度, assert(offsetof(Node, i) == sizeof(double)); // d定义在Node中第一个位置, 所以偏移为0 assert(offsetof(Node, d) == 0); assert(&n.i == (char*)&n + sizeof(double)); // 事实上, 由于成员d偏移为0, 所以d的地址和n的地址一样 assert(&n.d == &n); // i的地址偏移其实就是一个double assert(&n.i == (char*)&n + sizeof(double));
这个offsetof好像没啥用啊? 平时不用它也工作得很好啊.
其实它是为了container_of服务的, 直接用它的情况确实很少.
container_of
-
container_of(ptr, type, member)
先说说三个参数:- ptr: 结构体变量的指针(比如上面的
&n.i
) - type: 结构体类型(如Node)
- member: 结构体成员名(如
i
)
- ptr: 结构体变量的指针(比如上面的
-
typeof 关键字
typeof
是GNUC的扩展语法, 用它可以获取变量的类型, 如:int j = 0; typeof(j) j2 = 1; printf("%d\n", j2); // 1
-
({})
container_of定义中({stmt1; stmt2;})
是GNUC的扩展语法, 其值是最后一个语句的运算结果. 如:int a = ({ int i = 10; int j = 100; i > j ? i : j; }); printf("%d\n", a); // 100
-
container_of实现原理
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
其中
const typeof( ((type *)0)->member ) *__mptr = (ptr);
是为了做类型转换, 暂时不管, 后面会说到.
因为正常情况下我们给第一个参数传递的就是&n.i
这样值, 也就是对应成员变量的指针.
那么container_of最后的结果其实就相当于:
(type *)( (char *)ptr- offsetof(type,member) );
用成员变量的地址, 减去这个成员变量相对于其所在结构体的地址偏移, 不就是其所在结构体变量的地址吗?
看代码更好懂:Node n = { .d = 3.14, .i = 6, }; Node* pNode = NULL; // 通过n.i的地址反推n的地址 pNode = container_of(&n.i, Node, i); assert(pNode == &n); assert(pNode->i == 6);
嗯! container_of其实也好简单嘛. 但是有些细节还是要说下.
-
感觉没用的第一句(
const typeof... = (ptr)
)
对于const typeof( ((type *)0)->member ) *__mptr = (ptr);
这个在正常使用时好像看不出来有什么用.
其实它的作用时,在编译时就能知道ptr与member的类型是否匹配.char tmp = 'a'; char* ptmp = &tmp; pNode = container_of(ptmp, Node, i); // assert(pNode == &n); // assert failed
这样在编译时会产生警告:
warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]|
当然此时计算的pNode的值与&n不相等.
-
为啥是(char *)
有人会问(包括我)为啥(char *)__mptr - offsetof(type,member)
这里要用char*强转一下.
想像一下, 如果__mptr此时是int*, 然后offsetof(type,member)的值为4, 那么就相当于回退了16, 而预期的是回退4.由于我的测试环境中, 一个int占4个字节, 一个double占8个字节, 则i相对于Node的偏移为8,
所以&n.i - 2
相当于i的地址值减8.assert(&n.i - 2 == &n); // success assert(&n.i - sizeof(double) == &n); // failed
所以
char*
强转是为了保证回退时步长为1. 可以将原宏中的(char *)__mptr - offsetof(type,member)
改成__mptr - offsetof(type,member)
试试就知道了.
注意
- offsetof也提醒我们, struct一旦定义, 成员变量位置就不要乱动了, 可能会引起不必要的问题.
- 内存对齐问题也要注意
参考:
https://ke.qq.com/course/223662#term_id=100264105
https://blog.csdn.net/npy_lp/article/details/7010752
欢迎补充指正.