Linux 中的经典宏 container_of(ptr, type, member)剖析

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

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

首先分析 offsetof宏

offsetof 用于计算 TYPE 结构体中 MENBER 成员的偏移位置

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

该宏最值得注意的就是 ((TYPE*)0)->MEMBER) 这个写法。这里是直接把0地址 强制转换为 TYPE* 指针,指向的空间为TYPE模子扣出来的空间,第一眼看,很多人都会奇怪,这么明晃晃的直接使用0地址,难道不会导致程序崩溃吗,很多人会以为 在0地址处有一个 TYPE类型的结构体,但是这是不可能的,0地址是给操作系统使用的。

所以 编译器到底是怎么解读这段代码的呢?
1 编译器在编译的时候就已经清楚的知道了结构体成员变量的偏移位置了。
2 编译器通过结构体成员变量首地址与偏移量定位成员变量

以上两点就是编译所做的事情,直接这样讲,有时候我自己都有点似懂非懂,直接看代码吧:

struct ST
{
    int i;     // 0
    int j;     // 4
    char c;    // 8
};

struct ST s = {0};
struct ST* pst = &s;

/*
以下取成员变量地址的操作的本质其实就是 根据结构体的首元素地址加上编译器知道的成员变量的偏移地址得到的。
*/
int* pi = &(pst->i);    //  0  pst首地址 + i成员的偏移量
int* pj = &(pst->j);    //  4 pst首地址 + j成员的偏移量
char* pc = &(pst->c);   //  8 pst首地址 + c成员的偏移量

这就是编译器所做的事情,只是做指针运算,并没有做内存访问!!!
所以,就算这里的 pst指针 指向的是0地址,也没什么大不了的,因为只是做指针运算,并不是访问内存。 而用NULL作为参数 可以很方便的直接得到 成员变量在结构体中偏移位置!

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

可以看成
(unsigned int) &((struct ST*)0 -> i)
即得到变量i 的偏移量。

实验:

#include <stdio.h

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

struct ST
{
    int i;     // 0
    int j;     // 4
    char c;    // 8
};

void func(struct ST* pst)
{
    int* pi = &(pst->i);    //  0
    int* pj = &(pst->j);    //  4
    char* pc = &(pst->c);   //  8

    printf("pst = %p\n", pst);
    printf("pi = %p\n", pi);
    printf("pj = %p\n", pj);
    printf("pc = %p\n", pc);
}

int main()
{
    struct ST s = {0};

    func(&s);
    
	//传入空指针
    func(NULL);

    printf("offset i: %d\n", offsetof(struct ST, i));
    printf("offset j: %d\n", offsetof(struct ST, j));
    printf("offset c: %d\n", offsetof(struct ST, c));

    return 0;
}

可以发现 传入空指针,程序并没有崩溃,原因就是 本质上,以上程序,编译器只是做了地址的指针运算,即加上偏移量,而并没有访问地址中的数据。

再分析 container_of

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

关于 ({}) 这个写法是什么意思?
在这里插入图片描述

将花括号{}中的代码块封装到圆括号中(),表示留下代码段的最后一个语句的值。代码段中的 a b 都是局部变量,这是与逗号表达式不同的地方。

关于 typeof
在这里插入图片描述

在这里插入图片描述
所以

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

可以这样分析:

1
#define container_of(ptr, type, member)

== (type*)((char*)__mptr - offsetof(type, member));

2
__mptr == ptr,表示member成员的真是地址

3
offsetof(type, member)); 表示 member成员在结构体type中的偏移量

4
做减法就可以得到 member成员所在结构体的首地址

可以简单理解为:

#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
#endif

但是 Linux 为什么在中间加上了

 const typeof(((type*)0)->member)* __mptr = (ptr);   \

原因在于,宏是预处理器处理的,不能做类型检查的,这行代码就是用于做类型检查

typeof(((type*)0)->member) 用于获取 成员变量 member的类型,即保证 container_of宏 参数1地址类型必须和 member类型一致,即保证传入的地址是member的地址,否则就是参数传入错误!!

所以这里也就是引入 GNU C 编译器扩展语法 ({}) 的原因了。这里为了增加代码的安全性,为了有一点点的类型安全检查,所以设计container_of 宏的时候 加上了 这条指针定义语句:

 const typeof(((type*)0)->member)* __mptr = (ptr);

而指针定义是不可能存在于逗号表达式中的,所以使用了 GNU C 编译器扩展语法 ({})

注意:
typeof 和 ({}) 只能用于 GNU C 编译器环境中。如果不是 GNU C 编译器。可以考虑使用改写版,即不做类型检查:

#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
#endif

实验:

#include <stdio.h>

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif

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

#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)(ptr) - offsetof(type, member)))
#endif


struct ST
{
    int i;     // 0
    int j;     // 4
    char c;    // 8
};

void method_1()
{
    int a = 0;
    int b = 0;

    int r = (
           a = 1,
           b = 2,
           a + b
                );

    printf("r = %d\n", r);
}

void method_2()
{
    int r = ( {
                  int a = 1;
                  int b = 2;

                  a + b;
              } );

    printf("r = %d\n", r);
}

void type_of()
{
    int i = 100;
    typeof(i) j = i;
    const typeof(j)* p = &j;

    printf("sizeof(j) = %d\n", sizeof(j));
    printf("j = %d\n", j);
    printf("*p = %d\n", *p);
}

int main()
{

    // method_1();
    // method_2();
    // type_of();

    struct ST s = {0};
    char* pc = &s.c;
    int e = 0;
    int* pe = &e;

    struct ST* pst = container_of(pc, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux老A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值