您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關C語言如何實現預處理效果,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
編譯一個C語言程序涉及很多步驟。其中第一個步驟被稱為預處理。C語言的預處理器在源代碼編譯之前對其進行一些文本性質的操作。它的主要任務包括刪除注釋、插入被#include指令包含的文件內容、定義和替換由#define指令定義的符號,同時確定代碼的部分內容是否應該根據一些條件編譯指令進行編譯。
下表為C語言預處理器定義的符號。他們的值有的是字符串常量,有的是十進制數字常量。
符號 | 示例值 | 含義 |
---|---|---|
__ FILE__ | “test.c” | 當前編譯的源文件名 |
__ LINE__ | 25 | 本文件當前行號 |
__ DATE__ | “Dec 27 2021” | 文件被編譯日期 |
__ TIME__ | “21:30:23” | 文件被編譯時間 |
__ STDC__ | 1 | 如果編譯器遵循ANSI C,其值就為1,否則未定義 |
我們先來看一下它的用法
#define name stuff
有了這條指令以后,每當有符號name出現在這條指令之后時,預處理器就會把它替換為stuff。
如果定義中的stuff非常長,那就可以將它分成幾行,除了最后一行,每行的末尾都要加一個反斜杠,如下面例子所示:
#define DEBUG_PRINT printf("File %s line %d:" \ "x = %d, y = %d, z = %d, \ __FILE__, __LINE__, x, y, z)
這里利用了一個特性:相鄰的字符串常量被自動連接為一個字符串。在調試一個存在許多涉及一組變量的不同計算過程的程序時,這種類型的聲明非常有用。我們可以很容易的插入一條測試語句,打印出它們的當前值。
x *= 2; y += x; z = x * y; DEBUG_PRINT;
#define機制包括一個規定,允許把參數替換到文本中,這種實現通常稱為宏(macro
)或者定義宏(define macro
)。下面是宏的聲明方式:
#define name(parameter-list) stuff
其中,parameter-list(參數列表)是一個由逗號分隔的符號列表,它們可能出現在stuff 中。參數列表的左括號必須與name緊鄰。否則,參數列表就會被解釋為stuf的一部分。
當宏被調用時,名字后面是一個由逗號分隔的值的列表,每個值都與宏定義中的一個參數相對應。當參數出現在程序中時,與每個參數對應的實際值都將會被替換到stuff中。
例如:
#define SQUARE(x) x*x SQUARE(5)
當這兩條語句位于程序中時,預處理器就會用上面的表達式替換下面的表達式,就會變成:5 * 5。
但是上面這個宏存在一個問題,請大家觀察下面的代碼:
a = 5; printf("%d\n", SQUARE(a + 1));
可能我們直觀的覺得這段代碼將打印36這個值。但是事實上,它會打印11。Why?來,我們按照宏的規則做一個替換,這條語句將變成:
printf("%d\n", a + 1 * a + 1);
發現問題了嗎,這里由替換而產生的表達式并沒有按照預想的次序進行求值。所以,我們要對宏定義的參數加上括號,包括stuff整體。這樣就能產生我們預期的結果了。
在程序中擴展#define定義符號和宏時,需要涉及幾個步驟。
1.在調用宏時,首先對參數進行檢查,看看是否包含任何由#define定義的符號。如果存在,它們會首先被替換掉。
2.替換文本隨后被插入到程序中原來文本的位置。對于宏,參數名被它們的值所替代。
3.最后,才氣對結果文本進行掃描,看看它是否包含了任何由#define定義的符號。如果是的話,就重復上述處理過程。
宏非常頻繁的用于執行簡單的計算,比如在兩個表達式中尋找其中較大的一個(或較小):
#define MAX(a, b) ((a) > (b) ? (a) : (b))
這個功能好像我們也能用函數來實現,那為什么不使用函數呢?有兩個原因。首先,用于調用和從函數返回的代碼很可能比實際執行這個小型工作的代碼更大,所以使用宏要比使用函數在程序中的規模和速度都更勝一籌。
其次,更為重要的是,函數的參數必須聲明為一種特定的類型,所以它只能在類型合適的表達式中使用。但是宏是與類型無關的。
有優點就會有缺點,和使用函數相比,使用宏的不利之處在于每次使用宏時,一份宏定義代碼的副本都將會插入到程序中。除非宏非常短,否則使用宏可能會大幅度增加程序的長度。
還有一些任務無法用函數來實現,比如下面這個代碼:
#define MALLOC(n, type) ((type*) malloc ((n) * sizeof(type)))
type是一個數據類型,而函數是無法將類型作為參數傳遞的。
當宏參數在宏定義中出現的次數吵過一次時,如果這個參數具有副作用,那么在使用這個宏時就可能出現危險,導致不可預料的后果。副作用就是在表達式求值時出現永久性的后果。如下:
x + 1;
這個表達式無論執行幾百次都是一樣的,所以它沒有副作用。
x++;
但是這個表達式就不同了,每次執行都會改變x的值,每一次執行都是一個不同的結果。所以,這個表達式是具有副作用的。我們看下面的例子,你覺得它會打印出什么:
#define MAX(a, b) ((a) > (b) ? (a) : (b)) x = 5; y = 8; z = MAX(x++, y++); printf("x =%d, y = %d, z = %d\n", x, y, z);
其結果是: x = 6, y = 10, z = 9
。產生這個結果的原因是那個較小的值只增加了一次,而那個較大的值卻增加了兩次——第一次是在比較的時候,第二次是在執行?后面的表達式時。這就是一個具有副作用的宏參數,我們在使用的時候一定要注意。
通過一個表格來分析:
屬性 | #define宏 | 函數 |
---|---|---|
代碼長度 | 每次使用時,宏代碼都被插入到程序中。除了非常小的宏,程序的長度將大幅增長 | 函數代碼只出現在一個地方,每次使用函數時,都調用那個地方的同一份代碼 |
執行速度 | 更快 | 存在函數調用和返回的額外開銷 |
操作符優先級 | 宏參數的求值是在所有周圍表達式的上下文環境里,除非它們加上括號,否則臨近操作符的優先級可能會發生改變 | 函數參數指在函數調用時求值一次,其結果傳遞給函數,求值結果更容易預測 |
參數求值 | 參數每次用于宏定義時,它們都將重新求值。由于多次求值,具有副作用的參數可能會產生不可預料的后果 | 參數在函數被調用前只求值一次,在函數中多次使用參數并不會導致多個求職過程。參數的副作用并不會造成任何特殊的問題 |
參數類型 | 宏與類型無關。只要對參數的操作是合法的,它可以使用任何類型的參數 | 函數的參數是與類型有關的,如果參數的類型不同,就需要使用不同的函數,即使它們執行的任務是相同的 |
#undef這個預處理指令用于移除一個宏定義:
#undef name
如果現存的名字需要被重新定義,那么首先必須用#undef移除它的舊定義。
在編譯一個程序時,如果可以翻譯或忽略選定的某條語句或某組語句,會給我們帶來極大的便利。而條件編譯(conditional compilation
)可以實現這個目的。使用條件編譯,可以選擇代碼的一部分是被正常編譯還是完全忽略。用于支持條件編譯的基本結構是#if指令和#endif指令。一下是其使用方式:
#if constant-expression statements #endif
#if constant-expression statements #elif constant-expression other statements #else other statements #endif
同時,條件編譯的另一個用途是在編譯時選擇不同的代碼部分。所以#if指令還具有可選的#elif和#else子句。如下:
#if constant-expression statements #elif constant-expression other statements #else other statements #endif
#elif子句出現的次數可以不限。但是每個常量表達式(constant-expression
)只有當前面所有常量表達式的值都為假時才會被編譯。#else子句中的語句只有當前面所有常量表達式的值都為假時才會被編譯。
#include指令使另一個文件的內容被編譯,就好像它實際出現在#include指令出現的位置一樣。這種替換執行的方式很簡單:預處理器刪除這條指令,并用包含文件的內容取而代之。如果一個頭文件被包含到十個源文件中,那它實際上被編譯了十次。
C語言編譯器支持兩種不同類型的#include文件包含:函數庫文件和本地文件。事實上,他們之間的區別很小。
函數庫文件的包含使用以下語法:
#include<filename>
對于filename(文件名),并沒有任何的限制,不過根據規定,標準庫文件以一個.h后綴結尾。
編譯器通過觀察由編譯器定義的“一系列標準位置”查找函數庫頭文件。你所使用的編譯器文檔會說明這些標準的位置是什么,以及怎樣修改它們或者在列表中添加其他位置。
以下是#include指令的另外一種形式:
#include"filename"
標準允許編譯器自行決定是否把本地形式的#include和函數庫形式的#include區別對待。可以先對本地頭文件使用一種特殊的處理方式,如果失敗,編譯器再按照函數庫頭文件的處理方式對待它們進行處理。
處理本地頭文件的一種常見策略就是在源文件所在的當前目錄進行查找,如果該頭文件并未找到,編譯器就像查找函數庫頭文件一樣在標準位置查找本地頭文件。
關于“C語言如何實現預處理效果”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。