語法書 / AA 競程語法書 下冊 / 第十二單元 / #define 與巨集(延伸知識)

12.9 #define 與巨集(延伸知識)

高手程式碼開頭常有一排 #define。要看懂它,先認識一個幕後角色:前置處理器(preprocessor)——所有 # 開頭的指令都歸它管,在編譯開始之前先對整份程式碼做文字加工。你其實從第一支程式就在用它:#include 就是前置處理器指令,做的事是「把標頭檔的內容貼進來」。

#define 是另一個前置處理器指令,做的事更直接:文本取代

#define PB push_back

這行之後,程式裡每個獨立出現的 PB 都會在編譯前被換成 push_back——v.PB(3); 編譯器實際看到的是 v.push_back(3);。被定義出來的東西叫巨集(macro)

像函式的巨集

#define 也能帶參數,長得像函式:

#define SZ(x) (int)((x).size())

之後 SZ(v) 一律被換成 (int)((v).size())——把 10.4 的「size() 一到手就轉 int」自動化,這是巨集在競程少數公認的正經用途:

#include <bits/stdc++.h>
using namespace std;

#define SZ(x) (int)((x).size())

int main() {
    vector<int> v;                          // 空的
    cout << SZ(v) - 1 << '\n';              // -1:不會環繞成天文數字
    for (int i = 0; i <= SZ(v) - 1; i++) {  // 迴圈正常不執行
        cout << v[i] << '\n';
    }
    cout << "safe" << '\n';
    return 0;
}

執行結果:

-1
safe

再強調一次:巨集不是函式。函式是執行期呼叫的一段程式;巨集在編譯前就被展開成文字,之後編譯器看到的程式裡根本沒有 SZ 這個名字。這個差異正是下面陷阱的根源。

另一個公認好用的:all(x)

競程模板裡最常出現的巨集是這一個:

#define all(x) (x).begin(), (x).end()

之後 sort(all(v)) 展開成 sort((v).begin(), (v).end())——把「開頭到結尾」這組每天要打好幾次的東西縮成三個字元。它也提醒你巨集到底有多「不動腦」:連中間那個逗號都是被貼進去的文字,所以一個 all(v) 可以變成函式的兩個參數,函式辦不到這種事。

#include <bits/stdc++.h>
using namespace std;

#define all(x) (x).begin(), (x).end()

int main() {
    vector<int> v = {5, 4, 6};
    sort(all(v));
    for (int x : v) cout << x << ' ';
    cout << '\n';
    return 0;
}

執行結果:

4 5 6

(參數兩邊照樣包括號,理由就是上面那個陷阱。)

#define int long long——看得懂,但知道代價

比賽程式碼裡偶爾會看到這行狠活:

#define int long long

整份程式的 int 在編譯前全部變成 long long——上冊 2.9 的溢位焦慮一行根治。代價:所有 int 佔的記憶體翻倍、運算可能變慢,而且 main 的回傳型態也被換掉了,得改寫成 signed main() 才能編譯。它是「懶得逐一檢查哪個變數會爆」的保險絲,看懂即可——本書的建議還是上冊 2.9 那句:該用 long long 的地方,自己想清楚。

順帶一提,前置處理器還有 #ifdef(判斷某個名字是否被 define 過)這類條件指令,搭配編譯參數 -D(在指令列補一個 define),同一份程式碼就能在不同環境有不同行為——例如在自己電腦上多印除錯資訊、交到 OJ 上自動安靜。這套技巧在處理檔案輸入輸出時特別好用,屬於工程配備,先知道有這回事就好。