Linux內(nèi)核可謂是集C語言大成者,從中我們可以學(xué)到非常多的技巧,本文來學(xué)習(xí)一下宏技巧,文章有點(diǎn)長,但耐心看完后C語言level直接飆升。
1.用do{}while(0)把宏包起來
#define init_hashtable_nodes(p, b) do {
int _i;
hash_init((p)- >htable##b);
...略去
} while (0)
Linux中常見如上定義宏的形式,我們都知道do{}while(0)只執(zhí)行一次,那么這個有什么意義呢?
我們寫一個更簡單的宏,來看看
#define fun(x) fun1(x);fun2(x);
則在這樣的語句中:
if(a)
fun(a);
被展開為
if(a)
fun1(x);fun2(x);;
fun2(x)將不會執(zhí)行!有同學(xué)會想,加個花括號
#define fun(x) {fun1(x);fun2(x);}
則在這樣的語句中
if (a)
fun(a);
else
fun3(a);
被展開為
if (a)
{fun1(x);fun2(x);};
else
fun3(a);
注意}后還有個;這將會 出現(xiàn)語法錯誤 。
但是假如我們寫成
#define fun(x) do{fun1(x);fun2(x);}while(0)
則完美避免上述問題!
2.獲取數(shù)組元素個數(shù)
寫一個獲取數(shù)組中元素個數(shù)的宏怎么寫?顯然用sizeof
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(*arr))
可以用,但****這樣是存在問題的 ,先看個例子
#include< stdio.h >
int a[3] = {1,3,5};
int fun(int c[])
{
printf("fun1 a= %dn",sizeof(c));
}
int main(void)
{
printf("a= %dn",sizeof(a));
fun(a);
return 0;
}
輸出:
a = 12;
b = 8;//32位電腦為4
為什么?因?yàn)閿?shù)組名和指針不是完全一樣的,函數(shù)參數(shù)中的數(shù)組名在函數(shù)內(nèi)部會降為指針!sizeof(a),在函數(shù)中實(shí)際上變成了sizeof(int *)。
上面的宏存在的問題也就清楚了**,這是一個非常重大,且容易忽略的bug!
讓我們看看,內(nèi)核中怎么寫:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
GNU C支持0長數(shù)組,在某些編譯器下可能會出錯。(不過不是因?yàn)檫@個來避開上面的問題)
sizeof(arr) / sizeof((arr)[0]很好理解數(shù)組大小除去元素類型大小即是元素個數(shù),真正的精髓在于后面__must_be_array(arr)宏
#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))
先看內(nèi)部的__same_type,它也是個宏
# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
__builtin_types_compatible_p 是gcc內(nèi)聯(lián)函數(shù),在內(nèi)核源碼中找不到定義也無需包含頭文件,在代碼中也可以直接使用這個函數(shù)。(只要是用gcc編譯器來編譯即可使用, 不用管這個, 只需知道:
當(dāng) a 和 b 是同一種數(shù)據(jù)類型時(shí),此函數(shù)返回 1。
當(dāng) a 和 b 是不同的數(shù)據(jù)類型時(shí),此函數(shù)返回 0。
再看外部的( 精髓來了 )
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
上來就是個小技巧: !!(e)是將e轉(zhuǎn)換為0或1,加個-號即將e轉(zhuǎn)換為0或-1。
再用到了位域:
有些信息在存儲時(shí),并不需要占用一個完整的字節(jié), 而只需占幾個或一個二進(jìn)制位。例如在存放一個開關(guān)量時(shí),只有0和1 兩種狀態(tài),用一位二進(jìn)位即可。這時(shí)候可以用位域
struct struct_a{
char a:3;
char b:3;
char c;
};
a占用3位,b占用3位,如上結(jié)構(gòu)體只占用2字節(jié),位域可以為無位域名,這時(shí)它只用來作填充或調(diào)整位置,不能使用,如:
struct struct_a{
char a:3;
char :3;
char c;
};
當(dāng)位數(shù)為負(fù)數(shù)時(shí)編譯無法通過!
當(dāng)a為數(shù)組時(shí),__same_type((a), &(a)[0]),&(a)[0]是個指針,兩者類型不同,返回0,即e為0,-!!(e)為0,sizeof(struct { int:0; })為0,編譯通過且不影響最終值。
當(dāng)a為指針時(shí),__same_type((a), &(a)[0]),兩者類型相同,返回1,即e為1,-!!(e)為-1,無法編譯。
3.求兩個數(shù)中最大值的宏MAX
思考這個問題,你會怎么寫
3.1一般的同學(xué):
#define MAX(a,b) a > b ? a : b
存在問題,例子如下:
#include< stdio.h >
#define MAX(x,y) x > y ? x: y
int main(void)
{
int i = 14;
int j = 3;
printf ("i&0b101 = %dn",i&0b101);
printf ("j&0b101 = %dn",j&0b101);
printf("max=%dn",MAX(i&0b101,j&0b101));
return 0;
}
輸出:
i&0b101 = 4
j&0b101 = 1
max=1
明顯不對,因?yàn)?運(yùn)算符優(yōu)先級大于&,所以會先進(jìn)行比較再進(jìn)行按位與。
3.2稍好的同學(xué):
#define MAX(a,b) (a) > (b) ? (a) : (b)
存在問題,例子如下:
#define MAX(x,y) (x) > (y) ? (x) : (y)
int main(void)
{
printf("max=%d",3 + MAX(1,2));
return 0;
}
輸出:
max = 1
同樣是優(yōu)先級問題+優(yōu)先級大于>。
附優(yōu)先級表:同一優(yōu)先級的運(yùn)算符,運(yùn)算次序由結(jié)合方向所決定。
3.3良好的同學(xué)
#define MAX(a,b) ((a) > (b) ? (a) : (b))
避免了前兩個出現(xiàn)的問題,但同樣還有問題存在:
#include< stdio.h >
#define MAX(x,y) ((x) > (y) ? (x): (y))
int main(void)
{
int i = 2;
int j = 3;
printf("max=%dn",MAX(i++,j++));
printf("i=%dn",i);
printf("j=%dn",j);
return 0;
}
期望結(jié)果:
max=3,i=3,j=4
實(shí)際結(jié)果
max=4,i=3,j=5
盡管用括號避免了優(yōu)先級問題,但這個例子中的j++實(shí)際上運(yùn)行了兩次。
3.4Linux內(nèi)核中的寫法
#define MAX(x, y) ({
typeof(x) _max1 = (x);
typeof(y) _max2 = (y);
(void) (&_max1 == &_max2);
_max1 > _max2 ? _max1 : _max2; })
下面進(jìn)行詳解。
3.4.1.GNU C中的語句表達(dá)式
表達(dá)式就是由一系列操作符和操作數(shù)構(gòu)成的式子。 例如三面三個表達(dá)式
a+b
i=a*2
a++
表達(dá)式加上一個分號就構(gòu)成了 語句 ,例如,下面三條語句:
a+b;
i=a*2;
a++;
A compound statement enclosed in parentheses may appear as an expression in GNU C.
——《Using the GNU Compiler Collection》6.1 Statements and Declarations in Expressions
GNU C允許在表達(dá)式中有復(fù)合語句,稱為語句表達(dá)式:
({表達(dá)式1;表達(dá)式2;表達(dá)式3;...})
語句表達(dá)式內(nèi)部可以有局部變量,語句表達(dá)式的值為內(nèi)部最后一個表達(dá)式的值。
例子:
int main()
{
int y;
y = ({ int a =3; int b = 4;a+b;});
printf("y = %dn",y);
return 0;
}
輸出:y = 7。
這個擴(kuò)展使得宏構(gòu)造更加安全可靠,我們可以寫出這樣的程序:
#define max(x, y) ({
int _max1 = (x);
int _max2 = (y);
_max1 > _max2 ? _max1 : _max2; })
int main(void)
{
int i = 2;
int j = 3;
printf("max=%dn",max(i++,j++));
printf("i=%dn",i);
printf("j=%dn",j);
return 0;
}
但這個宏還有個缺點(diǎn),只能比較int型變量,改進(jìn)一下:
#define max(type,x, y) ({
type _max1 = (x);
type _max2 = (y);
_max1 > _max2 ? _max1 : _max2; })
但這需要傳入type,還不夠好。
3.4.2 typeof關(guān)鍵字
GNU C 擴(kuò)展了一個關(guān)鍵字 typeof,用來獲取一個變量或表達(dá)式的類型。
例子:
int a;
typeof(a) b = 1;
typeof(int *) a;
int f();
typeof(f()) i;
于是就有了
#define max(x, y) ({
typeof(x) _max1 = (x);
typeof(y) _max2 = (y);
_max1 > _max2 ? _max1 : _max2; })
3.4.3真正的精髓
對比一下,內(nèi)核的寫法:
#define max(x, y) ({
typeof(x) _max1 = (x);
typeof(y) _max2 = (y);
(void) (&_max1 == &_max2);
_max1 > _max2 ? _max1 : _max2; })
發(fā)現(xiàn)比我們的還多了一句
(void) (&_max1 == &_max2);
這才是真正的精髓,對于不同類型的指針比較,編譯器會給一個警告:
warning:comparison of distinct pointer types lacks a cast
提示兩種數(shù)據(jù)類型不同。
至于加void是因?yàn)楫?dāng)兩個值比較,比較的結(jié)果沒有用到,有些編譯器可能會給出一個警告,加(void)后,就可以消除這個警告。
4.通過成員獲取結(jié)構(gòu)體地址的宏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));
})
4.1作用
我們傳給某個函數(shù)的參數(shù)是某個結(jié)構(gòu)體的成員,但是在函數(shù)中要用到此結(jié)構(gòu)體的其它成員變量,這時(shí)就需要使用這個宏:container_of(ptr, type, member)
ptr為已知結(jié)構(gòu)體成員的指針,type為結(jié)構(gòu)體名字,member為已知成員名字,例子:
struct struct_a{
int a;
int b;
};
int fun1 (int *pa)
{
struct struct_a *ps_a;
ps_a = container_of(pa,struct struct_a,a);
ps_a- >b = 8;
}
int main(void)
{
float f = 10;
struct struct_a s_a ={2,3};
fun1(&s_a.a);
printf("s_a.b = %dn",s_a.b);
return 0;
}
輸出:s_a.b=8。
本例子中通過struct_a結(jié)構(gòu)體中的a成員地址獲取到了結(jié)構(gòu)體地址,進(jìn)而對結(jié)構(gòu)體中的另一成員b進(jìn)行了賦值。
4.2詳解
首先來看:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)- >MEMBER)
這個是獲取在結(jié)構(gòu)體TYPE中,MEMBER成員的偏移位置。
定義一個結(jié)構(gòu)體變量時(shí),編譯器會按照結(jié)構(gòu)體中各個成員的順序,在內(nèi)存中分配一片連續(xù)的空間來存儲。例子:
#include< stdio.h >
struct struct_a{
int a;
int b;
int c;
};
int main(void)
{
struct struct_a s_a ={2,3,6};
printf("s_a addr = %pn",&s_a);
printf("s_a.a addr = %pn",&s_a.a);
printf("s_a.b addr = %pn",&s_a.b);
printf("s_a.c addr = %pn",&s_a.c);
return 0;
}
輸出
s_a addr = 0x7fff2357896c
s_a.a addr = 0x7fff2357896c
s_a.b addr = 0x7fff23578970
s_a.c addr = 0x7fff23578974
結(jié)構(gòu)體的地址也就是第一個成員的地址,每一個成員的地址可以看作是對首地址的 偏移 ,上面例子中,a就是首地址偏移0,b就是首地址偏移4字節(jié),c就是首地址偏移8字節(jié)。
我們知道C語言中指針的內(nèi)容其實(shí)就是地址,我們也可以把某個地址強(qiáng)制轉(zhuǎn)換為某種類型的指針,(TYPE *)0)即將地址0,通過強(qiáng)制類型轉(zhuǎn)換,轉(zhuǎn)換為一個指向結(jié)構(gòu)體類型為 TYPE的常量指針。
&((TYPE *)0)->MEMBER自然就是MEMBER成員對首地址的偏移量了。
而(size_t)是內(nèi)核定義的數(shù)據(jù)類型,在32位機(jī)上就是unsigned int,64位就是unsiged long int,就是強(qiáng)制轉(zhuǎn)換為無符號整型數(shù)。
再來看:
#define container_of(ptr, type, member) ({
const typeof(((type *)0)- >member) *__mptr = (ptr);
(type *)((char *)__mptr - offsetof(type, member));
})
第一句(其實(shí)這句才是精華)
const typeof(((type *)0)- >member) *__mptr = (ptr);
typeof在前面講過了,獲取類型,這句作用是利用賦值來確保你傳入的ptr指針和member成員是同一類型,不然就會出現(xiàn)警告。
第二句
(type *)((char *)__mptr - offsetof(type, member));
有了前面的講解,應(yīng)該就很容易理解了,成員的地址減去偏移不就是首地址嗎,為什么要加個(char *)強(qiáng)制類型轉(zhuǎn)換?
因?yàn)閛ffsetof(type, member)的結(jié)果是偏移的字節(jié)數(shù),而指針運(yùn)算,(char *)-1是減去一個字節(jié),(int *)-1就是減去四個字節(jié)了。
最外面的 (type *),即把這個值強(qiáng)制轉(zhuǎn)換為結(jié)構(gòu)體指針。
5.#與變參宏
5.1#和##
#運(yùn)算符 ,可以把宏參數(shù)轉(zhuǎn)換為字符串,例子
#include < stdio.h >
#define PSQR(x) printf("The square of " #x " is %d.n",((x)*(x)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}
輸出:
The square of y is 25.
The square of 2 + 4 is 36.
##運(yùn)算符 ,可以把兩個參數(shù)組合成一個。例子:
#include < stdio.h >
#define PRINT_XN(n) printf("x" #n " = %dn", x ## n);
int main(void)
{
int x1 = 2;
int x2 = 3;
PRINT_XN(1); // becomes printf("x1 = %dn", x1);
PRINT_XN(2); // becomes printf("x2 = %dn", x2);
return 0;
}
該程序的輸出如下:
x1 = 2
x2 = 3
5.2變參宏
我們都知道printf接受可變參數(shù),C99后宏定義也可以使用可變參數(shù)。C99 標(biāo)準(zhǔn)新增加的一個 VA_ARGS 預(yù)定義標(biāo)識符來表示變參列表,例子:
#define DEBUG(...) printf(__VA_ARGS__)
int main(void)
{
DEBUG("Hello %sn","World!");
return 0;
}
但是這個在使用時(shí),可能還有點(diǎn)問題比如這種寫法:
#define DEBUG(fmt,...) printf(fmt,__VA_ARGS__)
int main(void)
{
DEBUG("Hello World!");
return 0;
}
展開后
printf("Hello World!",);
多了個逗號,編譯無法通過,這時(shí),只要在標(biāo)識符 VA_ARGS 前面加上宏連接符 ##,當(dāng)變參列表非空時(shí),## 的作用是連接 fmt,和變參列表宏正常使用;當(dāng)變參列表為空時(shí),## 會將固定參數(shù) fmt 后面的逗號刪除掉,這樣宏也就可以正常使用了,即改成這樣:
#define DEBUG(fmt,...) printf(fmt,##__VA_ARGS__)
除了這些,其實(shí)Linux內(nèi)核中還有很多宏和函數(shù)寫得非常精妙。Linux內(nèi)核越看越有味道,看內(nèi)核源碼,很多時(shí)候都會不明所以,但看明白后又醍醐灌頂,又感慨人外有人!
評論
查看更多