這是2016年的最後一篇博客,年初定的計劃是寫12篇博客,每月一篇,1/3轉載,2/3原創,看來是實現不了了! -- 題外話。今天要寫的東西是C語言中的預處理器,我們常說的宏定義的用法。為什麼要寫這個東西呢,原因很簡單:之前對預處理了解不深。如果你對C語言只是了解或者是僅僅在大學中學習過C語言,說到預處理估計你只知道下面這條語句:(因為我就是這種情況,哈哈!)
1 #define name value
我再學習預處理直接的驅動力是看了php的源碼,開頭一大推的宏定義器,之前'掌握'的一點#define的用法太少了,根本看不懂源碼中宏的處理邏輯和運行的路徑。所以再學習預處理器很有必要,裡面好多東西其實並不難,只是你沒有接觸到,等你學習了,就感覺容易了。
一、宏定義和使用中的坑
這小節采用先給代碼再說明的形式,這樣你可以看看每個代碼的運行結果是否和你預期的一致!
宏是什麼,宏就是#define機制把指定的參數替換的文本中,這樣的實現方式就是宏。使用宏定義可以抽出頻繁調用的函數,加快執行的速度。定義如下:#define name(參數) 執行體... “參數”可以是使用逗號分隔的參數列表,這些參數可以被應用到執行體中,必須要注意的是“參數”的左括號必須和宏名字緊鄰,不然編輯器會報錯,或者被解釋成執行體中的一部分。比如你寫了一個 TEST(a) a * a 調用執行的時候寫上 TEST(1) 實際執行的是替換後的 1 * 1。
凡事都有利弊,宏定義固然使用方便,並且有著函數不可比擬的執行速度,但是宏定義中存在不少的坑,下面就說一說這個坑。看下面的代碼:
1 #include <stdio.h> 2 3 #define TEST(a) a * a 4 5 int main() { 6 int b = TEST(2); 7 int c = TEST(1+2); 8 printf("b=%d, c=%d", b, c); 9 printf("\n\n"); 10 }
沒有執行的情況下,你感覺得到的結果是多少呢!好多人不加思索的說:b=4,c=9。如果真是這樣,就不存在坑了,實際打印出來是:b=4, c=5 ,為什麼c的值和預想的會有偏差,其實你把執行體中的值替換一下試試,就不難發現問題了,當輸入1+2的時候,宏替換成了 1+2*1+2,當然就是5了。好了明白了,那你學會了嗎?學會了再看一個:
1 #include <stdio.h> 2 3 #define TEST(a,b) ((a) > (b) ? (a) : (b)) 4 5 int main() { 6 int zyf = 1; 7 int abc = 2; 8 int ret = TEST(zyf++, abc++); 9 printf("zyf=%d,abc=%d,ret=%d", zyf, abc, ret); 10 printf("\n\n"); 11 }
輸出多少呢,如果是 zyf=2,abc=3,ret=3 就錯了,實際結果是:zyf=2,abc=4,ret=3 。道理和前面的一樣,只看替換後的結果才能真正看到答案。
這樣的問題防不勝防,怎樣才能解決呢,其實辦法很簡單,錯誤的原因是執行的順序和我們預想的不一樣,那添加小括號應該可以解決這種問題。 比如 (a) * (a)。這樣其實也不是最萬全的辦法,比如你看這個:ADD(a) (a) + (a) ,如果這樣調用:ADD(2) * 5 ,這樣又不行了,被替換成了 (a) + (a) * 5 執行順序和預想的還是不一樣,所以還要在最外層加上括號:((a) + (a)),這樣就解決了。
二、預定義符號
C語言中有幾個預定義的符號,還是有必要和大家說上一說,先看一段代碼:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #define VAR_DUMP printf( \ 4 "[\n \tfile:%s\n" \ 5 "\tline:%d\n" \ 6 "\ttime:%s %s\n" \ 7 "\tvalue:%d\n]", \ 8 __FILE__, __LINE__, __DATE__, __TIME__, value \ 9 ) 10 int main() { 11 int value = 1; 12 VAR_DUMP; 13 printf("\n\n"); 14 }
是不是和你在大學學習的有點不一樣,最簡單的宏定義可以使用#define name value 的方式,當然也可以把值寫成一個函數,運行的時候直接替換函數。這個宏定義是封裝了調試方法,是打印變量內容能像PHP中var_dump()或者print_r()函數一樣,打印出變量的內容。
從這段代碼中能學習到幾點內容:
1、使用#define可以使任何文本替換到程序中,在主程序中你可以隨意使用VAR_DUMP。
2、宏定義不以分號結束,如果非常長的宏定義,你可以在末尾加上反斜槓來分行,保持代碼易讀性。
3、你可以定義頻繁調用的函數為宏定義,這樣可以加快執行的速速,具體原因後面會說到。
4、C語言有幾個預定的符號需要我們知道,很多時候特別有用:
__FILE__ 預編譯的文件名
__LINE__ 文件當前行的行號(執行到這一行)
__DATE__ 文件編譯的日期
__TIME__ 文件編譯的具體時間
__STDC__ 是否遵循ANSI C (不常用)
最後附上運行結果,如圖:
三、宏替換的過程
在程序的編譯階段,宏先被執行替換,一般要涉及下面的步驟:
1、調用宏的地方看是否 進行了 #define定義,如果是就進行替換。
2、把替換的文本信息插入到替換的位置,其中參數被替換成了實際的值。
3、#define可以包含其他定義的#define定義的東西,需要注意的是不能出現遞歸的情況。
因為替換存在臨近字段自動結合,所以可以使用一些巧妙的方案:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define VAR_DUMP(A,B)\ 5 printf("Value of " #B " is " A "\n", B) 6 7 int main(){ 8 int x = 1; 9 VAR_DUMP("%d", x+2); 10 }
四、條件編譯和其他宏用法
在大型的C程序中你能看到許多的條件編譯,比如可以根據當前的環境加載不同的宏配置,或者在編譯的時候加上直極預設的編譯條件。這些東西的實現都離不開條件編譯。
1、條件嵌套,#if #endif 原型:
1 #if condition 2 執行體 3 #endif
可以根據condition來確定執行體要不要執行,以此來控制在不同的環境下編譯成不同的系統。看下面的代碼,當把DEBUG定義成非0值時,MAX宏定義是存在的,當定義成0時,程序就會報錯。
1 #include <stdio.h> 2 3 #define DEBUG 0 4 #if DEBUG 5 #define MAX(a) ((a) * (a)) 6 #endif 7 8 int main() { 9 int b = MAX(2); 10 int c = MAX(1+2); 11 printf("b=%d, c=%d", b, c); 12 printf("\n\n"); 13 }
當然#if 也可以與#elif嵌套使用,這樣就和我們在函數裡使用if else一樣了,下面是一段php源碼中的一段話,你能看到編譯php指定不同的參數,檢查不同的環境等等都可以通過預處理中的條件編譯開完成。
1 #ifndef PHP_H 2 #define PHP_H 3 4 #ifdef HAVE_DMALLOC 5 #include <dmalloc.h> 6 #endif 7 8 #define PHP_API_VERSION 20100412 9 #define PHP_HAVE_STREAMS 10 #define YYDEBUG 0 11 12 #include "php_version.h" 13 #include "zend.h" 14 #include "zend_qsort.h" 15 #include "php_compat.h" 16 #include "zend_API.h" 17 18 #undef sprintf 19 #define sprintf php_sprintf 20 21 /* PHP's DEBUG value must match Zend's ZEND_DEBUG value */ 22 #undef PHP_DEBUG 23 #define PHP_DEBUG ZEND_DEBUG 24 25 #ifdef PHP_WIN32 26 # include "tsrm_win32.h" 27 # include "win95nt.h" 28 # ifdef PHP_EXPORTS 29 # define PHPAPI __declspec(dllexport) 30 # else 31 # define PHPAPI __declspec(dllimport) 32 # endif 33 # define PHP_DIR_SEPARATOR '\\' 34 # define PHP_EOL "\r\n" 35 #else 36 # if defined(__GNUC__) && __GNUC__ >= 4 37 # define PHPAPI __attribute__ ((visibility("default"))) 38 # else 39 # define PHPAPI 40 # endif 41 42 # define THREAD_LS 43 # define PHP_DIR_SEPARATOR '/' 44 # define PHP_EOL "\n" 45 #endif 46 47 #ifdef NETWARE 48 /* For php_get_uname() function */ 49 #define PHP_UNAME "NetWare" 50 #define PHP_OS PHP_UNAME 51 #endif 52 53 #if HAVE_ASSERT_H 54 #if PHP_DEBUG 55 #undef NDEBUG 56 #else 57 #ifndef NDEBUG 58 #define NDEBUG 59 #endif 60 #endif 61 #include <assert.h> 62 63 #else /* HAVE_ASSERT_H */ 64 #define assert(expr) ((void) (0)) 65 #endif /* HAVE_ASSERT_H */ 66 67 #define APACHE 0 68 #if HAVE_UNIX_H 69 #include <unix.h> 70 #endif 71 72 #if HAVE_ALLOCA_H 73 #include <alloca.h> 74 #endif 75 76 #if HAVE_BUILD_DEFS_H 77 #include <build-defs.h> 78 #endif 79 . . .
2、是否已經被定義
被定義:#if define() 或者是#ifdef
不被定義:#if !define() 或者是#ifndef
前者的寫法雖然沒有後者精煉,但是前者有更多的使用場景,比如下面這種,可以進行嵌套執行。
1 #if defined(DEBUG) 2 #ifdef DEBUGTWO 3 #define TEST(a) a * a 4 #endif 5 #endif
3、移除一個宏定義,當不再使用一個宏定義後,可以使用undef來把不需要的宏移除,原型:
1 #undef name
五、宏命名規則和與函數區別
從前面的使用中我們可以看到,宏的使用規則和函數真是一模一樣,但是本質上還是有區別的,在使用中怎樣區別宏和函數,涉及到代碼規范和代碼的可讀性問題。標准的宏使用應該使用大寫字母,這樣在程序中任意地方使用宏都會知道這是一個宏定義。比如前面用到的 #define TEST(a) ((a) * (a))。
宏與函數區別有以下幾點:
1、執行速度上,宏定義更快,函數因為需要調用棧,存在調用,返回,保存現場的系統開銷,所以比宏要慢。
2、代碼長度上,宏在代碼長度上實際是增長的,每一處的使用宏都會把name替換成宏內容如果大量使用,會是代碼顯著增長,函數代碼只有一份,比較節省代碼空間。
3、參數類型上,宏沒有參數類型,只要可以 使用都行。函數不一樣,函數有參數類型確定性。正式因為這樣,有些宏能巧妙的利用這一點,完成函數不能完成的任務,看下面代碼(書上看的),巧妙的利用傳遞類型無限制的特點自動開辟想要的各種類型空間:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define CREATE_P(nums, type) ((type *) malloc((nums) * sizeof(type))) 5 6 int main(){ 7 int nums = 2; 8 CREATE_P(nums, int); 9 }
4、宏定義和函數的使用場景,宏定義一般在程序的開頭,函數轉化成宏定義一定要考慮成本問題,短小精煉的函數轉化成宏使用時最好的,功能負責的函數轉化成宏就有點得不償失了。
六、文件包含
1、本地文件包含和庫文件包含
文件包含在大型系統中必然會用到,大型系統宏定義巨多無比,不可能把所有的宏定義都復制到每個文件中,那麼文件包含就能解決這種問題。
實際上編輯器支持兩種文件包含,一種是我們經常會用的庫文件的包含,比如上面我們看到的:#include <stdio.h>,還有一種是本地文件包含,說白了就是我們自己寫的文件,包含的原型如下:
1 #include <filename> 2 #include "filename"
這兩種方式都可以進行文件的包含,不同的是第一種是庫文件的包含,標准的C庫函數都會以.h擴展名結尾,第二種是本地文件包含,當編輯器看到第二種方式時,優先查找本路徑下得本地庫文件,如果沒有找到就會像包含庫文件那樣在指定的路徑下去找,這時第二種和第一種就差不多了。第二種包含方式在編碼習慣上也是比較好的,別人看你的代碼很容易知道這個文件是庫函數還是你自己寫的。
1、嵌套文件包含
大型系統中不僅有大量的文件包含,還會有大量的嵌套文件包含,看下面的例子:
a.h,b.h,c.h,define.c文件,其中a,b,c,define文件的內容如下:
1 a.h: 2 #include "c.h" 3 void var_dumpa(){ 4 test obja; 5 obja.a[1] = 2; 6 printf("obja.a[1]: %d\n", obja.a[1]); 7 } 8 9 b.h: 10 #include "c.h" 11 void var_dumpb(){ 12 test objb; 13 objb.a[1] = 2; 14 printf("objb.a[1]: %d\n", objb.a[1]); 15 } 16 17 c.h: 18 #include <stdlib.h> 19 #include <stdio.h> 20 21 typedef struct test{ 22 int a[10]; 23 }test; 24 25 define.c: 26 #include <stdio.h> 27 #include "a.h" 28 #include "b.h" 29 30 int main() { 31 var_dumpa(); 32 var_dumpb(); 33 printf("\n\n"); 34 }
ab文件包含c文件,define.c文件文件引用a,b文件後會引發一個錯誤:typedef struct test類型錯誤,因為c.h文件被包含了兩次,像這種情況在大型系統中會經常遇到,或者說,你會發現重復引用庫文件也不會報錯,由此可見,庫文件一定是使用了解決辦法。其實解決這種錯誤的方案就是采用條件編譯,當這個文件引入到另一個文件中後我們可以設置一個宏定義,比如:
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 #ifndef PATH_C_H 5 #define PATH_C_H 1 6 typedef struct test{ 7 int a[10]; 8 }test; 9 #endif
因為每次編譯編譯器都會讀入整個頭文件,如果把所有的文件都加上這個條件編譯的話,那交叉引用文件產生的重復宏編譯問題就解決了,運行如下:
好了,就寫這麼多吧,重新梳理了對宏定義的認識和基本的使用。時間倉促,出錯的地方請大嬸們一定指出,萬分感謝!
注意: 1、本博客同步更新到我的個人網站:http://www.zhaoyafei.cn 2、本文屬原創內容,為了尊重他人勞動,轉載請注明本文地址:http://www.cnblogs.com/zyf-zhaoyafei/p/6237295.html