C语言小结--offsetof和container_of宏的使用

在Linux内核中这两个宏的使用非常普遍,所以研究透彻这两个宏非常有必要。接下来详细介绍一下这两个宏的使用。

1、offsetof宏的使用

这个宏比较简单,其作用就是求一个结构体成员变量在这个结构体中的偏移量。在Linux kernel中的路径是:include/linux/stddef.h
我们先来看一下这个宏的原型:

#define offsetof(TYPE, MEMBER)  ((int)&(((TYPE *)0)->MEMBER))

根据C语言的符号优先级我们拆解来试试:
1、找到变量:0;
2、和变量 0 结合的是:( TYPE * ) ; 这说明变量0 被强制转换为 TYPE类型的指针,且这个指针的地址是0 , 这点非常重要,是理解这个宏的关键。
3、然后是: -> MEMBER 说明取类型为TYPE的指针变量里面的值:MEMBER;
4、然后和&结合取其地址。通过2分析我们知道这个指针的地址是从0开始的,所以现在取出来的地址其实就是这个变量在这个结构体中的偏移地址。
5、最后加一个(int)强制转换为int型数据。

通过以上的分析我们知道,这个宏其实就是将一个地址为0的值强制转换为这个类型的结构体,然后求其相对于0地址的偏移量,说的直白一点就是想让编译器自己帮我们算出这个结构体成员变量在结构体中的偏移量。

接下来写个简单的函数来测试一下:

#include <stdio.h>

#define offsetof(TYPE, MEMBER)  ((int)&(((TYPE *)0)->MEMBER))

typedef struct 
{
    char a[9];
    int b;
    float c;
}mystrut;


int main(void)
{
    mystrut s1;
    memccpy(s1," hello world!");

    printf("offsetof(s1,c) = %d, \n ",(offsetof(mystrut,c)));

    return 0;
}

按照上面的理解,我们计算出来的值应该是:9+4=13字节。从0开始计算,那么我们的输出结果应该是:13 ,运行结果如下:

root@ubuntu:/mnt/hgfs/share/code/c_advance/containerof# gcc containerof.c 
root@ubuntu:/mnt/hgfs/share/code/c_advance/containerof# ./a.out 
offsetof(s1,c) = 16, 

值怎么是16呢? 其实是Ubuntu系统的4字节数据对齐导致的,char a[9] 占用9个字节,但是在内存中四字节对齐就占用了12字节内存,再加上一个int型b占四个字节。共16个字节。从0开始计算,所以最后的结果是16。
我们可以将结构体中的char改为int再看一下输出结果。

#include <stdio.h>

#define offsetof(TYPE, MEMBER)  ((int)&(((TYPE *)0)->MEMBER))

typedef struct 
{
    int a[23];
    int b;
    float c;
}mystrut;


int main(void)
{
    mystrut s1;

    printf("offsetof(s1,c) = %d, \n ",(offsetof(mystrut,c)));

    return 0;
}

运行结果:offsetof(s1,c) = 96,
简单分析一下:23*4 +4 = 96。说明这个宏是正确的。 细心的同学可能要发问了:你将0地址强制转换为(TYPE *)类型,0 地址什么概念,能随便转吗? 其实大可不必担心,我们只是将0地址强制转换了一下,但是没有引用和调用,没什么关系。况且,我们只是在预处理阶段用了一下而已,不会有什么安全隐患的,放心好了。

2、container_of宏的使用

这个宏的作用是根据当前的结构体成员的地址来获取整改结构体的地址。
这个宏的原型如下:
/** 
 * container_of - cast a member of a structure out to the containing structure 
 * @ptr:    the pointer to the member. 
 * @type:   the type of the container struct this is embedded in. 
 * @member: the name of the member within the struct. 
 * 
 */  
#define container_of(ptr, type, member) ({          \  
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
    (type *)( (char *)__mptr - offsetof(type,member) );})  

其中:
ptr:指向成员的指针。
type:需要返回的结构实例类型。
member:成员在结构实例内部的名称,如果为数组,需要指定下标。

我们拆解分析一波。
1、这个宏比较长,分为三行,中间用 “\” 隔开,表示工鞥上连接为一行。
2、第一行:是宏的原型,有三个输入参数:ptr type member
3、第二行:const typeof( ((type )0)->member ) *__mptr = (ptr); 这个比较有意思,定义一个临时变量:__mptr ; (通过typeof( ((type )0)->member )获得)与member相同的指针变量__mptr,然后用它来保存ptr的值。指出一点是,ptr指的就是结构体中member的地址,所以 _mptr的指针类型和ptr的类型是一致的。
4、第三行:先把这个变量转换为(char )类型的指针,为什么转为char 类型呢?因为我们知道指针运算和指针指向的数据类型是有关系的。我们ofsetof返回的结果是int型的,所以转换成char * 类型说明每次指移动一个字节,这样就和整形的数据运算一一对应了。 然后减去 member在type中的偏移量,最后在强制转换为(type *)类型。

补充讲一点:

typeof关键字是C语言中的一个新扩展,这个特性在linux内核中应用非常广泛。 通过以下几个例子讲一下他的用法:

1、把y定义成x指向的数据类型:
typeof(*x) y;
2、把y定义成x指向数据类型的数组:
typeof(*x) y[4];
3、把y定义成一个字符指针数组:
typeof(typeof(char *)[4] y;
这与下面的定义等价:
char *y[4];

总的来说就是为了获取括号中的表达式的数据类型。注意:当typeof(函数) 这种用法时这个函数实际上并没有执行,只是为了获取该函数的返回值类型。如:int fun() ; typeof(fun()) b; 其等价于 int b; 这里的fun() 并没有被执行。

接下来我们通过一个例子来实地分析一下。

#include <stdio.h>

#define offsetof(TYPE, MEMBER)  ((int)&(((TYPE *)0)->MEMBER))
/** 
 * container_of - cast a member of a structure out to the containing structure 
 * @ptr:        the pointer to the member. 
 * @type:       the type of the container struct this is embedded in. 
 * @member:     the name of the member within the struct. 
 * @ptr     :   指向成员的指针。
 * @type    :   需要返回的结构体实例类型。
 * @member  :   成员在结构实例内部的名称,如果为数组,需要指定下标。
 */  

#define container_of(ptr, type, member) ({                       \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);     \
        (type *)( (char *)__mptr - offsetof(type,member) );})

typedef struct 
{
    char a[20];
    int b;
    float c;
}mystruct_type;


int main(void)
{
    mystruct_type s1 = {"hello world !", 100, 12.83} ;

    printf("offsetof(s1,c) = %d, \n",(offsetof(mystruct_type,c)));

    printf("container_of(&s1.b, mystruct_type, b) = %p \n" ,container_of(&s1.b, mystruct_type, b));
    printf("container_of(&s1.b, mystruct_type, b) = %p \n" ,container_of(&s1.c, mystruct_type, c));
    printf("&s1 = %p \n", &s1);


    return 0;
}

按照上面的叙述,我们的最后三个printf输出结果应该是一致的,编译运行结果如下:

root@ubuntu:/mnt/hgfs/share/code/c_advance/containerof# ./a.out 
offsetof(s1,c) = 24, 
 container_of(&s1.b, mystruct_type, b) = 0xbfef53e0 
container_of(&s1.b, mystruct_type, b) = 0xbfef53e0 
&s1 = 0xbfef53e0 

果然运行结果是一致的。
注意:如果是数组,则要指定数组下标。

printf("container_of(&s1.a[3], mystruct_type, b) = %p \n" ,container_of(&s1.a[3], mystruct_type, a[3]));

插入补充一下:编译发现出现以下警告,是因为 \ 后边多了一个空格,去掉就OK了。

root@ubuntu:/mnt/hgfs/share/code/c_advance/containerof# gcc containerof.c 
containerof.c:14:66: warning: backslash and newline separated by space [enabled by default]
 #define container_of(ptr, type, member) ({                       \  
 ^
containerof.c:15:66: warning: backslash and newline separated by space [enabled by default]
         const typeof( ((type *)0)->member ) *__mptr = (ptr);     \  
 ^
root@ubuntu:/mnt/hgfs/share/code/c_advance/containerof# gcc containerof.c 

3、总结

ofsetof和container_of宏定义在 Linux kernel中用的比较多,也不是很难理解。关键是需要理解这样实现的原理是需要编译器帮忙计算这个结构体变量在整个结构体中的偏移量。理解了这点再理解这两个宏定义就比价简单了。

这两个宏在kernel中有什么作用呢? 在链表和驱动中经常被使用,而且很广泛。等后续分析到相关代码时再做实际介绍。

发布了44 篇原创文章 · 获赞 60 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/u014421520/article/details/77857148