今天分享C语言中的两个宏,这两个宏包含了指针和结构体的知识,非常具有代表性。另外,这个题目曾经是大疆无人机的一道笔试题,可见,这两个宏对C语言基础还是有一定要求的。先说明一下,今天所有的例子都是以32位系统来说的。
废话不多说,今天要说的两个宏分别是offsetof和container_of,第一个宏是用来计算结构体中某个成员相对于结构体的偏移量,第二个宏是已知指向结构体某个成员的指针,来计算结构体的指针。来看一下它们的原型:
define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
下面分别来介绍一下这两个宏。
一、offsetof
这个宏是用来计算结构体某个成员的偏移量的,所以我们先来定义一个简单的结构体类型,来说明。
struct mystruct
{
char a; // 0
int b; // 4
short c; // 8
};
利用这个机构体类型定义一个结构体变量
struct mystruct s;
在这个结构体当中,有三个成员变量,本来char类型是占一个字节,int类型是占4个字节,short类型是占2个字节,一共占7个字节,但是根据结构体的三个对齐原则,我们知道在这里,char占了4个字节,int占了4个字节,short占了4个字节(关于结构体对齐原则不是今天介绍的重点,所以不多介绍)。所以c实际上的偏移量是8,而不是5。这里因为结构体的成员很少,且类型不复杂,所以可以自己手动算出来,但是如果结构体更复杂一些,我们就不可能自己手动去算了,那有什么好的办法呢?
我们知道,C语言给我们提供了一个很好的方式去访问结构体成员,比如结构体变量我们可以用点.去访问,结构体指针我们可以用->去访问,这两种访问方式本质上是通过指针进行访问的,只不过这个过程是编译器帮我们处理了。
比如我们要给变量c赋值,我们可以用简单的方法:
s.c=12;
我们也可以用指针的方法:
short *p=(short*)((int)&s+8);
*p=12;
显然第二种方法要麻烦的多,并且要自己计算偏移量,还要知道变量类型,所以C语言帮我们考虑了这一点,使用简单的点的方式就行了。
既然C语言帮我们做了计算偏移量这件事情,那我们是不是可以反过来利用一下它,先通过点的方式访问变量,再对变量进行取地址运算,减去结构体首地址不就是变量的偏移量了吗?如果首地址是0的话就更好了,直接取地址之后就是偏移量了。
没错,这就是这个宏的思路。
(TYPE *)0:就是将地址0转化为TYPE类型的指针;
((TYPE *)0)->MEMBER通过结构体指针来访问结构体变量;
&((TYPE *)0)->MEMBER对结构体变量进行取地址运算;
((int) &((TYPE*)0)->MEMBER)最后将地址转化为整形,这一步其实可以省略,看你是需要返回整形还是直接返回地址。
我们可以做个简单的实验来验证这一点
二、container_of
上面介绍了offsetof宏的使用,相信不是那么难理解,那么这个宏就看起来复杂多了,但是,其实只要把思路理清楚了,也不是那么复杂。这个宏我在VC6.0编译器上编译的时候是会报错的,其中的typeof这个关键字它就不认识,因此没法做实验,但是在gcc编译器上是可以的,估计因为这个原因,使用的会更少一些,但是这没关系,重要的在于我们能够理解它的原理。
下面是我用这个宏在gcc上做的实验:
这个宏的作用是已知某个结构体成员变量的指针,反过来得到结构体的地址。其实有了上面的那个基础,这个会更简单一些。
既然有了指向结构体成员变量的指针,那么也就是说知道了这个变量的地址,如果我们又知道了这个变量的偏移量,那么利用这个变量的地址减去它的偏移量不就知道结构体地址了吗?没错,它的思路就是这么简单。
下面将这个宏拆分来理解:
((type *)0):将地址0转化为type类型的指针;
((type *)0)->member:通过指针访问member这个成员;
typeof(((type*)0)->member):获取member这个成员的数据类型;
typeof(((type*)0)->member) * __mptr:利用获取的这个类型来定义一个指针变量;
typeof(((type*)0)->member) * __mptr = (ptr);利用上面定义的指针来指向你已知的那个结构成员。
((char *)__mptr -offsetof(type, member));将指针转化为char类型,并且减去偏移量。这里要注意的就是这个偏移量是int类型的,上面说到计算偏移量时可以不强制转化为int型,但是这里做加减时就必须转化为int型了,因为char*类型不能和指针相加减,只能和数字相加减。
(type *)((char *)__mptr -offsetof(type, member));最后将获取的指针再转化为type类型。
可能前面几句可以理解,后面就糊涂了。其实也不难理解,我举个简单的例子。
int *p=&b; //p指向成员b;
((char *)p-4) //p减去偏移量4,不就是结构体地址了吗,只不过这个是char*类型的指针,如果要将它还原成结构体,还得再强制类型转化一次。
(struct mystruct*) ((char *)p-4);
可能还是有些人不理解为什么要先转化成char*类型之后再减4,那么这就涉及到指针的加减问题了。
我们知道,在内存当中,是按字节为单位来编址的,可以想象为一个字节就是一个个的小格子,每个小格子都有一个编号,这个编号实际上就是地址。如果我们定义一个 int *p;那么,每次p加一或者减一都是移动四个字节,而如果定义一个 char *p,那么每次p加一或者减一都是移动一个字节,换句话说,p一次移动的字节数就是它指向的变量的类型所占的字节数。上面因为返回的偏移量是以字节为单位的,所以必须先转化为char*类型才能加减,加减完之后再转化为你需要的类型。
实际上,也不是一定要转化成char*类型的,我们可以将地址先转化为int类型的,加减完之后再转化为指针,这两种方式都是一样的。我们可以做一个小实验来证明这一点
我们可以发现,这两种方法都可以准确的还原结构体的地址。
代码清单:
#include <stdio.h>
struct mystruct
{
char a; // 0
int b; // 4
short c; // 8
int d; //c
};
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
int main(void)
{
struct mystruct s;
printf("s的地址为:%p\n",&s);
int *p;
p=&(s.d);
printf("p的地址为:%p\n",p);
printf("p的偏移量%d\n",offsetof(struct mystruct,d));
printf("方式一得到的s的地址为:%p\n", (struct mystruct *) ( (char*)p-offsetof(struct mystruct,d)));
printf("方式二得到的s的地址为:%p\n", (struct mystruct *) ( (int)p-offsetof(struct mystruct,d)));
return 0;
}
以上就是今天要分享的内容,实际上,这些内容所涉及到的东西是很多的,比如指针,结构体,强制类型转化等等。但是这些归根结底来说,是对内存和数据类型要有非常深刻的理解。必须要先搞清楚什么是内存,还有数据类型的含义到底是什么,变量是什么,变量和数据类型的关系,才能理解上面说的东西,否则的话只是表面懂了,稍微变化一下就不知道怎么办了。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有