三、C高级 - offsetof 宏 和 contaner_of 宏
offsetof
宏:
由
c
标准库的stddef.h
,只要包含这个.h
,就可以使用这个宏
有关stddef.h
,我们在第五章介绍C
标准库的头文件时,我们提到过这个.h
,不知道大家还有映像没有
contaner_of
宏:
这个是
Linux
内核的kernel.h
所提供的宏
其实完全可以自己定义并使用这两个宏,只要你理解了这两个宏的原理,定义这两个宏非常容易
一、访问结构体成员的原理
1. 结构体变量指针 与 第一个成员的指针
“结构体变量的指针”为结构体变量第一个字节的地址,恰好也是第一个成员的第一个字节的地址
如果地址的类型“结构体指针类型”,通过这个指针访问的就是整个结构体变量的空间,
如果为第一个成员的指针类型,通过这个指针访问的就是第一个成员的空间
2. .和->访问成员的原理是一样的
不管是通过.
和->
((*p).
)方式来访问,最终都是“结构体变量指针+成员偏移量
”得到成员指针(成员地址),
然后通过成员指针去访问里面的每个成员
之所以是相加而不是相减,是因为结构体变量的第一个成员都在低地址位置,后面的成员都在高地址位置
3. 通过成员来访问外部结构体变量
通常情况下,我们都是先得到外部结构体变量的指针,然后再去访问里面的成员,
但是反过来,如果我们得到了某个成员的指针之后,能不能通过成员指针计算得到“外部结构体变量”的指针?
答:当然可以!
结构体变量的指针 + 成员偏移 = 成员指针 (这个计算过程,由编译器自动计算完成)
反过来:
结构体变量的指针 = 成员指针 - 成员偏移 (这个需要由我们自己写的代码来计算)
offsetof
和contaner_of
宏就上面的计算公式的具体实现,通过offsetof
和contaner_of
宏,
就可以利用“成员指针”计算出“外部结构体变量”的指针
我们后面讲Linux
内核链表时,内核链表的实现核心,就是通过成员指针来计算出外部结构体变量的指针
二、offsetof宏
offsetof(struct Test, c)
:只要将结构体类型和成员名给它,就能自动计算出成员的偏移量
1. 作用
用于计算成员指针与结构体变量指针间的偏移量,
每个成员具体的偏移量是多少,这个与结构体成员的对齐方式有关
2. 使用举例
#define offsetof(TYPE, MEMBER)((int) & ((TYPE *)0->MEMBER))
struct Test
{
char a;
int b;
short c;
}ts = {'a', 100, 10};
int main(void)
{
int offset = 0;
offset = (int)&(ts.c) - (int)&ts;
printf("&ts.c - &ts= %d\n", offset);
offset = offsetof(struct Test, c);
printf("offset = %d\n", offset);
return 0;
}
通过打印结果可知,通过offsetof
所得到的偏移,与(int)&(ts.c) - (int)&ts
所得到的偏移是一样的,
所以使用offsetof
来计算机成员的偏移是没有问题的
3. offsetof 宏的工作原理
完整写法,把0
看作是结构体变量的地址
( ((int) & (((TYPE *)0)->MEMBER)) - 0 )
成员地址 - 结构体变量地址 = 成员的偏移
具体工作原理
#define offsetof(TYPE, MEMBER)((int) & ((TYPE *)0->MEMBER))
将例子中的offsetof(struct Test, c)
进行宏替换后,就变为了
( ((int) & ( ((struct Test *)0)->c) ) - 0 )
( (struct Test *)0 )
:
将
0
地址强行转换为struct Test *
类型的指针,然后使用0
这个指针,
就能以“struct Test
”的结构去访问0
地址往后的sizeof(struct Test)
大小的空间,
此时0
是结构体变量的首字节地址
程序基于OS
运行时,实际上0
地址并没有对应合法空间,
我们这里只是在借助0
地址来进行“模拟”计算,只要不对0
地址进行解引用,就不会导致指针错误
编译器在编译(((int) & (((struct Test *)0)->c)) - 0)
时,就会按照struct Test
这个类型去模拟计算,
不涉及0
地址的解引用,只要不接应就不会导致指针错误
(int) & ( ( (struct Test *)0 )->c )
:
得到成员
C
地址,然后强制换为int
类型,之所以转为int
,主要为了方便后续的算数运算,得到成员的偏移量
( (int) & ( ( (struct Test *)0 )->c) ) - 0
“成员c的地址 - 结构体变量地址(0)
”,自然就得到了成员的偏移
我们既然已经理解了offsetof
宏的工作原理,那么0
实际上可以改为任何数字,比如:
(((int) & ((TYPE *)1011->MEMBER)) - 1011)
编译器在编译offsetof
宏时,只是在使用结构体类型来模拟计算成员的偏移,
不涉及真实的解引用,不用担心指针错误的问题
三、contaner_of宏
contaner_of
嵌套了offsetof
1. contaner_of 宏的作用
这个宏的作用就是,通过“成员指针” - “成员偏移”
,计算得到外部结构体变量的指针
2. 例子
#define offsetof(TYPE, MEMBER)((int) & (((TYPE *)0)->MEMBER))
#define container_of(ptr, type, member) (type *)((char *)ptr - offsetof(type, member))
struct Test
{
char a;
int b;
short c;
}ts = {'a', 100, 10};
int main(void)
{
printf("%d\n", &ts);
printf("container_of(&ts.c, struct Test, c) = %d\n", container_of(&ts.c, struct Test, c));
return 0;
}
3. container_of 的原理
#define container_of(ptr, type, member) (type *)((char *)ptr - offsetof(type, member))
container_of(ptr, type, member)
的参数
- (a)
ptr
:成员指针 - (b)
type
:结构体类型 - (c)
member
:成员名
通过宏替换来分析原理
对例子中container_of(&ts.c, struct Test, c)
进行宏替换后,就得到了如下结果
(struct Test *) ( (char *)&ts.c - ( (int) & ( (struct Test *)0->c ) )
成员c的指针 - 成员c的偏移
得到结构体变量的地址后,再强制转为struct Test *
,那么这个地址就一个真正的struct Test *
的指针了
疑问:成员指针为什么要强转为char *
?
答:通过第四章节所讲的指针运算,假设是
int *
的话,“成员指针 - 成员偏移
”时就变成了
“成员指针 - 成员偏移
”,显然是不对的,当然将char *
写成void *
也是可以的
4. Linux下的container_of宏
在前面就说过,container_of
宏定义在了Linux
内核的kernel.h
中,
只是Linux
下kernel.h
的container_of
宏,在形式上看起来会稍微复杂一些,当然原理都是一样
kernel.h
中container_of
的原型:
#define container_of(ptr, type, member)\
({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr);\
(type *)( (char *)__mptr - offsetof(type,member) ); \
})
相比我们自己定义的container_of
来说,多了一句
const typeof( ((type *)0)->member ) *__mptr = (ptr);
而且还被({ })
包括起来了
typeof(((type *)0)->member)
:
先通过typeof
关键字提取出成员的类型,如果成员类型为int
,那么提取得到int
这个类型
假设提取到的类型为int
:
const typeof( ((type *)0)->member ) *__mptr = (ptr)
可以被简化为:
const int *__mptr = (ptr)
- 利用提取到的“成员类型”定义一个指针变量
__mptr
- 将“成员指针”从
ptr
转放到__mptr
指针变量中,并修饰为const
疑问:为什么不直接使用ptr
?
答:直接使用
ptr
也是可以的,但是直接使用ptr
存在风险,假如操作失误,
很有可能会通过ptr
这个指针把成员的值给修改了但是转到_
_mptr
中后,由于__mptr
被const
修饰了,操作__mptr
时是无法修改成员的,
自然就避免了这种风险,显然更加的安全
5. 分析Linux下的container_of宏
原型:
#define container_of(ptr, type, member)\
({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr);\
(type *)( (char *)__mptr - offsetof(type,member) ); \
})
示例代码:
#define container_of(ptr, type, member) (type *)((char *)ptr - offsetof(type, member))
struct People *p = container_of(&ts.c, struct Test, c);
进行宏替换后:
struct People *p = ({
const typeof( ((struct Test *)0)->c ) *__mptr = (&ts.c);
(struct Test *)( (char *)__mptr - offsetof(struct Test, c));
});
第二条语句的计算结果会赋给p
,类似如下情况:
int a = ({
int aa=10;
int b = aa+20;
b+40; //最后一句话的相加结果会交给a,
//最后一个不能包含类型,比如int c = b+40,int c是无法编译通过的
});
疑问:({...; ...; ...;})
有什作用?
答:这个是
gcc
支持特有的一种语法,换一个gcc
以外的其它编译器(windows
c
编译器),将无法识别
加上这个到底有什么作用,我们在第八章中会解释。
6. typeof 关键字
前面说过,typeof关键字的作用是用于提取变量、数据的类型,比如:
int a;
typeof(a) b = 10; //等价于int b = 10
typeof(10.33) c = 20.5; //等价于 double c = 20.5
printf("%d\n", b);
printf("%f\n", c);
不过typeof
这个关键字是GNU
标准扩展的关键字,在C
标准中并不支持这个关键字,
当出现如下两种情况时,typeof
将无法使用:
1)使用的不是gcc
编译器
这个是
GNU
标准的扩展关键字,gcc
是GNU
推出的编译器,所以如果使用的不是这个GNU
的gcc
编译器的话,
可能无法识别typeof
2)使用的是gcc
编译器,但是如果指定编译标准时,指定为-std=c99
或者c11
等C
标准,将也无法识别,
因为
C
标准不支持,只有指定-std=gnu99
或者gnu11
等gnu
标准时才能识别,默认情况下gcc
编译时,
都是按照gnu
标准来编译的,如果我们在编译时特意的指定-std=c99
选项的话,是无法编译通过的