C語言之預處理詳解

C語言之預處理詳解

綱要:html

  • 預約義符號
  • #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;
}

 

 

   注意:this

    1.這些預約義符號都是語言內置的。不須要再引用其餘的庫函數spa

    2.這些預約義符號再預編譯階段就別替換了命令行

 

  接下來咱們來看看咱們的編譯器對 __STDC__ 的支持:3d

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

  VS 2019:unix

 

 

   gcc:調試

 

 

   咱們能夠看到VS對於STDC的支持並非很好;code

 

二.#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

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

   對此就介紹到這

 

 

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

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

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

相關文章
相關標籤/搜索