C語言之預處理

  這是2016年的最後一篇博客,年初定的計劃是寫12篇博客,每個月一篇,1/3轉載,2/3原創,看來是實現不了了! -- 題外話。今天要寫的東西是C語言中的預處理器,咱們常說的宏定義的用法。爲何要寫這個東西呢,緣由很簡單:以前對預處理了解不深。若是你對C語言只是瞭解或者是僅僅在大學中學習過C語言,說到預處理估計你只知道下面這條語句:(由於我就是這種狀況,哈哈!)php

1 #define name value

  我再學習預處理直接的驅動力是看了php的源碼,開頭一大推的宏定義器,以前'掌握'的一點#define的用法太少了,根本看不懂源碼中宏的處理邏輯和運行的路徑。因此再學習預處理器頗有必要,裏面好多東西其實並不難,只是你沒有接觸到,等你學習了,就感受容易了。html

  1、宏定義和使用中的坑
c++

  這小節採用先給代碼再說明的形式,這樣你能夠看看每一個代碼的運行結果是否和你預期的一致!編輯器

  宏是什麼,宏就是#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 。道理和前面的同樣,只看替換後的結果才能真正看到答案。ui

  這樣的問題防不勝防,怎樣才能解決呢,其實辦法很簡單,錯誤的緣由是執行的順序和咱們預想的不同,那添加小括號應該能夠解決這種問題。 好比 (a) * (a)。這樣其實也不是最萬全的辦法,好比你看這個:ADD(a)  (a) + (a) ,若是這樣調用:ADD(2) * 5 ,這樣又不行了,被替換成了 (a) + (a) * 5 執行順序和預想的仍是不同,因此還要在最外層加上括號:((a) + (a)),這樣就解決了。編碼

 

  、預約義符號spa

  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()函數同樣,打印出變量的內容。

從這段代碼中能學習到幾點內容:

一、使用#define可使任何文本替換到程序中,在主程序中你能夠隨意使用VAR_DUMP。

二、宏定義不以分號結束,若是很是長的宏定義,你能夠在末尾加上反斜槓來分行,保持代碼易讀性。

三、你能夠定義頻繁調用的函數爲宏定義,這樣能夠加快執行的速速,具體緣由後面會說到。

四、C語言有幾個預約的符號須要咱們知道,不少時候特別有用:

  __FILE__ 預編譯的文件名

     __LINE__ 文件當前行的行號(執行到這一行)

  __DATE__ 文件編譯的日期

  __TIME__ 文件編譯的具體時間

     __STDC__ 是否遵循ANSI C (不經常使用)

最後附上運行結果,如圖:

  、宏替換的過程

  在程序的編譯階段,宏先被執行替換,通常要涉及下面的步驟:

  一、調用宏的地方看是否 進行了 #define定義,若是是就進行替換。

  二、把替換的文本信息插入到替換的位置,其中參數被替換成了實際的值。

  三、#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程序中你能看到許多的條件編譯,好比能夠根據當前的環境加載不一樣的宏配置,或者在編譯的時候加上直極預設的編譯條件。這些東西的實現都離不開條件編譯。

  一、條件嵌套,#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     . . . 

  二、是否已經被定義

      被定義:#if   define() 或者是#ifdef 

  不被定義:#if !define() 或者是#ifndef

  前者的寫法雖然沒有後者精煉,可是前者有更多的使用場景,好比下面這種,能夠進行嵌套執行。

1 #if defined(DEBUG)
2      #ifdef DEBUGTWO
3            #define TEST(a) a * a
4      #endif
5 #endif

  三、移除一個宏定義,當再也不使用一個宏定義後,可使用undef來把不須要的宏移除,原型:

1 #undef name

 

  、宏命名規則和與函數區別

  從前面的使用中咱們能夠看到,宏的使用規則和函數真是如出一轍,可是本質上仍是有區別的,在使用中怎樣區別宏和函數,涉及到代碼規範和代碼的可讀性問題。標準的宏使用應該使用大寫字母,這樣在程序中任意地方使用宏都會知道這是一個宏定義。好比前面用到的 #define TEST(a) ((a) * (a))。

  宏與函數區別有如下幾點:

  一、執行速度上,宏定義更快,函數由於須要調用棧,存在調用,返回,保存現場的系統開銷,因此比宏要慢。

  二、代碼長度上,宏在代碼長度上實際是增加的,每一處的使用宏都會把name替換成宏內容若是大量使用,會是代碼顯著增加,函數代碼只有一份,比較節省代碼空間。

  三、參數類型上,宏沒有參數類型,只要能夠 使用都行。函數不同,函數有參數類型肯定性。正式由於這樣,有些宏能巧妙的利用這一點,完成函數不能完成的任務,看下面代碼(書上看的),巧妙的利用傳遞類型無限制的特色自動開闢想要的各類類型空間:

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 }

  四、宏定義和函數的使用場景,宏定義通常在程序的開頭,函數轉化成宏定義必定要考慮成本問題,短小精煉的函數轉化成宏使用時最好的,功能負責的函數轉化成宏就有點得不償失了。

 

  、文件包含

  一、本地文件包含和庫文件包含  

  文件包含在大型系統中必然會用到,大型系統宏定義巨多無比,不可能把全部的宏定義都複製到每一個文件中,那麼文件包含就能解決這種問題。

  實際上編輯器支持兩種文件包含,一種是咱們常常會用的庫文件的包含,好比上面咱們看到的:#include <stdio.h>,還有一種是本地文件包含,說白了就是咱們本身寫的文件,包含的原型以下:

1 #include <filename>
2 #include "filename"

  這兩種方式均可以進行文件的包含,不一樣的是第一種是庫文件的包含,標準的C庫函數都會以.h擴展名結尾,第二種是本地文件包含,當編輯器看到第二種方式時,優先查找本路徑下得本地庫文件,若是沒有找到就會像包含庫文件那樣在指定的路徑下去找,這時第二種和第一種就差很少了。第二種包含方式在編碼習慣上也是比較好的,別人看你的代碼很容易知道這個文件是庫函數仍是你本身寫的。

  一、嵌套文件包含

  大型系統中不只有大量的文件包含,還會有大量的嵌套文件包含,看下面的例子:

  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

由於每次編譯編譯器都會讀入整個頭文件,若是把全部的文件都加上這個條件編譯的話,那交叉引用文件產生的重複宏編譯問題就解決了,運行以下:

  好了,就寫這麼多吧,從新梳理了對宏定義的認識和基本的使用。時間倉促,出錯的地方請大嬸們必定指出,萬分感謝!

注意:
一、本博客同步更新到個人我的網站:http://www.zhaoyafei.cn
二、本文屬原創內容,爲了尊重他人勞動,轉載請註明本文地址:

http://www.cnblogs.com/zyf-zhaoyafei/p/6237295.html

相關文章
相關標籤/搜索