offsetof宏:

c标准库的stddef.h,只要包含这个.h,就可以使用这个宏
有关stddef.h,我们在第五章介绍C标准库的头文件时,我们提到过这个.h,不知道大家还有映像没有

contaner_of宏:

这个是Linux内核的kernel.h所提供的宏


其实完全可以自己定义并使用这两个宏,只要你理解了这两个宏的原理,定义这两个宏非常容易

一、访问结构体成员的原理

1. 结构体变量指针 与 第一个成员的指针

“结构体变量的指针”为结构体变量第一个字节的地址,恰好也是第一个成员的第一个字节的地址

如果地址的类型“结构体指针类型”,通过这个指针访问的就是整个结构体变量的空间,
如果为第一个成员的指针类型,通过这个指针访问的就是第一个成员的空间

2. .和->访问成员的原理是一样的

不管是通过.->(*p).)方式来访问,最终都是“结构体变量指针+成员偏移量”得到成员指针(成员地址),
然后通过成员指针去访问里面的每个成员

之所以是相加而不是相减,是因为结构体变量的第一个成员都在低地址位置,后面的成员都在高地址位置

3. 通过成员来访问外部结构体变量

通常情况下,我们都是先得到外部结构体变量的指针,然后再去访问里面的成员,

但是反过来,如果我们得到了某个成员的指针之后,能不能通过成员指针计算得到“外部结构体变量”的指针?

答:当然可以!

结构体变量的指针 + 成员偏移 = 成员指针   (这个计算过程,由编译器自动计算完成)

反过来:

结构体变量的指针 = 成员指针 - 成员偏移   (这个需要由我们自己写的代码来计算)

offsetofcontaner_of宏就上面的计算公式的具体实现,通过offsetofcontaner_of宏,
就可以利用“成员指针”计算出“外部结构体变量”的指针

我们后面讲Linux内核链表时,内核链表的实现核心,就是通过成员指针来计算出外部结构体变量的指针


二、offsetof宏

offsetof(struct Test, c):只要将结构体类型和成员名给它,就能自动计算出成员的偏移量

1. 作用

用于计算成员指针与结构体变量指针间的偏移量,
每个成员具体的偏移量是多少,这个与结构体成员的对齐方式有关

2. 使用举例

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

struct Test
{
    char a;
    int b;
    short c;
}ts = {'a', 100, 10};

int main(void)
{
    int offset = 0;

    offset = (int)&(ts.c) - (int)&ts;
    printf("&ts.c - &ts= %d\n", offset);

    offset = offsetof(struct Test, c);
    printf("offset     = %d\n", offset);

    return 0;
}

通过打印结果可知,通过offsetof所得到的偏移,与(int)&(ts.c) - (int)&ts所得到的偏移是一样的,
所以使用offsetof来计算机成员的偏移是没有问题的

3. offsetof 宏的工作原理

完整写法,把0看作是结构体变量的地址

( ((int) & (((TYPE *)0)->MEMBER)) -  0  )
              成员地址             - 结构体变量地址  = 成员的偏移

具体工作原理

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

将例子中的offsetof(struct Test, c)进行宏替换后,就变为了

( ((int) & (  ((struct Test *)0)->c)  ) -  0 )

( (struct Test *)0 )

0地址强行转换为struct Test *类型的指针,然后使用0这个指针,
就能以“struct Test”的结构去访问0地址往后的sizeof(struct Test)大小的空间,
此时0是结构体变量的首字节地址

程序基于OS运行时,实际上0地址并没有对应合法空间,
我们这里只是在借助0地址来进行“模拟”计算,只要不对0地址进行解引用,就不会导致指针错误

编译器在编译(((int) & (((struct Test *)0)->c)) - 0)时,就会按照struct Test这个类型去模拟计算,
不涉及0地址的解引用,只要不接应就不会导致指针错误

(int) & ( ( (struct Test *)0 )->c )

得到成员C地址,然后强制换为int类型,之所以转为int,主要为了方便后续的算数运算,得到成员的偏移量

( (int) & (  ( (struct Test *)0 )->c)  ) - 0

成员c的地址 - 结构体变量地址(0)”,自然就得到了成员的偏移

我们既然已经理解了offsetof宏的工作原理,那么0实际上可以改为任何数字,比如:

(((int) & ((TYPE *)1011->MEMBER)) -  1011)

编译器在编译offsetof宏时,只是在使用结构体类型来模拟计算成员的偏移,
不涉及真实的解引用,不用担心指针错误的问题


三、contaner_of宏

contaner_of嵌套了offsetof

1. contaner_of 宏的作用

这个宏的作用就是,通过“成员指针” - “成员偏移”,计算得到外部结构体变量的指针

2. 例子

#define offsetof(TYPE, MEMBER)((int) & (((TYPE *)0)->MEMBER))
#define container_of(ptr, type, member)  (type *)((char *)ptr - offsetof(type, member))

struct Test
{
    char a;
    int b;
    short c;
}ts = {'a', 100, 10};

int main(void)
{
    printf("%d\n", &ts);
    printf("container_of(&ts.c, struct Test, c) = %d\n",  container_of(&ts.c, struct Test, c));

    return 0;
}

3. container_of 的原理

#define container_of(ptr, type, member)   (type *)((char *)ptr - offsetof(type, member))

container_of(ptr, type, member)的参数

  • (a)ptr:成员指针
  • (b)type:结构体类型
  • (c)member:成员名

通过宏替换来分析原理
对例子中container_of(&ts.c, struct Test, c)进行宏替换后,就得到了如下结果

(struct Test *) (   (char *)&ts.c - ( (int) & ( (struct Test *)0->c )   )
                      成员c的指针  -       成员c的偏移

得到结构体变量的地址后,再强制转为struct Test *,那么这个地址就一个真正的struct Test *的指针了


疑问:成员指针为什么要强转为char *

答:通过第四章节所讲的指针运算,假设是int *的话,“成员指针 - 成员偏移”时就变成了
成员指针 - 成员偏移”,显然是不对的,当然将char *写成void *也是可以的


4. Linux下的container_of宏

在前面就说过,container_of宏定义在了Linux内核的kernel.h中,
只是Linuxkernel.hcontainer_of宏,在形式上看起来会稍微复杂一些,当然原理都是一样

kernel.hcontainer_of的原型:

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

相比我们自己定义的container_of来说,多了一句

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

而且还被({ })包括起来了


typeof(((type *)0)->member)
先通过typeof关键字提取出成员的类型,如果成员类型为int,那么提取得到int这个类型

假设提取到的类型为int

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

可以被简化为:

const int *__mptr = (ptr)
  • 利用提取到的“成员类型”定义一个指针变量__mptr
  • 将“成员指针”从ptr转放到__mptr指针变量中,并修饰为const

疑问:为什么不直接使用ptr

答:直接使用ptr也是可以的,但是直接使用ptr存在风险,假如操作失误,
很有可能会通过ptr这个指针把成员的值给修改了

但是转到__mptr中后,由于__mptrconst修饰了,操作__mptr时是无法修改成员的,
自然就避免了这种风险,显然更加的安全

5. 分析Linux下的container_of宏

原型:

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

示例代码:

#define container_of(ptr, type, member)    (type *)((char *)ptr - offsetof(type, member))

struct People *p =  container_of(&ts.c, struct Test, c);

进行宏替换后:

struct People *p = ({
    const typeof( ((struct Test *)0)->c ) *__mptr = (&ts.c); 
    (struct Test *)( (char *)__mptr - offsetof(struct Test, c));
});

第二条语句的计算结果会赋给p,类似如下情况:

int a = ({
int aa=10; 
int b = aa+20; 
b+40; //最后一句话的相加结果会交给a,
      //最后一个不能包含类型,比如int c = b+40,int c是无法编译通过的
});

疑问:({...; ...; ...;})有什作用?

答:这个是gcc支持特有的一种语法,换一个gcc以外的其它编译器(windows c编译器),将无法识别
加上这个到底有什么作用,我们在第八章中会解释。

6. typeof 关键字

前面说过,typeof关键字的作用是用于提取变量、数据的类型,比如:

int a;
typeof(a) b = 10;   //等价于int b = 10

typeof(10.33) c = 20.5;  //等价于 double c = 20.5

printf("%d\n", b);
printf("%f\n", c);

不过typeof这个关键字是GNU标准扩展的关键字,在C标准中并不支持这个关键字,
当出现如下两种情况时,typeof将无法使用:

1)使用的不是gcc编译器

这个是GNU标准的扩展关键字,gccGNU推出的编译器,所以如果使用的不是这个GNUgcc编译器的话,
可能无法识别typeof

2)使用的是gcc编译器,但是如果指定编译标准时,指定为-std=c99或者c11C标准,将也无法识别,

因为C标准不支持,只有指定-std=gnu99或者gnu11gnu标准时才能识别,默认情况下gcc编译时,
都是按照gnu标准来编译的,如果我们在编译时特意的指定-std=c99选项的话,是无法编译通过的