ANSI C定义了sizeof关键字,用来获取一个变量和数据类型在内存中所占的存储字节数。GNU 扩展了一个关键字,typeof用来获取一个变量或表达式的类型。

int i;
typeof(i) j = 20;
typeof(int *) a;
int f();
typeof(f()) k;

在上面的代码中,因为变量i的类型为int,所以typeof(i)就等于int,typeof(i) j = 20就相当于int j = 20;typeof(int *) a;就相当于int * a;函数也是有类型的,函数的类型就是其返回值类型,所以typeof(f()) k;就相当于int k;。

typeof( typeof(int *)[5] ) a; //相当于int * a[5];
typeof( int x[5]) y; //相当于int y[5];

完美MAX(a,b)宏的诞生

在之前黑鸟的文章中,有提到过如何定义一个适用不同类型且无副作用的MAX宏。今天就带大家复习一下:

青铜级别:

#define MAX(x,y) ( (x) > (y)?(x):(y) )

一般我们在教科书上看到的简单的MAX宏定义如上。但他有两个致命缺陷,一是两个参数必须类型一致,二是参数不能带副作用,即自增或自减。原因大家自己思考,当然也可以查看我之前的文章。

白银级别:

#defineMAX(type,x,y)({ \
    type _x = x; \
    type _y = y; \
    _x > _y ? _x : _y; \
})

该方法通过添加一个type参数解决了不同类型变量比较的问题;通过定义临时变量_x和_y成功解决了参数的副作用问题。但他还不是最好的。

黄金级别:

#define MAX(x,y) ({\
typeof(x)_x=x;\
typeof(y)_y=y;\
_x>_y ? _x : _y;\
})

通过typeof直接获取宏的参数类型,这样我们就不必再单独将参数的类型传给宏了。到这里他已经基本可以在江湖中有自己一席之地了,但离号令天下还有一步之遥。

砖石级别:

#defineMAX(x,y)({\
    typeof(x) _x = x; \
    typeof(y) _y = y; \
    (void) ( &_x == &_y ); \
    _x>_y?_x:_y;\
})

该宏定义之所以称之为钻石级别,是因为他多了一句(void)(&_x==&_y); 该语句看起来貌似一句废话,但其实用的很巧妙。它主要是用来检测宏的两个参数的数据类型是否相同。如果不相同,编译器会给一个警告信息提醒开发人员。

waring:comparison of distinct pointer types lacks a cast
  • 让我们分析一下他是如何实现判断他的两个参数的数据类型是否一致。

从字面意思来看,他用来判断两个变量的地址是否相等。可能有人会说,两个变量的地址怎么可能相等呢?但妙就妙在这个地方!当该语句还未执行到判断两个变量的地址是否相等的时候,编译器首先要检查两个变量的数据类型是否相同。如果两个变量的数据类型不相同的话,那么编译器会有警告信息。当然我们也就可以从中获益。而如果两个变量的数据类型相等的话,那么该语句在整个宏中也不起任何作用,同样我们也没任何损失,笑问这种无本生意为何不做呢?

所谓是驴子是马拉出来溜溜!上面我们讲了这么多,但sizeof关键字到底有什么作用呢?下面通过解剖内核宏container_of来为大家展示sizeof关键字的强大作用和内核设计者的巨大脑洞。

解剖内核第一宏container_of

首先让我们来膜拜一下天下第一宏的风采:

#define offsetof( TYPE, MEMBER ) ( (size_t)&((TYPE *)0)->MEMBER )
#define container_of(ptr, type,member) ({ \
    const typeof( ((type *)0)->member ) _mptr = (ptr); \
    (type  *)( (char *)_mptr - offsetof(type, member) );
})

它的主要作用就是:根据结构体某一成员的地址,获取这个结构体的首地址。根据宏定义我们可以看到,这个宏有三个参数,他们分别是:

  • type:结构体类型
  • member :结构体内的成员
  • ptr: 结构体内成员member的地址

也就是说,只要我们知道了一个结构体的类型,结构体内某一成员的地址,就可以直接获得这个结构体的首地址。这个宏返回的就是这个结构体的首地址。

container_of宏实现分析

作为一名Linux内核驱动开发者,除了要面对各种手册、底层寄存器,有时候还要应付底层造轮子的事情。为了系统的稳定和性能,有时候我们不得不深入底层,死磕某个模块进行分析和优化。底层的工作虽然很有挑战性,但有时候也是很枯燥的。不像应用开发那样有意思,所以为了提高对工作的兴趣,大家表面上虽然不说自己牛叉,但内心深处一定要建立起自己的职位优越感。人不可有傲气,但不能无傲骨。我们可不像开发,知道api接口、读读文档、完成功能就ok了。作为一名底层开发者要时刻记住:要和寄存器、内存、硬件电路等各种底层群众打成一片。从群众中来,到群众中去,急群众之所急,想群众之所想。这样才能构建一个稳定和谐的嵌入式社会。

  • 首先来看offsetof宏
#defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER)

这个宏有两个参数,一个是结构体类型TYPE,一个是结构体的成员MEMBER。它使用的技巧就是:将0强制转换为一个指向TYPE类型的结构体常量指针。因为常量指针为0,即可以看做结构体首地址为0,所以结构体每个成员变量的地址即为该成员相对于结构体首地址的偏移。最后通过强制类型转换size_t,将成员地址值转换为整数,取得其在整个结构体中的偏移。

  • 再来看container_of宏
consttypeof(((type*)0)->member)_mptr=(ptr);

因为结构体的成员数据类型可以是任意的数据类型,所以为了让这个宏兼容各种数据类型,我们定义了一个临时指针变量_mptr,该变量用来存储结构体成员member的地址ptr。那如何获取ptr指针类型的呢?就是通过上面的宏语句。

(type*)((char*)_mptr-offsetof(type,member));

最后通过该成员变量地址_mptr减去该成员在结构体中的偏移,就得到了结构体的首地址。当然因为返回的是结构体的首地址所以数据类型还必须强制转换一下。

小思

好了到这里我们对天下第一宏的分析也就接近尾声了。那么可以看出任何一个复杂的东西,我们都可以把它分解。运用所学的基础知识一层一层,一点以一点去剖析,先去降维分析,然后再进行综合。那么这种能力就是你的核心竞争力,也是你超越其他工程师脱颖而出的机会!