首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C Primer Plus 第 16 章:C 預處理器與 C 庫深度解析

     編輯:關於C語言

第十六章 C預處理器和C庫

16.1 翻譯程序的第一步:預處理前的準備工作

在 C 程式進入預處理階段之前,編譯器會先執行一系列翻譯步驟。首先,編譯器將原始碼中的字符映射到原始字符集。接著,它會尋找反斜線(\)後緊跟換行符的情況並將其刪除,這樣就能將跨越多個物理行的程式碼合併為一個邏輯行。例如:

printf("That's wond\
erful!\n");

會被轉換為:

printf("That's wonderful!\n");

隨後,編譯器將文本劃分為預處理語言符號序列、空白字符序列與註釋序列,並用一個空格字符取代每一個註釋。完成這些步驟後,程式才正式進入預處理階段,預處理器開始尋找以 # 符號開頭的預處理指令。

16.2 明顯常量:#define 指令詳解

#define 指令以 # 符號開頭,可出現在原始檔的任何位置,其作用域從定義處開始直到檔案結尾。每個 #define 行包含三部分:指令本身、宏名稱(遵循 C 變量命名規則,不含空格)以及替換列表(主體)。當預處理器在程式中發現宏時,會用替換文本進行替換,這個過程稱為「宏展開」。

16.2.1 語言符號與字符串的區別

預處理器將宏主體視為語言符號類型字符串,而非單純字符串。例如 #define FOUR 2*2 只有一個語言符號 2*2;而 #define SIX 2 * 3 則有三個語言符號:2、*、3。當作語言符號處理時,多餘空格會被忽略,最終以單個空格分隔各語言符號進行替換。

16.2.2 重定義常量與 #undef

若同一個宏被定義多次,且其語言符號序列完全相同(忽略多餘空格),則視為相同定義;否則屬於重定義,會引發警告。可使用 #undef 取消宏定義,之後方可重新定義。

16.3 帶參數的宏:類函數宏

透過參數可建立外觀與行為類似函數的「類函數宏」。例如 #define SQUARE(X) X*X。但需注意:預處理器僅做字符串替換,不進行運算。若呼叫 SQUARE(x+2),會展開為 x+2*x+2 而非 (x+2)*(x+2)。解決方法是在宏定義中加上括號:#define SQUARE(X) ((X)*(X))。

16.3.1 字符串化運算符 #

若希望在字符串中包含宏參數,可使用 # 運算符將參數名轉為對應字符串。例如:

#define PR(X) printf("The square of " #X " is %d.\n", (X)*(X))

呼叫 PR(y) 會展開為 printf("The square of " "y" " is %d.\n", (y)*(y)),經字符串連接後輸出 The square of y is 25.。

16.3.2 符號粘合運算符 ##

## 運算符可將兩個語言符號合併為一個。例如 #define XNAME(n) x ## n,呼叫 XNAME(4) 會展開為 x4。此運算符亦可用於類對象宏。

16.3.3 可變參數宏:__VA_ARGS__

C99 引入 ... 與 __VA_ARGS__ 實現可變參數宏。例如:

#define PR(...) printf(__VA_ARGS__)
PR("Hello");          // 展開為 printf("Hello");
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
PR(1, "x=%g\n", x);   // 輸出 Message 1: x=48

16.4 宏與函數的選擇:時間與空間的權衡

宏產生內聯代碼,每次使用都會插入代碼,增加程式體積但省去函數呼叫開銷;函數僅有一份代碼拷貝,節省空間但涉及控制轉移,耗時較多。宏不檢查參數類型,靈活但易產生難以預期的結果。

16.5 文件包含:#include

#include 指令會將指定檔案的內容插入到當前檔案中。慣例使用 .h 後綴表示標頭檔,內容通常為編譯所需的聲明資訊,而非實際可執行語句,因此包含大型標頭檔不一定會顯著增加最終程式大小。標頭檔也常用於聲明多檔案共享的外部變量。

16.6 其他預處理指令

16.6.1 #undef:取消宏定義

#undef LIMIT 可取消 LIMIT 的定義,即使該宏未定義也可安全使用。

16.6.2 已定義的判斷

預處理器視角下,唯有透過 #define 定義且未被 #undef 取消的標識符才算「已定義」。普通 C 變量(如 int q;)在預處理器眼中屬於「未定義」。

16.6.3 條件編譯

  • #ifdef/#else/#endif:根據宏是否定義編譯不同代碼區塊。
  • #ifndef:#ifdef 的反向指令,常用於防止標頭檔重複包含(Include Guard)。
  • #if/#elif:後接常量整數表達式,功能更強大。現代寫法偏好 #if defined(MACRO) 而非 #ifdef MACRO。

16.7 內聯函數:C99 的新選擇

內聯函數使用 inline 說明符聲明,編譯器會嘗試將函數本體內嵌到呼叫點,結合宏的速度優勢與函數的類型檢查優勢。內聯函數適用於短小函數;定義與呼叫需在同一檔案,C 標準放寬了對內聯函數重複定義的限制。

16.8 C 庫的訪問方式

  • 自動訪問:許多系統編譯時自動鏈接常用庫函數,只需包含對應標頭檔聲明類型。
  • 文件包含:若函數以宏形式定義,透過 #include 引入定義檔案。
  • 庫鏈接:透過編譯器/鏈接器選項指定庫檔搜索路徑與名稱。

16.9 通用工具庫:

16.9.1 exit() 與 atexit()

atexit() 註冊程式結束時自動呼叫的函數(類型為 void func(void)),按後進先出(LIFO)順序執行。exit() 執行清理工作:刷新輸出流、關閉打開的流等。

16.10 診斷庫:

assert(expr) 宏用於調試:若表達式為假,向標準錯誤輸出錯誤訊息並呼叫 abort() 終止程式。定義 NDEBUG 宏可禁用所有斷言。

16.11 中的 memcpy() 與 memmove()

兩者原型均為 void *func(void *s1, const void *s2, size_t n),功能都是從 s2 複製 n 位元組到 s1 並返回 s1。關鍵差異在於 memcpy() 要求來源與目標記憶體區域不重疊,而 memmove() 允許重疊並正確處理。第三個參數常寫作 num * sizeof(int) 以增強可讀性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved