#:前置處理器的語言

前言

前處理器 (preprocessor) 會在 compilation 階段之前執行。作為編譯之前的前處理,preprocessor 可以完成幾種類型的任務,包含條件式的編譯,比如常見的 #if#ifndef 等指令,檔案引入、巨集定義等等。在這篇文章中,我們將會一一來看這幾種前置處理的功能。

前置處理的語言

要與 preprocessor 溝通,我們首先就得了解它的語言。一個標準的 preprocessor 語言包含了以下的三個部分所組成,分別是:

  1. # 字元
  2. 一系列標準定義的命令或是可由使用者定義的前置處理命令
  3. 換行

也就是說,反過來看開頭為 # 的程式碼片段,就是給 preprocessor 看的啦!

preprocessor:不是 # 開頭的我可是不認得哦!

那麼,對於 preprocessor 來說,有幾種類型的語言呢?在文件 1 中清楚的列出了幾個類別,包含

  1. Conditional inclusion 2
  2. Replacing text macros 3
  3. Source file inclusion 4
  4. Diagnostic directive 5
  5. Implementation defined behavior control 6
  6. Filename and line information 7

這六種類型。

Conditional inclusion

條件式前處理是由 #if#ifdef#ifndef 等指令開始,之後可以包含任意數量的 #elif#elifdef#elifndef指令,以及一個 #else指令。最後,條件式前處理由 #endif 終止。在條件是前處理的區塊中,這些區塊會被分開的處理。

舉例來說,以下是一個條件式處理的例子。我們在第一行先定義了 ABCD,並將所有 ABCD 處取代為 2。在第五行時,我們便判斷 ABCD 是否定義,若是則印出 yes,若否,則印出 no。

 1#define ABCD 2
 2#include <iostream>
 3
 4int main () {
 5    #ifdef ABCD
 6    std::cout << "1: yes\n" << std:endl;
 7    #else
 8    std::cout << "1: no\n" << std::endl;
 9    #endif
10}

Replacing text macros

1// Object-like macros
2#define identifier replacement-list 
3// Function-like macros
4#define identifier(parameters, ...) replacement-list

取代文字的巨集。在這個類型中,我們會以 #define 作為開頭來定義巨集。一旦我們透過 #define 定義後,compiler 會將所有程式碼中出現的 identifier 取代為 replacement-list 中的內容。好比說我們在前一小節 conditional inclusion 中看到的 #define ABCD 2 即是這個類型的巨集定義。 再更精準的來看這個類型的指令,可以細分成兩個不同的巨集形式。其一是 Object-like macros 而另一個則是 Function-like macros。

Source file inclusion

1#include <h-chr-sequence> new-line
2#include "q-char-sequence" new-line 

這是我們常見的引入標頭檔的方式,好比說 #include <stdio.h> 指的就是將該指令所在的位置取代為 stdio.h 檔案中的內容。

Diagnostic directive

1#error diagnostic-message
2#warning diagnostic-message

Implementation defined behavior control

1#pragma prama-params

Pragma 指令控制了一些 compiler 實作相關的行為,比方說關閉 warnings 的提示。ISO C language standard 8 中並沒有規定 pragma 的實作,但仍有幾個常見的 pragma 實作諸如 #pragma STDC#pragma unpack#pragma once等。

pragma once

現今多數的 compiler 都支援此功能 9。假如我們在一個 header 檔案中看到這樣的指示, 代表儘管該 header 可能會在多個檔案中被引用,但這份檔案在編譯過程中只會被解析一次。這個分法與一般標準的 include guards 的作法相同,都是為了避免對相同的 header 進行多次引入。 但要注意的是,透過 pragma once 來避免重複引入可能會遇到以下幾個問題。首先若使用 pragma once 指令,對於多個檔案中定義的相同名字的 macros 是無法辨認的。再者,由於 pragma once 無法檢查系統層級的引入,只能檢查專案內的檔案,因此若是使用到了與存在系統標頭檔案中相同的 header 亦無法避免重複引入。

pragma pack

1#pragma pack(arg)
2#pragma pack()
3#pragma pack(push)
4#pragma pack(push, arg)
5#pragma pack(pop)

這一系列的指令是用來設定使用者定義之連續記憶體資料結構的 alignment。透過設定 arg 我們可以決定以 bytes 為單位的 alignment 大小。

Filename and line information

1#line lineno
2#line lineno "filename"

用來修改當前 preprocessor 的檔案名稱 (FILE) 和行數 (LINE)。

 1#include <cassert>
 2#define FNAME "test.cc"
 3int main()
 4{
 5#line 777 FNAME
 6        assert(2+2 == 5);
 7}
 8
 9// Output
10// test: test.cc:777: int main(): Assertion `2+2 == 5' failed.

小結

在這篇文章中,我們看了前置處理的幾個指令型態。其中常見的有 File inclusion、Replacing text macros 與 Conditional inclusion ⋯⋯等等。透過這些前置處理的方式,我們可以更有效、更彈性的來撰寫程式,並在編譯階段前對程式碼進行轉譯。

參考資料