语句表达式的定义
GNU C 对 C 标准作了扩展,允许在一个表达式里内嵌语句,允许在表达式内部使用局部变量、for 循环和 goto 跳转语句。这样的表达式,我们称之为语句表达式。语句表达式的格式如下:
({ 表达式1; 表达式2; 表达式3; })
语句表达式最外面使用小括号()括起来,里面一对大括号{}包起来的是代码块,代码块里允许内嵌各种语句。语句的格式可以是 “表达式;”这种一般格式的语句,也可以是循环、跳转等语句。
跟一般表达式一样,语句表达式也有自己的值。语句表达式的值为内嵌语句中最后一个表达式的值。
语句表达式的亮点在于定义复杂功能的宏。使用语句表达式来定义宏,不仅可以实现复杂的功能,而且还能避免宏定义带来的歧义和漏洞。下面就以一个宏定义例子,让我们来见识见识语句表达式在宏定义中的强悍杀伤力!
这里再次分析总结gitbook中的两个宏,一个是max/min宏,一个是内核第一宏container_of。
max/min宏
内核中的样子:
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
#define max(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
这里的max宏可以让我们学会语句表达式,typeof关键字;基础方面可以巩固运算符优先级。
这个宏是怎么得到的呢?
我们来写一个宏,用来比较两个变量的大小,我一定会这么写:
#define MAX(x,y) x>y?x:Y
那么我们来比较一下4!=4
和2!=3
,结果是错误的,原因是运算符优先级出了问题。那么我们来解决,使用括号是最简单的方法:
#define MAX(x,y) (x)>(y)?(x):(y)
再来运行一下printf("max=%d\n",MAX(2++,3++));
,输出的会是4,但我们只想要比较2和3的值,这里是因为自增自减运算符导致的问题,那么怎么解决呢?和交换两个数字的想法一样,通过一个中转值来存放,就可以隔离影响了
#define MAX(x,y) ({ \
int _x = x; \
int _y = y; \
_x > _y ? _x : _y; \
})
这里就有一些内核代码中的味道了,注意一个细节,这里的第四行没有括号了,为什么?这里就是因为语句表达式了,不存在上面的影响了。这里我们回顾一下代码,再看看目前这个宏的第二三行,是int
,也就是我们这个宏只能比较int类型的变量,而在内核中需要比较大小的变量有很多,那么我们来提高一下:
#define MAX(type,x,y) ({ \
type _x = x; \
type _y = y; \
_x > _y ? _x : _y; \
})
这个宏就可以用来比较任意类型的变量了,再来看一下代码,我们需要替换的变量有type
,x
,y
三个,如果有了typeof关键字,我们还可以减少一个:
#define MAX(x,y) ({ \
typeof(x) _x = x; \
typeof(y) _y = y; \
_x > _y ? _x : _y; \
})
接着来,如果我们使用了一次宏,是MAX(i,j)
,其中i是int类型,j是float类型,这样比较是可以的,但是在内核的设计过程之中,很有可能有些地方会出现问题,所以还需要改造:
#define MAX(x,y) ({ \
typeof(x) _x = x; \
typeof(y) _y = y; \
(void)(&_x == &_y); \
_x > _y ? _x : _y; \
})
这就是究极形态了,我们添加了第四行的代码,来看&_min1
,它的意思是取_min1
的地址,而&_min2
的意思是取_min2
的地址,我们也知道,这两个地址肯定不可能是一样的,那为什么还要这样写呢?这里就很巧妙了,当两个变量的类型不同时,对应的地址,也就是指针类型也不相同,比如一个是int类型,一个是char类型,那么指向他们的指针就是int *和char *,这两个指针在比较的时候,就比较的是类型了。如果比较的类型不一样,gcc会警告的。
我们来看这一系列改进,我相信内核设计人员也想把代码写成# define MAX(x,y) x > y? x : y
的样子,但是现实是残酷的,我们为了代码的健壮性,就必须这样一步一步来改进,所以,内核代码看起来很复杂,又很巧妙,是因为我们直接看到的是究极形态的代码,它是向现实妥协了多次以后的产物,也就是健壮性+GNU C。但是,内核设计者的初衷,或者说最初的想法和我们都是一样的。
-
在以后处理因为运算符而导致的问题的时候,使用括号是最方便的,内核就这么干了。
-
在写程序的时候,要巧用中转变量,虽然只是简单的存入另一个变量之中,但是代码的健壮性提高了很多。
-
两个地址在进行比较的时候,我们可以得知这两个指针类型是否一致。
container_of
gitchat中把container_of宏叫做内核第一宏。在list.h中就使用这个宏。
代码来了:
#define offsetof(TYPE, MEMBER) ((size_t) &((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.
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
这个宏的作用我们已经很清楚了,根据结构体中某一成员的地址,就可以获得这个结构体的首地址,再说的明白一点,假如你是内核设计人员,前面也说道了,我们已经对数据进行了多次封装,我们一定会遇到这种情况:传给某个函数的参数是某个结构体成员变量,但是我们在这个函数中还想使用这个结构体的其它成员变量,这个时候就需要想办法,于是才有了我们现在看到的这个内核第一宏。
它的三个参数是:
- ptr:此结构体内成员member的地址
- type:此结构体类型
- member:此结构体内的成员
我们直接看代码,这个宏的最后的值,就是最后一条语句,(type *)( (char *)__mptr - offsetof(type,member) );})
,这条语句也是这个宏的中心思想拿结构体成员的地址减去此成员的偏移
,这里也体现了指针做减法是很有意义的。成员的地址好说,我们直接传进来了,偏移是通过offsetof来实现的,来看看这个offsetof:将0强制类型转换成这个结构体的指针类型,然后访问这个成员,加上&得到它的偏移,返回。
再来看(char *)__mptr
,这个通过第四行代码可以很容易得出它是成员的地址,为什么要强制转换成char *
呢?转换成int *
不行吗?这里又可以学习一下C指针的基础知识,通过代码可以很容易知道有什么区别:我们的偏移是按照字节来算的,所以不能使用(int *),必须使用(char *)。在最后,再次强制类型转换成指向这个结构体的指针类型。
回过头来看第四行代码,const typeof( ((type *)0)->member ) *__mptr = (ptr);
,这里和max宏之中类似,使用了中转变量来存放,这里为什么要使用中转变量?max宏中是为了防止自增自减的影响(当然只是原因之一了),但我们在使用的时候总不至于发过来成员的地址再加一个++运算符吧。我们可以从const的用法来思考,const int * p //p可变,p指向的内容不可变
,所以,使用了const,我们就可以保证ptr指向的内容在这里只是可读的,这也许就是为什么使用中转变量的原因,为了防止我们通过指针改变了原有的成员的值,毕竟指针虽然强大,但也是很危险的,所以,这里的中转要配合const来使用。既然是中转,那么类型就必须要求一致了,所以我们要得到和这个成员一致的类型,就通过typeof来得到了,将0强制类型转换成这个这个结构体的指针类型,然后访问这个变量,(注意仔细看代码,这里的代码和offsetof非常类似)这里没有使用&,所以只是访问到变量了,没有得到偏移。
我们再来注意一个细节,就是offsetof里的size_t
,这个是什么,这里在敲代码的过程中偶然学到一个小技巧,就是这个size_t绝对是封装,就是C语言中那几种变量类型,我们可以typedef int size_t;
然后运行,gcc就会报错,并且会给你显示:以前已经定义过:typedef __SIZE_TYPE__ size_t
,并且会指定这个值在哪个文件,我们就可以知道它的真面目了。换句话说,gcc这么强大,我们当然可以把它当做一个学习工具来使用。
另外还可以通过sublime,可以很快找到它的真面目(3.10版本):
最后,为了更深入理解这些知识的使用方法,还是需要自己动手来敲代码的。