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

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

   對此就介紹到這

 

 

|——————————————————————

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

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

 

Tags: