第三十一课 老生常谈的两个宏

Posted 天道酬勤

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第三十一课 老生常谈的两个宏相关的知识,希望对你有一定的参考价值。

 

TYPE应该是一个结构体类型,MEMBER是结构体中的一个成员。

使用了这个宏之后可以得到MEMBER在TYPE中的偏移量。

0被强制类型转换成TYPE指针。

在计算机的0地址处没有TYPE结构体,0地址是留给操作系统使用的。

我们存在的疑问是将0地址转换为TYPE类型的指针,然后取MEMBER成员,这里会引起崩溃吗?

编译器到底做了什么?

&((TYPE*)0)->MEMBER 这个语句就是根据结构体的首地址和相应成员的偏移量获得成员的具体地址,这里我们将0转换为TYPE类型的指针,最终获得的成员的绝对地址和偏移地址是一样的。这句话并没有真正的去内存中取MEMBER这个成员。

 示例程序:

 

 1 #include <stdio.h>
 2 
 3 
 4 struct ST
 5 {
 6     int i;     // 0
 7     int j;     // 4
 8     char c;    // 8
 9 };
10 
11 void func(struct ST* pst)
12 {
13     int* pi = &(pst->i);    //  0
14     int* pj = &(pst->j);    //  4
15     char* pc = &(pst->c);   //  8
16 
17     printf("pst = %p\\n", pst);
18     printf("pi = %p\\n", pi);
19     printf("pj = %p\\n", pj);
20     printf("pc = %p\\n", pc);
21 }
22 
23 int main()
24 {
25     struct ST s = {0};
26 
27     func(&s);
28 
29     return 0;
30 }

结果如下:

 

 以空指针调用:

 

 1 #include <stdio.h>
 2 
 3 
 4 struct ST
 5 {
 6     int i;     // 0
 7     int j;     // 4
 8     char c;    // 8
 9 };
10 
11 void func(struct ST* pst)
12 {
13     int* pi = &(pst->i);    //  0
14     int* pj = &(pst->j);    //  4
15     char* pc = &(pst->c);   //  8
16 
17     printf("pst = %p\\n", pst);
18     printf("pi = %p\\n", pi);
19     printf("pj = %p\\n", pj);
20     printf("pc = %p\\n", pc);
21 }
22 
23 int main()
24 {
25     struct ST s = {0};
26 
27     func(&s);
28     func(NULL);
29 
30     return 0;
31 }

 

结果如下:

 

空指针调用func并没有崩溃,而且得到了想要的结果。

可见编译器只是计算成员的地址,并没有实际去取成员的值。也没有访问内存,只是做了一些计算。

 

offsetof的使用示例:

 1 #include <stdio.h>
 2 
 3 #ifndef offsetof
 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
 5 #endif
 6 
 7 
 8 struct ST
 9 {
10     int i;     // 0
11     int j;     // 4
12     char c;    // 8
13 };
14 
15 void func(struct ST* pst)
16 {
17     int* pi = &(pst->i);    //  0
18     int* pj = &(pst->j);    //  4
19     char* pc = &(pst->c);   //  8
20 
21     printf("pst = %p\\n", pst);
22     printf("pi = %p\\n", pi);
23     printf("pj = %p\\n", pj);
24     printf("pc = %p\\n", pc);
25 }
26 
27 int main()
28 {
29     struct ST s = {0};
30 
31     func(&s);
32     func(NULL);
33 
34     printf("offset i: %d\\n", offsetof(struct ST, i));
35     printf("offset j: %d\\n", offsetof(struct ST, j));
36     printf("offset c: %d\\n", offsetof(struct ST, c));
37 
38     return 0;
39 }

结果如下:

 

{}包装的是一个代码块,内部定义的局部变量在外面是看不到的。

{}外面加上一层()就表示留下{}中最后一个语句的值。

示例程序1:

 1 #include <stdio.h>
 2 
 3 #ifndef offsetof
 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
 5 #endif
 6 
 7 
 8 struct ST
 9 {
10     int i;     // 0
11     int j;     // 4
12     char c;    // 8
13 };
14 
15 void method_1()
16 {
17     int a = 0;
18     int b = 0;
19 
20     int r = (
21            a = 1,
22            b = 2,
23            a + b
24                 );
25 
26     printf("r = %d\\n", r);
27 }
28 
29 int main()
30 {
31     method_1();
32 
33     return 0;
34 }

结果如下:

 

示例程序2:

 1 #include <stdio.h>
 2 
 3 #ifndef offsetof
 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
 5 #endif
 6 
 7 
 8 struct ST
 9 {
10     int i;     // 0
11     int j;     // 4
12     char c;    // 8
13 };
14 
15 void method_1()
16 {
17     int a = 0;
18     int b = 0;
19 
20     int r = (
21            a = 1,
22            b = 2,
23            a + b
24                 );
25 
26     printf("r = %d\\n", r);
27 }
28 
29 void method_2()
30 {
31     int r = ( {
32                   int a = 1;
33                   int b = 2;
34 
35                   a + b;
36               } );
37 
38     printf("r = %d\\n", r);
39 }
40 
41 int main()
42 {
43     method_1();
44     method_2();
45     return 0;
46 }

结果如下:

 

 

 

示例程序3:

 1 #include <stdio.h>
 2 
 3 #ifndef offsetof
 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
 5 #endif
 6 
 7 
 8 struct ST
 9 {
10     int i;     // 0
11     int j;     // 4
12     char c;    // 8
13 };
14 
15 void method_1()
16 {
17     int a = 0;
18     int b = 0;
19 
20     int r = (
21            a = 1,
22            b = 2,
23            a + b
24                 );
25 
26     printf("r = %d\\n", r);
27 }
28 
29 void method_2()
30 {
31     int r = ( {
32                   int a = 1;
33                   int b = 2;
34 
35                   a + b;
36               } );
37 
38     printf("r = %d\\n", r);
39 }
40 
41 void type_of()
42 {
43     int i = 100;
44     typeof(i) j = i;
45     const typeof(j)* p = &j;
46 
47     printf("sizeof(j) = %d\\n", sizeof(j));
48     printf("j = %d\\n", j);
49     printf("*p = %d\\n", *p);
50 }
51 
52 int main()
53 {
54     method_1();
55     method_2();
56     type_of();
57     return 0;
58 }

结果如下:

 

 

做指针运算的时候pc要先强制转换为char*。从pc开始,向后退offset个字节,就可以得到结构体变量的地址。

 

container_of原理剖析:

1 #ifndef container_of
2 #define container_of(ptr, type, member) ({                 \\
3         const typeof(((type*)0)->member)* __mptr = (ptr);   \\
4         (type*)((char*)__mptr - offsetof(type, member)); })
5 #endif

在这个宏中,我们看到了圆括号()嵌套花括号{},这正是我们前面讲到的。

通过ptr这个指向成员member的指针,来反推出member这个成员所对应的结构体变量的指针。

示例程序:

 1 #include <stdio.h>
 2 
 3 #ifndef offsetof
 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
 5 #endif
 6 
 7 #ifndef container_of
 8 #define container_of(ptr, type, member) ({                 \\
 9         const typeof(((type*)0)->member)* __mptr = (ptr);   \\
10         (type*)((char*)__mptr - offsetof(type, member)); })
11 #endif
12 
13 struct ST
14 {
15     int i;     // 0
16     int j;     // 4
17     char c;    // 8
18 };
19 
20 
21 
22 int main()
23 {
24     struct ST s = {0};
25     char* pc = &s.c;
26 
27     struct ST* pst = container_of(pc, struct ST, c);
28 
29     printf("&s = %p\\n", &s);
30     printf("pst = %p\\n", pst);
31 
32     return 0;
33 }

结果如下:

 

 可以看到两个值是一样的。

container_of宏中的 const typeof(((type*)0)->member)* __mptr = (ptr);看起来是多余的,我们改写一下:

 1 #include <stdio.h>
 2 
 3 #ifndef offsetof
 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
 5 #endif
 6 
 7 #ifndef container_of
 8 #define container_of(ptr, type, member) ({                 \\
 9         const typeof(((type*)0)->member)* __mptr = (ptr);   \\
10         (type*)((char*)__mptr - offsetof(type, member)); })
11 #endif
12 
13 #ifndef container_of_new
14 #define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
15 #endif
16 
17 struct ST
18 {
19     int i;     // 0
20     int j;     // 4
21     char c;    // 8
22 };
23 
24 
25 
26 int main()
27 {
28     struct ST s = {0};
29     char* pc = &s.c;
30 
31     struct ST* pst = container_of_new(pc, struct ST, c);
32 
33     printf("&s = %p\\n", &s);
34     printf("pst = %p\\n", pst);
35 
36     return 0;
37 }

结果如下:

 

再次给出linux内核中的container_of宏:

1 #ifndef container_of
2 #define container_of(ptr, type, member) ({                 \\
3         const typeof(((type*)0)->member)* __mptr = (ptr);   \\
4         (type*)((char*)__mptr - offsetof(type, member)); })
5 #endif

const typeof(((type*)0)->member)* __mptr = (ptr);代码的作用就是做类型检查。

示例程序:

 1 int main()
 2 {
 3     struct ST s = {0};
 4     char* pc = &s.c;
 5     int e = 0;
 6     int* pe = &e;
 7 
 8     struct ST* pst = container_of_new(pe, struct ST, c);
 9 
10     printf("&s = %p\\n", &s);
11     printf("pst = %p\\n", pst);
12 
13     return 0;
14 }

在第8行的程序中,第一个参数传入pe,第三个参数传入c,pe并不是指向c的指针,这时候使用containter_of_new宏不会报错,没有类型检查。

运行出来的结果也是不对的:

我们将第8行换成linux内核的宏container_of,编译结果如下:

这时候就多报了一个警告出来。

告诉我们存在类型不兼容的情况。

要想类型检查就得多加一行指针定义,指针定义又不可能存在逗号表达式里面,因此,linux内核使用了圆括号嵌套花括号的形式。

const typeof(((type*)0)->member)* __mptr = (ptr); 语句中,先将0转换为type类型的指针,然后获得member成员,然后通过typeof获得这个成员的类型,然后定义这个类型的指针变量__mptr,并将ptr作为这个变量的初始化值。正常情况下,ptr是指向member的,类型一致,不会报警高,但是我们传参数错误时,就会报类型不一致的警高。

 typeof在编译器就得到了类型,不会等到运行期,所以这一行语句通过0地址指向member也不会崩溃。

 

我们自己改写的那一个宏没有了typeof,但是可以在标准的编译器下使用了。

 

 小结:

 

 

 

以上是关于第三十一课 老生常谈的两个宏的主要内容,如果未能解决你的问题,请参考以下文章

开始写游戏 --- 第三十一篇

WPF学习第三十一章 WPF命令模型

小刘同学的第三十一篇博文

2018-07-26 第三十一次课

第三十一节,使用谷歌Object Detection API进行目标检测

“全栈2019”Java第三十一章:二维数组和多维数组详解