宏(Macro)本質上就是代碼片段,通過別名來使用。在編譯前的預處理中,宏會被替換為真實所指代的代碼片段,即下圖中 Preprocessor 處理的部分。
C/C++ 代碼編譯過程 - 圖片來自 ntu.edu.sg
根據用法的不同,分兩種,Object-like 和 Function-like。前者用於 Object 對象,后者用於函數方法。
C/C++ 代碼編譯過程中,可通過相應參數來獲取到各編譯步驟中的產出,比如想看被預處理編譯之后的宏,使用 gcc
使加上 -E
參數。
$ gcc -E macro.c
宏的定義
通過 #define
指令定義一個宏。
#define NAME_OF_MACRO value
比如,以下代碼定義了一個名為 BUFFER_SIZE
的宏,指代 1024
這個數字。
#define BUFFER_SIZE 1024
使用時,
foo = (char *) malloc (BUFFER_SIZE);
使用預處理器編譯:
$ gcc -E test.c
編譯結果:
foo = (char *) malloc (1024);
多行
宏的定義是跟隨 #define
在一同一行內的,但可通過 反斜杠 \
實現換行從而定義出多行的宏。
#include <stdio.h>
#define GREETING_STR \
"hello \
world"
int main() {
printf(GREETING_STR);
return 0;
}
多行的宏經過編譯后會還原到一行中。
test.c
#include <stdio.h>
#define GREETING_STR \
"hello \
world"
int main() { printf(GREETING_STR); }
編譯后:
int main() {
printf("hello world");
return 0;
}
宏展開時的順序
宏的展開是在處理源碼時按照其出現位置進行的,如果宏定義有嵌套關系,也是層層進行展開,比如:
#include <stdio.h>
define GREETING_NAME "wayou"
define GREETING "hello," GREETING_NAME
int main() {
printf(GREETING);
return 0;
}
首先遇到 GREETING
,將其展開成 GREETING_NAME "wayou"
,然后發現另一個宏 GREETING_NAME
,將其展開最后得到 "hello," "wayou"
。所以編譯后的代碼為:
int main() {
printf("hello," "wayou");
return 0;
}
其展開的順序並不是宏定義時的順序,為了驗證,可將上面示例代碼中兩個宏的定義調換一下,得到:
-#define GREETING_NAME "wayou"
#define GREETING "hello," GREETING_NAME
+#define GREETING_NAME "wayou"
再次編譯查看產出,會發現沒有區別,也不會報 GREETING
中所依賴的 GREETING_NAME
找不到的錯。其實 #define
只是告訴編譯器定義了這么個宏,而具體的求值,則是使用宏的地方才開始的。
像下面這樣,當宏存在覆蓋時,會以新的為准,其結果為 37。
#define BUFSIZE 1020
#define TABLESIZE BUFSIZE
#undef BUFSIZE
#define BUFSIZE 37
Object-like 宏
Object-like 類型的宏看起來就像普通的數據對象,故名。多用於數字常量的情形下。且宏名一般使用全大寫形式方便識別。像上面示例中,都是 Object-like 的。
Function-like 宏
也可定義出使用時像是方法調用一樣的宏,這便是 Function-like 類型的宏。
#define lang_init() c_init()
lang_init()
// 編譯后
c_init()
函數類型的宏只在以方法調用形式使用時才會被展開,即名稱后加括號,否則會被忽略。當宏名和函數名重名時,這一策略就會顯得有用了,比如:
extern void foo(void);
#define foo() /* optimized inline version */
…
foo();
funcptr = foo;
這里 foo()
的調用會來自宏里面定義的那個函數,而 funcptr
會正確地指向函數地址,如果后者也被宏展開,則成了 funptr=foo()
顯然就不對了。
函數類型的宏在定義時需注意,宏名與后面括號不能有空格,否則就是普通的 Object-like 類型對象。
#define lang_init () c_init()
lang_init()
// 編譯后:
() c_init()()
宏的參數
函數類型的宏,可以像正常函數一樣指定入參,入參需為逗號分隔合法的 C 字面量。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
x = min(a, b); → x = ((a) < (b) ? (a) : (b));
y = min(1, 2); → y = ((1) < (2) ? (1) : (2));
z = min(a + 28, *p); → z = ((a + 28) < (*p) ? (a + 28) : (*p));
入參中的括號
入參中只需要括號對稱,但不要求方括號或花括號成對出現,所以下面的代碼:
macro (array[x = y, x + 1])
其入參實際為 array[x = y
和 x + 1]
。
入參的展開
入參本質上也是宏,對象類型的宏,在函數宏展示時,這些參數也被展示到了函數宏的函數體里。
min (min (a, b), c)
首先被展開成:
min (((a) < (b) ? (a) : (b)), (c))
然后進一步展開成(此處換行為方便閱讀,實際編譯后沒有):
((((a) < (b) ? (a) : (b))) < (c)
? (((a) < (b) ? (a) : (b)))
: (c))
參數的缺省
函數宏在使用時其入參可缺省,但不能全部缺省,至少提供一個入參。
min(, b) → (( ) < (b) ? ( ) : (b))
min(a, ) → ((a ) < ( ) ? (a ) : ( ))
min(,) → (( ) < ( ) ? ( ) : ( ))
min((,),) → (((,)) < ( ) ? ((,)) : ( ))
min() error→ macro "min" requires 2 arguments, but only 1 given
min(,,) error→ macro "min" passed 3 arguments, but takes just 2
字符化/Stringizing
如果函數宏中入參在字符串中,是不會被展開的,它就是普通的字符串字面量,這樣的結果是符合預期的。
#define foo(x) x, "x"
foo(bar) → bar, "x"
但如果確實想將入參展開成字符串,可在使用入參時,加上 #
前綴。
#define WARN_IF(EXP) \
do { if (EXP) \
fprintf (stderr, "Warning: " #EXP "\n"); } \
while (0)
WARN_IF (x == 0);
→ do { if (x == 0)
fprintf (stderr, "Warning: " "x == 0" "\n"); } while (0);
此處 #EXP
在字符串中會被正確展開。What's more, 如果這里的 x
也是宏,那只會在 if
語句中進行展開。
拼接
通過 ##
可將兩個宏展開成一個,即將兩者進行了拼接,這種操作叫 "token pasting",或 "token concatenation",就是拼接嘛。
宏拼接一般用在需要拼接的宏是來自宏參數的情況,其他情況,大可直接將兩個宏寫在一起即可,用不着 ##
指令。
考察下面這個場景,其中命令名重復出現:
struct command
{
char *name;
void (*function) (void);
};
struct command commands[] =
{
{ "quit", quit_command },
{ "help", help_command },
…
};
通過定義宏配合拼接,可達到精簡代碼的目的:
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command commands[] =
{
COMMAND (quit),
COMMAND (help),
…
};
不定參數
像普通函數一樣,函數類型的宏也可定義接收不定參數。
#define eprintf(…) fprintf (stderr, __VA_ARGS__)
調用時,命名參數后面,包括逗號都會進入到 __VA_ARGS__
關鍵字當中。但 C++ 中還支持對這些參數命名從而不用 __VA_ARGS__
。
eprintf ("%s:%d: ", input_file, lineno)
// 編譯后:
fprintf (stderr, "%s:%d: ", input_file, lineno)
C++ 中可這么寫:
#define eprintf(args…) fprintf (stderr, args)
不定參數與命名參數混合的情況
不定參數為命名參數后面省略的部分。
#define eprintf(format, …) fprintf (stderr, format, __VA_ARGS__)
預設的宏
標准庫及編譯器中預設了一些有用的宏,可以在這里 查閱。
取消和重置宏
當某個宏不再使用時,可通過 #undef
將取注銷掉。#undef
后緊跟宏名,后面不要跟其他東西,即使是函數類型的宏。
#define FOO 4
x = FOO; → x = 4;
#undef FOO
x = FOO; → x = FOO;
兩個宏相似的定義
滿足以下條件時,我們認為兩者是相似的:
- 類型相同,比如同為對象類型,或函數類型的宏
- 展開后各位置的符號(token)相同
- 如果是函數宏,入參相同
- 空白的不限但出現的位置相同
比如,下面這些是相似的:
#define FOUR (2 + 2)
#define FOUR (2 + 2)
#define FOUR (2 /* two */ + 2)
而下面這些則不然:
#define FOUR (2 + 2)
#define FOUR ( 2+2 ) // 空白位置不一樣
#define FOUR (2 * 2) // 宏的內容不一樣
#define FOUR(score,and,seven,years,ago) (2 + 2) // 入參不一樣
宏重復定義時的表現
對於使用了 #undef
注銷過的宏,再次定義同名的宏時,要求新定義的宏不與老的相似。
而如果說一個已經存在的宏,並沒有注銷,重復定義時,如果相似,則新的定義會忽略,如果不相似,編譯器會報警告同時使用新定義的宏。這允許在多個文件中定義同一個宏。