1. 程式人生 > >C語言之預處理詳解

C語言之預處理詳解

C語言之預處理詳解

綱要:

  • 預定義符號
  • #define
    • #define定義識別符號
    • #define定義巨集
    • #define的替換規則
    • #與##
  • 幾點注意#undef
    • 帶副作用的巨集引數
    • 巨集和函式的對比
    • 命名約定
  • 命令列定義
  • 條件編譯
    • 單分支條件編譯
    • 多分支條件編譯
    • 判斷是否被定義
    • 巢狀指令
  • 檔案包含
    • 標頭檔案被包含的方式
    • 巢狀檔案包含
  • 其他預處理指令
    • #error
    • #line
    • #pragma

注:此篇內容會微微涉及到:C語言之簡易瞭解程式環境,但是對與此篇的理解影響不大

 

一.預定義符號

__FILE__//進行編譯的原始檔

__LINE__//檔案當前的行號

__DATE__//檔案被編譯的日期

__TIME__//檔案被編譯的時間

__STDC__//如果編譯器遵循ANSI C,其值為1,否則未定義

__FUNCTION__//當前所在的函式

  我們來看一個例子:

void test()
{
    printf("FILE: %s\n", __FILE__);//所在的檔案
    printf("LINE: %d\n", __LINE__);//所在的行
    printf("DATE: %s\n", __DATE__);//被編譯的日期
    printf("TIME: %s\n", __TIME__);//被編譯的時間
    printf("FUNCTION: %s\n", __FUNCTION__);//所在的函式名稱
}
int main()
{
    test();
    printf("FUNCTION: %s\n", __FUNCTION__);//所在的函式名稱
    return 0;
}

 

 

   注意:

    1.這些預定義符號都是語言內建的。不需要再引用其他的庫函式

    2.這些預定義符號再預編譯階段就別替換了

 

  接下來我們來看看我們的編譯器對 __STDC__ 的支援:

int main()
{
    printf("%d\n", __STDC__);
    return 0;
}

  VS 2019:

 

 

   gcc:

 

 

   我們可以看到VS對於STDC的支援並不是很好;

 

二.#define

  對於#define 定義的東西同樣也是再預編譯階段就進行了替換。

   1.#define定義識別符號

  語法: #define name stuff 

    在預編譯時,將 name 替換為 stuff

  示例:

#define MAX 100

#define STR "HEHE"

#define reg register //register 這個關鍵字是請求編譯器把變數儲存在暫存器中,而不是放在記憶體裡,可以提高訪問效率
                    //但register 給你提供的地方很小,放不了很多變數


int main()
{
    reg int age = 10;

    printf("%d\n", MAX);//100
    printf("%s\n", STR);//HEHE
    printf("%d\n", age);//10

    return 0;
}

  即替換之後為:

int main()
{
    register int age = 10;

    printf("%d\n",100);
    printf("%s\n","hehe");
    printf("%d\n",10);

    return 0;
}

 注意:

   在#define定義識別符號時,儘量不要新增 ;   

   如:

#define MAX 1000;
//#define MAX 1000

int main()
{
    int max, condition = 1;
    if (condition)
        max = MAX;//要是第一種加了 ; 就會很容易出現錯誤,因為在我們的認知中,一條語句結束就要加一個 ; 
    else
        max = 0;

    return 0;
}

 

    2.#define定義巨集

  #define 機制包括了一個規定,允許把引數替換到文字中,這種實現通常稱為巨集(macro)或定義巨集(definemacro)。

  下面是巨集的申明方式:

#define name( parament-list ) stuff 其中的 parament-list 是一個由逗號隔開的符號表,它們可能出現在stuff中。

注意: 引數列表的左括號必須與name緊鄰。 如果兩者之間有任何空白存在,引數列表就會被解釋為stuff的一部分。

  示例:

#define SQUARE(x) (x*x)

int main()
{
    printf("%d\n", SQUARE(5));
    return 0;
}
#define SQUARE (x) (x*x)//如果我們在E後敲一個空格,我們就會發現編譯器就已經報了錯

int main()
{
    printf("%d\n", SQUARE(5));
    return 0;
}
#define SQUARE(x) (x*x) 
//我們再來換個數字來看看,換成一個表示式

int main()
{
    printf("%d\n", SQUARE(2+3));//此時的結果會是25嗎?
    return 0;
}

  可是我們執行後發現結果為 11 為什麼呢?

 

 

 

#define SQUARE(x) (x*x) //11
#define SQUARE(x) ((x)*(x)) //25

  提示:

所以用於對數值表示式進行求值的巨集定義都應該用這種方式加上括號,避免在使用巨集時由於引數中的操作符或鄰近操作符之間不可預料的相互作用。

  例:offsetof 的模擬實現

#include<stdlib.h>
//模擬實現offsetof的實現
#define OFFSETOF(type,member) ((int)&(((type*)0)->member))

struct test
{
    int a;
    char b;
    double c;
};

int main()
{
    struct test stu = { 0,0,0 };
    printf("OFFSETOF:\n");
    printf("%d\n",OFFSETOF(struct test, a));
    printf("%d\n",OFFSETOF(struct test, b));
    printf("%d\n",OFFSETOF(struct test, c));
    printf("offsetof:\n");
    printf("%d\n", offsetof(struct test, a));
    printf("%d\n", offsetof(struct test, b));
    printf("%d\n", offsetof(struct test, c));
    return 0;
}

   3.#define的替換規則

在程式中擴充套件#define定義符號和巨集時,需要涉及幾個步驟。

1. 在呼叫巨集時,首先對引數進行檢查,看看是否包含任何由#define定義的符號。如果是,它們首先被替換。

2. 替換文字隨後被插入到程式中原來文字的位置。對於巨集,引數名被他們的值替換。

3. 最後,再次對結果檔案進行掃描,看看它是否包含任何由#define定義的符號。如果是,就重複上述處理過程。

注意:

1. 巨集引數和#define 定義中可以出現其他#define定義的變數。但是對於巨集,不能出現遞迴。

2. 當前處理器搜尋#define定義的符號的時候,字串常量的內容並不被搜尋。

   4.#與##

  在此之前,我們先來看一給引例:

//對於它,我們要是放在巨集裡該怎麼實現?
int main()
{
    int a = 4;
    printf("a=%d", a);
    return 0;
}

  我們先來試著寫一下:

  我們要想到,這寫出來不能只打印整形,要兼顧其他的型別

  我們發現好像有點困難

  這時,就需要 # 來幫忙了

  1.#

    使用 # ,可以把一個巨集引數變成對應的字串

    我們發現,現在只需寫成這樣,便可滿足上面的要求了:

#define print(num,data) printf("The value of "#num " is " data"\n",num);

int main()
{
    int a = 3;
    print(a,"%d");
    return 0;
}

    可能有人會對printf中的那麼多 “ ” 感到疑惑。,我們繼續來看一個例子:

int main()
{
    printf("Hello"" World ""!\n");//它會打印出什麼
    return 0;
}

 

 

     我們發現字串是有自動連線的特點的。這時,只要參考這個例子就可以理解上面那個例子為什麼要那樣寫了

   2.##

    ##可以把位於它兩邊的符號合成一個符號。 它允許巨集定義從分離的文字片段建立識別符號。

例:

#define STR "HELLO "##"WORLD!"
#define NUM 100##999
#define ADD_TO_SUM(num, value) sum##num += value  . 

int main()
{
    printf("%s\n", STR);//HELLO WORLD!
    printf("%d\n", NUM);//100999


    int sum5 = 0;
    ADD_TO_SUM(5, 10);//作用是:給sum5增加10
    printf("%d",sum5);

    return 0;
}

 

 

     注:

      在拼湊變數名時,這樣的連線必須產生一個合法的識別符號。否則其結果就是未定義的。

 

三.幾點注意

  在我們寫#define定義的時候,往往會出現一些摸不到頭腦的問題,下面我就來提一提。

   1.帶副作用的巨集引數

  我們先看一個例子:

int main()
{
    int a = 10;
    int b = 20;
    int c = MAX(a++, b++);
    printf("%d\n", c);
    printf("a=%d b=%d\n", a, b);
    return 0;
}

  它的結果會是什麼呢?我們可以好好想一想。

  執行結果:

  是不是沒有想到呢,我們再來補充一點註釋來看:

#define MAX(X,Y)  ((X)>(Y)?(X):(Y))

int main()
{
    //int m = 5;
    //int n = m + 1;//n = 6 m = 5
    //int n = ++m;  //n = 6 m = 6

    int a = 10;
    int b = 20;
    
    //傳遞給MAX巨集的引數是帶有副作用的
    int c = MAX(a++, b++);

    //int c = ((a++) > (b++) ? (a++) : (b++));

    printf("%d\n", c);//?
    printf("a=%d b=%d\n", a, b);

    return 0;
}

 

 

     所以:當巨集引數在巨集的定義中出現超過一次的時候,如果引數帶有副作用,那麼你在使用這個巨集的時候就可能出現危險,導致不可預測的後果。副作用就是表示式求值的時候出現的永久性效果

  如:

x+1;//不帶副作用
x++;//帶有副作用

 

   2.巨集和函式的對比

  在這分別有一個求最大值的巨集和函式,哪個好一點呢?

#define MAX(X,Y)  ((X)>(Y)?(X):(Y))

int INT_max(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    printf("%d\n", INT_max(1, 5));
    printf("%d\n", MAX(1, 5));
    return 0;
}

  要是我選擇,我選擇用巨集來實現,為什麼呢?

  我們看到利用巨集:

 

 

   利用函式:

 

 

 

 

 

   我們發現:在這個例子中,巨集轉成的組合語言要比函式少的多!

 

  巨集的優點:

1. 用於呼叫函式和從函式返回的程式碼可能比實際執行這個小型計算工作所需要的時間更多。所以巨集比函式在程式的規模和速度方面更勝一籌。

2. 更為重要的是函式的引數必須宣告為特定的型別。所以函式只能在型別合適的表示式上使用。反之這個巨集怎可以適用於整形、長整型、浮點型等可以用於>來比較的型別。巨集是型別無關的。

  但是並不是這樣說,巨集就沒有缺點了

  巨集的缺點:

1. 每次使用巨集的時候,一份巨集定義的程式碼將插入到程式中。除非巨集比較短,否則可能大幅度增加程式的長度。

2. 巨集是沒法除錯的。

3. 巨集由於型別無關,也就不夠嚴謹。

4. 巨集可能會帶來運算子優先順序的問題,導致程容易出現錯。

 

  但是,巨集有時候可以做函式做不到的事情。比如:巨集的引數可以出現型別,但是函式做不到。

  例如:

#define MALLOC(type,num) ((type*)malloc((num)*sizeof(type)))//動態開闢記憶體

int main()
{
    int* p = MALLOC(int, 10);//開闢10個整形的空間
    //...
    free(p);//釋放記憶體
    p = NULL;//及時置NULL
    return 0;
}

 

    巨集和函式的對比:、

 

 

 

   3.命名約定

  一般來講函式的巨集的使用語法很相似。所以語言本身沒法幫我們區分二者。 那我們平時的一個習慣是:

    1.把巨集名全部大寫

    2.函式名不要全部大寫


四.#undef

  #undef 是用來撤銷巨集定義的,例:

#include <stdio.h>

#define MAX 100

int main()
{
    printf("%d\n", MAX);
#undef MAX
    printf("%d\n", MAX);

    return 0;
}

  我們執行會發現,在第二個printf語句中的MAX是未定義的

  注:

    如果現存的一個符號內容需要被重新定義,那麼它的舊內容首先要被移除。

 

五.命令列定義

  許多C 的編譯器提供了一種能力,允許在命令列中定義符號。用於啟動編譯過程。

  例如:當我們根據同一個原始檔要編譯出不同的一個程式的不同版本的時候,這個特性有點用處。

  (假定某個程式中聲明瞭一個某個長度的陣列,如果機器記憶體有限,我們需要一個很小的陣列,但是另外一個機器記憶體大寫,我們需要一個數組能夠大寫。)

  示例:

#include <stdio.h> 
int main()
{
    int array[NUM];
    int i = 0;
    for (i = 0; i < NUM; i++)
    {
        array[i] = i;
    }
    for (i = 0; i < NUM; i++)
    {
        printf("%d ", array[i]);
    }
    printf("\n");
    return 0;
}

 

 

   這時我們就可以在命令列裡定義NUM的大小了,命令 gcc -D NUM=10 test.c 

六.條件編譯

在編譯一個程式的時候我們如果要將一條語句(一組語句)編譯或者放棄是很方便的。因為我們有條件編譯指令。

比如說:

  除錯性的程式碼,刪除可惜,保留又礙事,所以我們可以選擇性的編譯。

   1.單分支條件編譯

   滿足條件就參與編譯,不滿足條件就不參與編譯

//條件編譯  - 滿足條件就參與編譯,不滿足條件就不參與編譯

#define DEBUG 1

int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", i);
#if DEBUG
        printf("hehe\n");
#endif
    }
    return 0;
}
//條件編譯  - 滿足條件就參與編譯,不滿足條件就不參與編譯

#define DEBUG 0

int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", i);
#if DEBUG
        printf("hehe\n");
#endif
    }
    return 0;
}

    在上面改變了DEBUG的值,執行結果也隨之變化!

 

   2.多分支條件編譯

//2.多個分支的條件編譯
#if 常量表達式
//... 
#elif 常量表達式
//... 
#else 
//... 
#endif 

    同樣是滿足條件就執行,但在一個過程中只執行一個!(從#if到所匹配的#endif結束)

int main()
{
    int a = 10;
#if a-2
    printf("First\n");
#elif 3-1
    printf("Second\n");
#elif 5-5
    printf("Third\n");
#else
    {
        printf("hehe\n");
        printf("hehe\n");
    }
#endif

    return 0;
}

 

   3.判斷是否被定義

  定義就執行

3.判斷是否被定義
#if defined(symbol) 
#ifdef symbol 
#if !defined(symbol) 
#ifndef symbol 
#define __DEBUG__ 0

int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", i);
#ifdef __DEBUG__
        printf("hehe\n");
#endif
    }
    return 0;
}

 

 

 

 

 

 

    不過它有兩種方式可供選擇:

#define PRINT 0

int main()
{
    //定義了PRINT才打印hehe --- 第一種寫法
#ifdef PRINT
    printf("hehe\n");
#endif
    return 0;
}

#define PRINT

int main()
{
    //定義了PRINT才打印hehe --- 第二種寫法
#if defined(PRINT)
    printf("hehe\n");
#endif

    return 0;
}
#define PRINT 0

int main()
{
    //沒有定義PRINT才打印hehe --- 第一種寫法
#ifndef PRINT
    printf("hehe\n");
#endif
    return 0;
}


#define PRINT
int main()
{
    //沒有定義PRINT才打印hehe --- 第二種寫法

#if !defined(PRINT)
    printf("hehe\n");
#endif
    return 0;
}

 

   4.巢狀指令

//簡單示例
//4.巢狀指令
#if defined(OS_UNIX) 
    #ifdef OPTION1 
        unix_version_option1();
    #endif 
    #ifdef OPTION2 
        unix_version_option2();
    #endif 
#elif defined(OS_MSDOS) 
    #ifdef OPTION2 
        msdos_version_option2();
    #endif 
#endif 

 

#define PASS
#define HAHA

void haha()
{
    printf("haha\n");
}

void ha()
{
    printf("ha\n");
}

int main()
{
#ifdef PASS
    #ifdef HAHA
        haha();
    #endif // haha

    #ifdef HAHA
        ha();
    #endif // ha

#endif // DEBUG

    return 0;
}

 

 

七.檔案包含

  我們已經知道, #include 指令可以使另外一個檔案被編譯。就像它實際出現於 #include 指令的地方一樣。

  這種替換的方式很簡單: 前處理器先刪除這條指令,並用包含檔案的內容替換。 這樣一個原始檔被包含10次,那就實際被編譯10次。

   1.標頭檔案被包含的方式

  1.<name>  : 包含庫裡的檔案

    程式怎麼查詢這個檔案呢:

      查詢標頭檔案直接去標準路徑下去查詢,如果找不到就提示編譯錯誤

  2."name"  : 包含我們自己寫的檔案

    程式怎麼查詢這個檔案呢:

      先在原始檔所在目錄下查詢,如果該標頭檔案未找到,編譯器就像查詢庫函式標頭檔案一樣在標準位置查詢標頭檔案。 如果找不到就提示編譯錯誤

  

   2.巢狀檔案包含

  這種情況是指出現了檔案套檔案,套來套去,如下圖:

 

 

   解釋一下:

    comm.h和comm.c是公共模組。 test1.h和test1.c使用了公共模組。 test2.h和test2.c使用了公共模組。 test.h和 test.c使用了test1模組和test2模組。 這樣最終程式中就會出現兩份comm.h的內容。這樣就造成了檔案內容的重複。

  那怎麼樣處理這種情況呢?---條件編譯

  在每個標頭檔案的開頭寫:

#ifndef __TEST_H__ 
#define __TEST_H__ 
//標頭檔案的內容
#endif //__TEST_H__

  或者:

#pragma once //只使用一次

 

八.其他預處理指令

   1.#error

  在程式編譯時,只要遇到 #error 就會生成一個錯誤提示訊息,並停止編譯,語法格式:

#error error-message

  示例:

#define test

int main()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d\n", i);
        if (i == 5)
        {
#ifdef test
#error this is a test!
#endif
        }
    }
    return 0;
}

   2.#line

  改變當前行數和檔名稱,基本形式:

#line number "filename"

  示例:

#include<stdio.h>

int main()
{
    printf("filename :%s\n",__FILE__);
    printf("line :%d\n",__LINE__);

#line 100 "test.c"
    printf("filename :%s\n", __FILE__);
    printf("line :%d\n", __LINE__);


    return 0;
}

  注:檔名可以不寫

 

 

   3.#pragma

  它的作用是設定編譯器的狀態或指示編譯器完成一些特點的動作,在這我們只挑出幾個來說:

  1.#pragma message

    message 引數:在編譯資訊輸出視窗中輸出相應的資訊

   示例:

#pragma message("This is a test!")
int main()
{
    return 0;
}

 

 

   2.pragma once

    這個在剛剛我們就已經提過了;

    它的作用是將標頭檔案只編譯一次;

  3.pragma pack

    在結構體記憶體章節,我們就已經對它有了介紹

   對此就介紹到這

 

 

|------------------------------------------------------------------

到此,對於預處理詳解便到此結束!

因筆者水平有限,若有錯誤,還望指正

&n