[Linux Kernel慢慢學]define macro with hashtag("#" and "##")

Posted by John on 2020-10-27
Words 1.2k and Reading Time 4 Minutes
Viewed Times

前言

這篇的標題定的很…白話(?) 主要是在看Linux kernel code的時候有時候常常會看到一堆define,有些define還會搭配井字號的寫法還會讓你很看不懂,所以重新寫一個小短篇順便幫自己複習一下。

然後井字號這種特殊符號又很難去search,所以標題就決定也這樣下,不知道未來遇到同樣問題的人搜尋起來會不會比較容易xD

來個小測驗,如果你覺得你夠懂#define了,下面這段Linux kernel code你看得懂嗎?

/* include/linux/init.h. line=172 */
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;

比較陌生的應該是跟井字號的用法(# 和 ##),他們其實被稱作Preprocessor operators

所以這篇一開始會快速介紹define macro,接下來開始介紹和Preprocessor operators的搭配,對這部分有興趣的可以直接看最後一Part。

1分鐘學會#define macro

define macro的用法很簡單,快速舉個例子:

#define MAX_LEVEL 100

int main() {
printf("%d\n", MAX_LEVEL);
return 0;
}

Compiler在編譯時期就會把這個MAX_LEVEL直接替換成後面的100,就4這麼簡單。

define常被用來定義一些常量,或是一些config。例如你的Code有個部分會引用到版本號,那每次版本號一變動所有相關的code都要跟著改很麻煩,此時你可以#define VERSION 1.1,然後把code都用VERSION來替代,這樣未來如果版本變了只需要改一次define的值就好。

  • 此外,也可以透過define macro來節省一些function call,具體可以在後面的介紹觀察到

最後,為了和function, variable區分,define macro通常都會使用大寫

define不只能用在常數

define也可以用在function,例如:

#define ADD(a, b) a + b // wrong way of define

但要注意,上面這兩種寫法是錯誤的。因為define只是將後面的東西替換你的add(a, b),所以如果你在呼叫的時候這樣寫:

#define ADD(a, b) a + b; // wrong way of define
int main(){
int c = ADD(1, 2); // exec correct, c = 3
int d = ADD(1, 2) * 0; // wrong output, d = 1
}

可以發現d的值跟預想的不同,因為ADD(1, 2)只是被替換成1+2,並不是實際去執行了,所以d的assign右半邊實際上是1+2*0,也就得到了1的output。

解決方法是在define前後加上括號,確保define被替換的內容也能正確地執行

#define ADD(a, b) (a + b)

所以define macro使用在function或condition(當然也可以拿來寫if else)的時候要很小心。

再看另一個例子:

#include <cstdio>
#include <iostream>
#define SWAP(a, b) { \
a ^= b; \
b ^= a; \
a ^= b; \
}
void swap(int a, int b) {
a ^= b;
b ^= a;
a ^= b;
}
int main() {
int x = 1, y = 2;
std::cout << x << y << std::endl; // 12
SWAP(x, y);
std::cout << x << y << std::endl; // 21
swap(x, y);
std::cout << x << y << std::endl; // 21
return 0;
}

會發現SWAP成功的將x y調換了,但swap()卻沒有,為什麼呢?

  • 因為swap()是call by value
  • 而SWAP是define macro,實際上compiler是把那三行替換main()中而不是呼叫一個function
    • 有注意到我一值強調替換嗎? 這是define macro很重要的行為要分辨清楚以後才不會出錯

然後有發現這個例子中define變成了多行的敘述了嗎? 沒錯,define當然可以分成多行來撰寫。

define也可以寫多行

如果define的敘述太長怎麼辦,一般建議程式碼一行不要超過79個字元,所以有時候有需要分行的需求,此時就可以這樣寫:

#define SWAP_XOR(a, b) {		\ 
a ^= b; \
b ^= a; \
a ^= b; \
}

透過用斜線的方式來寫多行的define macro。

當#define碰上井字號

這些符號稱之Preprocessor operators,共有三種。下面快速總結,淺顯易懂:

  • Stringizing operator (#): 用來將引數變成一個字串
  • Charizing operator (#@): 用來將引數變成一個字元
  • Token-pasting operator (##): 用來拼接引數

然後再搭配個簡單的例子就知道了:

#define MAKESTR(s) #s
#define MAKECHAR(x) #@x
#define CONS(a,b) int(a##e##b)

int main()
{
a = MAKECHAR(b); // a = 'b';
b = MAKESTR(hello) // b = "hello";
c = CONS(2,3); // c = 2e3;
return 0;
}

了解了之後,最後回頭看我們在前言提到的那段kernel code:

/* include/linux/init.h. line=172 */
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;

如果fn=module, id=3的話,那__define_initcall(fn, id)就會變成

static initcall_t __initcall_module3 __used \
__attribute__((__section__(".initcall" "6" ".init"))) = fn;

你答對了嗎?

References


>