工欲善其事必先利其器-C语言拓展–嵌入式C语言(三)
typeof()
使用typeof可以获取一个变量或表达式的类型。
typeof的参数有两种形式:表达式或类型。
int i; typeof(i) j = 20; --> int j = 20; typeof(int *) a; -->int *a; int f(); -->typeof(f()) k;--? int k
我们可以看出通过typeof获取一个变量的类型int后,可以使用该类型再定义一个变量。
高级用法:
typeof (int *) y;-->int *y y是一个指向int类型的指针 typeof (int) *y; 执行int类型的指针变量y typeof (*x) y; y是一个指向x类型的指针 (这下是不是对前面两个的小小区别有所感悟) typeof (int) y[4]; y这个数组元素的类型是int (换成x)就是x的类型 int y[4] typeof (*x) y[4]; *x y[4] typeof (typeof (char *)[4]) y;//-->char *y[4] 字符指针数组 这个里面的数组元素都是 指针变量 typeof (char *)[4] --> char *[4] type (char *)类型不就是括号里面的 然后 在加一层--> typeof(int x[4]) y; int y[4] 对于以上其实观察的时候就一层层的剥开即可
typeof对于数组的操作还是挺花哨的,惊艳了。
container_of宏
Linux内核第一宏:container_of
听起来这么牛的宏,我们来一睹芳容一下
#define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER) #define container_of(ptr,type,member)({ \ const typeof( ((type *)0)->member) * __mptr = (ptr);\ (type *)((char *)__mptr - offsetof(type,member));})
怎么样,内核里到处都是这。
宏中有宏。
三个参数:
- type: 结构体的类型
- member: 结构体成员
- ptr: 结构体成员member地址
这个宏的作用:
通过结构体的某一成员的地址,来获取这个结构体的首地址。
这下再看是不是大概还是好点了。
我们来看个实用例子。
struct student { int age; int num; int math; }; int main(void) { struct student stu; struct student *p; p = container_of(&stu.num, struct student, num); return 0; }
定义一个结构体类型student,
然后定义一个结构体变量stu,
知道了结构体成员变量stu.num的地址,
那么我们就可以通过container_of宏来获取结构体变量stu的首地址。
container_of这个玩意有什么用呢?
因为内核中有很多的结构体类型数据,为了抽象,会对结构体进行多次的封装,往往一个结构体里面又包含了多个结构体。无限套娃。不同的层次,不同的模块,使用的是对应的不同封装程度的结构体。
这个是不是有点让你联想到面向对象思想,没想到?没事我知道你认真专注于当下。
这样的优点我就不多说了。面向对象的优点。
在内核中,我们传递个函数的参数是某个结构体的成员,在这个函数中,还想用这个结构体的其他变量,这不是就需要container_of出场了。
找到了这个结构体的首地址–>
offsetof()函数主要用来计算member成员相对于结构体起始地址的偏移量。
现在我们来详细看看这个宏定义:
这里需要你先知道结构体怎么存储的。
结构体作为一个复合类型数据,它里面可以有多个成员。当我们定义一个结构体变量时,编译器要给这个变量在内存中分配存储空间。根据每个成员的数据类型和字节对齐方式,编译器会按照结构体中各个成员的顺序,在内存中分配一片连续的空间来存储它们。
将数字0通过强制类型转换,转换为一个指向结构体类型为student的常量指针,然后分别打印这个常量指针指向的各成员地址。运行结果如下。
因为常量指针的值为0,即可以看作结构体首地址为0,所以结构体中每个成员变量的地址即该成员相对于结构体首地址的偏移。
知道了结构体的相对偏移地址,用结构体成员的地址减去相对偏移,就可以得到结构体的首地址。
从语法角度来看,container_of宏的实现由一个语句表达式构成。语句表达式的值即最后一个表达式的值。
取结构体某个成员member的地址,减去这个成员在结构体type中的偏移,运算结果就是结构体type的首地址。
因为语句表达式的值等于最后一个表达式的值,所以这个结果也是整个语句表达式的值,container_of最后会返回这个地址值给宏的调用者。
结构体的成员数据类型可以是任意数据类型,为了让这个宏兼容各种数据类型,我们定义了一个临时指针变量__mptr,该变量用来存储结构体成员MEMBER的地址,即存储宏中的参数ptr的值。如何获取ptr指针类型呢。
#define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER) #define container_of(ptr,type,member)({ \ const typeof( ((type *)0)->member) * __mptr = (ptr);\ (type *)((char *)__mptr - offsetof(type,member));}) 三个参数: + type: 结构体的类型 + member: 结构体成员 + ptr: 结构体成员member地址 + 指针变量__mptr:存储结构体成员MEMBER的地址-->ptr
我们知道,**宏的参数ptr代表的是一个结构体成员变量MEMBER的地址,**所以ptr的类型是一个指向MEMBER数据类型的指针,当我们使用临时指针变量__mptr来存储ptr的值时,必须确保__mptr的指针类型和ptr一样,是一个指向MEMBER类型的指针变量。
确保__mptr的指针类型和ptr一样
typeof(((type*)0)->member)表达式使用typeof关键字,用来获取结构体成员MEMBER的数据类型,然后使用该类型,通过typeof(((type*)0)->member)*__mptr这条程序语句,就可以定义一个指向该类型的指针变量了。
在语句表达式的最后,*因为返回的是结构体的首地址,所以整个地址还必须强制转换一下,转换为TYPE,即返回一个指向TYPE结构体类型的指针,*所以你会在最后一个表达式中看到一个强制类型转换(TYPE)
这个文章也写的不错,讲的蛮清楚的。
一个parent代表结构体的类型,name代表结构体中的成员。
*((parent)0),**把数字0强制转换成parent 结构体指针类型。这样 ((parent )0) 这个整体就相当于一个指针指向了 0 这个地址,不管 0 这个地址是否合法,是否真的有这么一个结构体对象,它都会把以 0 地址为首的一片连续内存当成一个结构体对象操作。
((parent)0)->name,* *结构体指针((parent)0)取结构体对象中name成员。因为这只是对内存操作,**并没有写内存,虽然地址不合法也不会出现段错误。
*&((parent )0)->name对name成员取地址。
*offset = (uint32_t)&((parent )0)->name偏移量。因为这个parent类型结构体对象是从0地址开始的,故而offset就是成员name的偏移量。
知道成员偏移量,就很容易求结构体对象本身的地址。成员的地址减去偏移量就是是结构体对象的首地址! – 本来的变量地址-便宜地址就是开始地方
((parent*)0)((uint32_t)node - (uint32_t)&((parent*)0)->name)结构体对象的首地址。
然后就可以开始访问变量了。->
到这里你应该懂了这个原理
[学习资料]:(https://www.freesion.com/article/57261214164/)
《嵌入式C语言自我修养:从芯片、编译器到操作系统》