C 語言在 Linux 系統中的重要性自然是無與倫比、不可替代,所以我寫 Linux 江湖系列不可能不提 C 語言。C 語言是我的啟蒙語言,感謝 C 語言帶領我進入了程序世界。雖然現在不靠它吃飯,但是仍免不了經常和它打交道,特別是在 Linux 系統下。
Linux 系統中普遍使用的是 GNU-C,這里有一份Gnu-C語言手冊.pdf。The GNU C Reference Manual 的主頁在這里:http://www.gnu.org/software/gnu-c-manual/。C 語言的內核極其緊湊,該手冊總共只有 91 頁,去掉目錄、附錄和索引后就只有 70 頁。我一般一個多小時就可以將其從頭至尾復習一遍。我曾有過將其翻譯成中文的想法,后來還是放棄了。翻譯這種字斟句酌的事情還是讓別人來干吧。我只寫寫我自己的感悟。
感悟一:C 語言標准干不過 GNU 擴展
最近為了研究 X Window 的底層協議,開始嘗試使用 XCB 編程。當我打開 XCB 的頭文件的時候,我被大量的 __restrict__ 關鍵字驚呆了,好在有 GNU C 語言手冊為我答疑解惑。__restrict__ 又是一個 GNU 擴展的關鍵字,后面我會詳細講解該關鍵字的用途。其實 C 語言的 C99 標准中已經引入了 restrict 關鍵字,沒有前后的下划線,但是在大量的開源代碼中,使用最普遍的還是 GNU 的擴展,而不是 C 語言標准。
和restrict關鍵字有相同命運的還有 inline、_Complex 等,它們都是在 C99 標准中引入的關鍵字,但是其實在 C99 標准出來之前,GNU C 中早就有了 __inline__、__complex__ 等擴展關鍵字。還記得多年前我學習 Linux 0.11 版的源代碼時,看到大量的 __inline__ 曾經疑惑不已,不知道為什么 Linus 在 91 年就能用上了如此先進的語言功能,后來才知道,這是 GNU 的擴展關鍵字。
C 語言的標准有 C89 和 C99,使用 GCC 的時候甚至要顯示指定 -std=c99 才能全面支持 C99 標准,所以在開源界,大家還是喜歡首選 GNU 的擴展關鍵字。比如 __inline__、__complex__ 和 __restrict__。總而言之, C 語言標准干不過 GNU 擴展。
下面來看看 __restrict__ 的真正含義。還記得 CSDN 上曾經載過一篇文章《為什么有些語言會比別的快》,其中提到“很長一段時間,相同的兩個程序在 Fortran 和 C(或者C++)中運行,Fortran 會快一些,因為 Fortran 的優化做的更好。這是真的,就算 C 語言和 Fortran 的編譯器用了相同的代碼生成器也是一樣。這個不同不是因為 Fortran 的某種特性,事實上恰恰相反,是因為 Fortran 不具備的特性。”這是因為 C 語言中的指針給編譯器的優化帶來了困難,文章中繼續說道:“問題就來了。這些指針可以代替任何內存地址。更重要的是,他們可以重疊。輸出數組的內存地址也可以同時是輸入數組的。甚至可以部分重疊,輸出數組可以覆蓋一個輸入數組的一半。這對編譯器優化來說是個大問題,因為之前基於數組的優化不再適用。特別的,添加元素的順序也成問題,如果輸出重疊的數組,計算的結果會變得不確定,取決於輸出元素的動作是發生在元素被覆蓋之前還是之后。”
有了 __restrict__,C 語言的該問題將不復存在。用 __restrict__ 修飾一個指針后,①該指針只能在定義的時候被初始化;②不會再有別的指針指向該指針指向的內存,因此編譯器可以對算法進行優化。如下代碼:
int * __restrict__ p = (int*)malloc(100*sizeof(int));
指針 p 有 __restrict__ 關鍵字修飾,所以它只能在定義的時候被初始化,以后不能賦值,而沒有 __restrict__ 修飾的指針,可以隨時賦值,如下:
int arr[100]; int* pArr; pArr = arr;
指針 pArr 沒有被 __restrict__ 關鍵字修飾,所以可以將數組的首地址賦值給它。
比如我們定義一個函數對兩塊數據進行操作,結果放入第 3 塊內存,如下:
void func1(void* p1, void* p2, void* p3, int size){ for(int i=0; i<size; i++){ p3[i] = p1[i] + p2[i]; } }
很顯然,由於編譯器沒辦法判斷指針 p1、p2、p3 指向的內存是否重疊,所以無法進行優化,加上 __restrict__ 關鍵字后,如下:
void func1(void* __restrict__ p1, void* __restrict__ p2, void* __restrict__ p3, int size){ for(int i=0; i<size; i++){ p3[i] = p1[i] + p2[i]; } }
相當於明確告訴編譯器這幾塊內存不會重疊,所以編譯器就可以放心大膽對程序進行優化。
另一個關鍵字是 _Complex,C99 才引入,而且需要包含 <complex.h> 頭文件。其實在 GNU C 中,早就有__complex__、__real__、__imag__ 等擴展關鍵字。如下代碼:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(){ 5 __complex__ a = 3 + 4i; 6 __complex__ b = 5 + 6i; 7 __complex__ c = a + b; 8 __complex__ d = a * b; 9 __complex__ e = a / b; 10 printf("a + b = %f + %fi\n", __real__ c, __imag__ c); 11 printf("a * b = %f + %fi\n", __real__ d, __imag__ d); 12 printf("a / b = %f + %fi\n", __real__ e, __imag__ e); 13 return 0; 14 }
可以看到,在 C 語言中也可以直接對復數進行計算。數值計算再也不是 Fortran 的專利。
感悟二:指針和數組還真是不一樣
從學 C 語言開始,老師就教導我們說指針和數組是一樣的,它們可以用同樣的方式進行操作。而事實上,指針和數組還是有差別的。直到多年后讀《C專家編程》,才直到所謂指針和數組一樣是一個美麗的錯誤,只是因為在《The C Programming Language》這本書里,把“作為函數參數時,指針和數組一樣”這樣一句話前后分開分別印到了兩頁而已。
比如,指針不保存數據的長度信息,而數組有,如下代碼:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(){ 5 int* p = (int*)malloc(100*sizeof(int)); 6 int arr[100] = {0}; 7 printf("The size of p: %d\n", sizeof(p)); 8 printf("The size of arr: %d\n", sizeof(arr)); 9 return 0; 10 }
這段代碼的運行結果為:
The size of p: 8
The size of arr: 400
我們經常可以使用如下的代碼片段來獲得一個數組中有多少個元素,如下:
int arr[100]; size_t length = sizeof(arr)/sizeof(int);
但是,當使用數組作為函數的參數的時候,數組會退化成指針。如下代碼:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 void test_array(int arr[]){ 5 printf("The size of arr in function: %d\n", sizeof(arr)); 6 return; 7 } 8 9 int main(){ 10 int arr[100] = {0}; 11 printf("The size of arr in main: %d\n", sizeof(arr)); 12 test_array(arr); 13 return 0; 14 }
這段代碼的運行結果為:
The size of arr in main: 400
The size of arr in function: 8
感悟三:C 語言中的不完全類型(Incomplete Types)
在 GNU C 中可以定義不完全類型,不完全類型主要有兩種,一種是空的結構,一種是空的數組,比如:
struct point; char name[0];
空的結構不能定義變量,只能使用空結構的指針。空結構可以在后面再將它補充完整,如下:
struct point{ int x,y; };
空結構在定義鏈表的時候經常用到,如下:
struct linked_list{ struct linked_list* next; int x; /*other elements here perhaps */ } struct linked_list* head;
還有一種不完全類型就是將一個結構的最后一項定義為一個空的數組,這樣可以用來表示一個可變長度的結構或數組,演示該技術的代碼如下:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 typedef struct { 5 int length; 6 int arr[0]; 7 } incomplete_type; 8 9 int main(){ 10 char hello[] = "Hello, world!"; 11 int length = sizeof(hello) / sizeof(char); 12 incomplete_type* p = (incomplete_type*)malloc(sizeof(int) + length*sizeof(char)); 13 p->length = length; 14 for(int i=0; i<p->length; i++){ 15 p->arr[i] = hello[i]; 16 } 17 printf("p->length=%d\n", p->length); 18 printf("p->arr=%s\n", p->arr); 19 }
打造 C/C++ 的 IDE
后面的內容展示如何將 Vim 打造成一個半自動的 C/C++ IDE。讀過我的 Java 博客的朋友應該知道,其實我更喜歡用 Eclipse。只有在需要寫非常簡單的程序(比如做習題)的情況下,我才會用 Vim。這在我的《打造屬於自己的Vim》中有論述。在這篇文章中我展示了怎么使用 Vundle 管理插件以及怎么怎么閱讀幫助文檔,同時展示了 taglist.vim 的簡單用法。如果要用 Vim 來寫 C/C++程序,還需要做少許擴展。
第一,安裝以下幾個插件,由於使用 Vundle 管理插件,所以只需要把插件名寫入 .vimrc 配置文件,然后運行 :BundleInstall 即可,如下圖:

分別介紹一下這幾個插件。The-NERD-tree 是一個瀏覽目錄和文件的插件,可以使用 :help NERD_tree.txt 查看它的幫助文檔。taglist.vim 是瀏覽符號以及在符號之間跳轉的插件,使用 :help taglist.txt 查看它的幫助文檔。 a.vim 是在源代碼文件和頭文件之間跳轉的插件,不需要幫助文檔,它的命令就是 :A。c.vim 是提供IDE功能的主要插件,它提供的功能有自動注釋、反注釋、自動插入代碼塊及自動運行,如果安裝了 splint,還可以對代碼進行靜態檢查,使用 :help csupport.txt 查看它的文檔。OmniCppComplete 是一個提供自動補全功能的插件,使用 :help omnicppcomplete.txt 查看它的文檔。
這些插件中,taglist.vim 和 OmniCppComplete 需要 ctags 軟件的支持,所以需要安裝 exuberant-ctags 軟件包,在 Fedora 20 中,只需要使用 yum install ctags 即可自動安裝。
第二,生成 tags 數據庫,並將其加入到 Vim 中。
我們寫C程序的時候,使用到的文件主要存在於兩個地方,一個是我們工作的當前目錄,另外一個是 /usr/include。所以要到 /usr/include 目錄下使用 ctags 命令生成 tags 數據庫文件。為了使 tags 數據庫中包含盡可能多的信息(結構、枚舉、類、函數、宏定義等等),需要指定 ctags 的參數,如下:

然后將該 tags 文件的路徑加入到 .vimrc 配置文件中,同時設置一個鍵盤映射,使得按 Ctrl+F12 時,在工作目錄中調用 ctags 命令。如下配置文件的最后兩行:

然后,在使用 Vim 寫 C 程序的時候,如果輸入了 .、-> 這樣的元素,則其成員會自動補全。如果輸入的是一個字符串(比如函數名),可以按 Ctrl-X Ctrl-O 調用自動補全,如下圖:

不僅會彈出候選窗口,而且在最上面的窗口中會顯示函數的完整的簽名,及其所在的文件。這對於我們經常記不全函數名、記不清函數簽名的人來說,已經是莫大的福音了。
taglist.vim 和 OmniCppComplete 插件提供的功能用起來都只需要一個命令,而 c.vim 提供的命令就比較多了。而且在 c.vim 的幫助文檔中並沒有列出所有功能的命令,有一個辦法可以學習這些命令,那就是打開 GVim,通過 GVim 菜單中的 C/C++ 菜單來學習 c.vim 提供的功能和命令。
相比網上其它的將 Vim 打造成 IDE 的文章,我的配置比較簡單,基本上只安裝了幾個插件,而沒有做過多的設置。當我需要某個功能的時候,我會使用命令顯式地調用它,所以,稱它為半自動化 IDE 吧。
(京山游俠於2014-06-29發布於博客園,轉載請注明出處。)
最新進展
最近幾年 Linux 越來越被受到重視,因此 Linux 系統下的編輯軟件發展很快。特別是像微軟這樣實力雄厚的公司,一旦發威,對 Linux 世界的影響可以用翻天覆地來形容。就說在編輯器領域吧,微軟推出的 Visual Studio Code 已經成為了我編輯程序代碼的首選。當然,使用 vim 編輯一下配置文件還是很順手的,但是確實沒有必要想盡辦法把它配置成 IDE 了。
另外,Eclipse 也不錯,用 Eclipse 做 C/C++ 的 IDE 是絕對夠格的,其體驗應該是絕對能超過 vim 的。而且在 Linux 下使用 C/C++, Gnu Autotools 應該是一個繞不過的坎。具體內容,請看我這一篇 使用 Eclipse 和 Gnu Autotools 管理 C/C++ 項目
(京山游俠於2016-08-20更新於博客園,轉載請注明出處。)
