GCC 編譯器對 C 語言標準進行了一些列擴展,接下來會逐個介紹GNU C 的擴展語法,可能有很多我們習以為常的用法,亦或是大家不常用的操作。
本文介紹以下兩個擴展語法:
指定初始化
語句表達式的應用
1. 指定初始化
在 C 語言標準中,當我們定義并初始化一個數組時,常用方法如下:
?
?
int?a[10]?=?{0,1,2,3,4,5,6,7,8};
?
?
按照這種固定的順序,我們可以依次的對a[0] 到 a[8] 賦值。a[9] 沒有賦值,編譯器會自動設置為0.
當數組畢竟小時,使用這種初始化方式會比較方便,但是當數組比較大,并且數組里的非零元素不連續,在按照固定順序賦值就很麻煩了。
C99 標準改進了數組初始化方式,支持指定元素初始化,不在按照固定的順序初始化。
?
?
int?b[100]?=?{[10]?=?1,?[30]?=?2};
?
?
通過數組元素索引,我們可以直接給指定的數組元素賦值,除了數組,一個結構體變量的初始化,也可以通過指定某個結構體成員直接賦值。
在早期 C 語言標準不支持指定初始化時,GCC編譯器就已經支持指定初始化了,因此這個特性也被看做GCC的一個擴展特性。
1.1 指定初始化數組元素
在 GNU C 中,通過數組元素索引,我們可以直接給指定的幾個元素賦值。這里注意,各個賦值之間用逗號隔開,而非分號
?
?
int?b[100]?=?{[10]?=?1,?[30]?=?2};
?
?
如果想給數組中一個索引范圍的元素初始化,可以采用?...
?
?
#include?int?main(void) { ????int?b[100]?=?{?[10?...?30]?=?1,?[50?...?60]?=?2?}; ????for?(int?i?=?0;?i?100;?i++)?{ ????????if?(i?%?10?==?0)?{ ???????????printf(" "); ????????}??? ????????printf("%d?",?b[i]); ????}??? ????printf(" "); ???? ????return?0; }
?
?
GNU C 支持 使用?...?表示范圍擴展,在這里使用[10 ... 30] 表示一個范圍,相當于給b[10] 到 b[30] 之間的20個數賦值。
...?不僅可以用在數組初始化中,也可以用在switch-case 語句中。
?
?
int?main(void) { ????int?i?=?4; ????switch(i) ????{ ????case?1: ????????printf("1 "?); ????????break; ???? ????case?2?...?8: ????????printf("%d "?,?i); ????????break; ???? ????default: ????????printf("default "); ????????break; ????} ????return?0; }
?
?
這里需要注意?...?兩邊的數據之間要有空格,否則,會報編譯錯誤。
1.2 指定初始化結構體成員
和數組類似,在 C 語言標準中,初始化結構體變量也要按照固定順序,但是在 GNU C 中我們可以結構體域來指定初始化某個成員。
?
?
struct?student?{ ????char?name[20]; ????int?age; }; int?main(void) { ????struct?student?stu1?=?{"s1",?20}; ????printf("%s:%d ",?stu1.name,?stu1.age); ???? ????struct?student?stu2?=?{ ????????.name?=?"s2"; ????????.age?=?28; ????} ???? ??????printf("%s:%d ",?stu2.name,?stu2.age); ?????? ??????return?0; }
?
?
1.3 Linux 內核中的指定初始化
在Linux ?驅動中,大量使用 GNU C的這種指定初始化方式,通過結構體成員來初始化結構體變量:
?
?
static?const?struct?file_operations?ci_port_test_fops?=?{ ?.open??=?ci_port_test_open, ?.write??=?ci_port_test_write, ?.read??=?seq_read, ?.llseek??=?seq_lseek, ?.release?=?single_release, };
?
?
在驅動程序中,我們經常使用file_operations 這個結構體來注冊我們開發的驅動,然后系統會以回調的方式。
結構體file_operations 里定義了很多結構體成員,而在這個驅動中,我們只是初始化了部分成員變量。通過訪問結構體的各個成員域來指定初始化,當結構體成員很多時優勢就體現出來了,初始化會更加方便。
1.4 指定初始化的好處
指定初始化不僅使用靈活,還有一個好處就是代碼易于維護。特別是在Linux 內核這種大型項目中,有幾萬個文件,大量使用了這種指定初始化。
如果采用標準C語言按照固定順序初始化賦值,一旦增加、刪除一個成員,大量的文件都有重新調整初始化順序,牽一發而動全身。
2. 語句表達式
2.1 語句表達式
GNU C 對 C 語言標準作了擴展,允許在一個表達式里內嵌語句,允許在表達式內部使用局部變量、for 循環 和 goto 跳轉語句。這種類型的表達式,我們稱之為語句表達式:
?
?
(?{?表達式1;?表達式2;?表達式3;?}?)
?
?
語句表達式最外面使用 () 括起來,里面使用 {} 包起來的是代碼塊,代碼塊里允許內嵌各種語句。
語句的格式可以是一般表達式,也可以是循環和跳轉語句。
和一般表達式一樣,語句表達式也有自己的值。語句表達式的值為內嵌語句中最后一個表達式的值。
?
?
int?main?(void)?{ ????int?sum?=?0; ????sum?=?(? ????????{????????????????????????????????????????? ????????????int?s?=?0,?i?=?0;? ????????????for?(i?=?0;?i?10;?i++)? ????????????????s?=?s?+?i;? ????????????s;?? ???????});? ????printf("sum?=?%d? ",?sum); ????return?0; }
?
?
編譯:
?
?
gcc?-std=gnu89 gnu1.c?/?gcc?-std=gnu99 gnu1.c
?
?
在上面的程序中,通過語句表達式計算1到10的累加,因為語句表達式的值等于最后一個表達式的值,所以在 for 循環后面要添加一個s。如果你將這個值改成 s=100,會發現sum結果變成100了。
在語句表達式中使用跳轉。
?
?
int?main?(void)?{ ????int?sum?=?0; ????sum?=?(? ????????{????????????????????????????????????????? ????????????int?s?=?0,?i?=?0;? ????????????for?(i?=?0;?i?10;?i++)? ????????????????s?=?s?+?i;? ????????????goto?here; ????????????s;?? ???????});? ????printf("sum?=?%d? ",?sum); here: ????printf("here: "); ????printf("sum?=?%d? ",?sum); ????return?0; }
?
?
2.2 在宏定義中使用語句表達式
語句表達式的主要用途在于定義功能復雜的宏。使用語句表達式來定義宏,不僅可以實現復雜的功能,還能避免宏定義帶來的歧義和漏洞。
下面就以一個例子,讓我們領略宏定義的殺傷力。
題目:定義一個宏,求兩個數的最大值。
合格:
對于學過C語言的同學,寫出這個宏基本上不是什么難事,使用條件運算符即可完成。
?
?
#include?#define?MAX(x,?y)????x?>?y???x?:y int?main() { ????printf("max?=?%d ",?MAX(1,2)); ????printf("max?=?%d ",?MAX(2,1)); ????printf("max?=?%d ",?MAX(2,2)); ????printf("max?=?%d ",?MAX(1!=1,1!=2)); ???? ????return?0; }
?
?
運行結果如下,發現最后一個結果與預期不符合
?
?
max?=?2 max?=?2 max?=?2 max?=?0?
?
?
我們使用預處理命令展開宏
?
?
gcc?-E gnu1.c?-o gnu1.i
?
?
因為 ?> 號的優先級(6)大于 != 號的優先級,所以展開后,結果就和預期不一樣了。
為了避免這種錯誤,我們可以給宏參數加一個小括號,防止展開后的運算符發生變化。
?
?
#define?MAX(x,?y)????(x)?>?(y)???(x)?:(y)
?
?
中等:
上面的宏只能算合格,還是存在漏洞:
?
?
#include?#define?MAX(x,?y)????(x)?>?(y)???(x)?:(y) int?main() { ???printf("max?=?%d ",?3?+?MAX(1,2));? ??? ???return?0; }
?
?
預期結果應該是5,結果是1.
預處理展開如下:
優先級順序:+?大于?>?號 所以表達式變成了
?
?
4?>?2???1:2
?
?
故對此宏進行改進:
?
?
#define?MAX(x,?y)?(?(x)?>?(y)???(x)?:?(y)?)
?
?
使用小括號括起來,就避免了當一個表達式同時含有宏定義和其他高優先級運算符時破壞整個表達式的運算順序。
良好:
上面的宏,雖然解決了運算符優先級問題,然任然存在一些漏洞。
定義兩個變量i和j,然后比較兩個變量的大小,并做自增運算。實際運行結果發現max=7
?
?
#include?#define?MAX(x,?y)????(?(x)?>?(y)???(x)?:(y)?) int?main() { ????int?i?=?2; ????int?j?=?6; ???? ????printf("max?=?%d ",?MAX(i++,j++)); ???? ????return?0; }
?
?
預處理展開后表達式如下:
i 和 j 在展開后做了兩次自增運算,導致打印 max的值為7。
當然,在C語言編程規范里,使用宏時一般是不允許參數變化的。但是萬一碰到這種情況,又該如何處理呢?
這個時候,語句表達式就需要上場了,在語句表達式中定義兩個臨時變量,分別來暫時存儲 i 和 j 的值,然后用臨時變量進行比較。
?
?
#include?#define?MAX(x,?y)????({? ????int _x?=?x;? ????int _y?=?y;? ????_x?>?_y???_x?:?_y;? ????}) int?main() { ????int?i?=?2; ????int?j?=?6; ????printf("max?=?%d ",?MAX(i++,j++)); ???? ????return?0; }
?
?
預處理展開:
優秀:
在上面定義的宏中,我們定義了兩個int型變量,只能比較整型數據。如果希望比較其他數據類型呢?
?
?
#include?#define?MAX(type,?x,?y)????({? ????type _x?=?x;? ????type _y?=?y;? ????_x?>?_y???_x?:?_y;? }) ???? int?main() { ????int?i?=?2; ????int?j?=?6; ????printf("max?=?%d ",?MAX(int,?i++,j++)); ????printf("max?=?%f ",?MAX(float,?3.14,3.15)); ???? ????return?0; }
?
?
很容易想到通過一個參數,將數據類型傳進去。
進一步修改:
我們只想保留兩個參數。
?
?
#include?#define?MAX(x,?y)????({? ????typeof(x)?_x?=?(x);? ????typeof(y)?_y?=?(y);? ????(void)?(&_x?==?&_y);? ????_x?>?_y???_x?:?_y;? }) int?main() { ????int?i?=?2; ????int?j?=?6; ???? ????printf("max?=?%d ",?MAX(i++,j++)); ????printf("max?=?%f ",?MAX(3.14,3.15)); ????return?0; }
?
?
GNU C 使用關鍵字 typeof 來獲取宏參數的數據類型。比較難以理解的就是第三句:(void) (&_x == &_y);
這句話看起來多余,實際上有兩個作用:
對于不同類型的指針比較,編譯器會發生一個警告,提示兩個數據類似不同;
當比較結果沒有用到時,有些編譯器可能會給一個警告,加上(void) 后 可以消除警告。
3. 總結
本文主要介紹了GNU C 的擴展:指定初始化和語句表達式的使用,重點介紹了語句表達式在宏中的使用。
事實上 Linux 內核大量使用了GNU C 的擴展語法,特別是語語句表達式在宏中的使用,了解GNU的擴展,有助于我們對C語言的認識更加清晰。
編輯:黃飛
?
評論
查看更多