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