一文搞懂 C 語言 #、##、__VA_ARGS__

1#’ 和 ‘##’ 屬於預處理標記。‘#’ 和 ‘##’ 用於相似函數的宏定義中(或者簡稱爲宏定義函數)。
2‘__VA_ARGS__’ 是 C99 引入的用於支持宏定義函數中使用可變參數。

操做符 ‘#’

在宏定義展開的時候,標記 ‘#’ 用於將 ‘#’ 後面的宏定義函數中的參數轉化爲對應的字符串。宏定義函數的參數與預處理標記 ‘#’ 之間出現的每個空格都會被刪除,並刪除第一個預處理標記以前和最後一個預處理標記以後的空白字符,可是宏定義函數參數中的空格會保留。php

其中,空參數轉化爲爲空,即宏定義函數入參爲空,那麼展開的時候也爲空。html

上面的這段話比較難理解,這裏爲了準確地傳達其意義,咱們來看一個示例程序。nginx

在看到代碼後,能夠先猜猜可能的輸出結果,若是你答對了,那就是真的會了!
注意,這裏我基於 RT-Thread QEMU BSP 進行代碼展現,代碼真實編譯經過,運行正常。git

示例程序 A

請看如下代碼:github

 1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) (#var)
5
6int main(void)
7
{
8    rt_kprintf("hello rt-thread\n");
9
10    rt_kprintf(mkstr(hello rt-thread));
11
12    return 0;
13}

請問:express

  • 它能編譯經過嗎?api

  • 它能輸出什麼內容?bash

答案:app

  • 它能夠正常編譯經過ide

  • 它輸出的內容

    1hello rt-thread
    2hello rt-threadmsh />

從上面輸出的信息能夠看到,hello rt-thread 字符串被準確地輸出到了控制檯,可是沒有增長回車換行。其中 msh /> 字符串是 RT-Thread 控制檯回顯。

如上,代碼 rt_kprintf(mkstr(hello rt-thread)); 中的 hello rt-thread 在沒有加引號的狀況下,被轉化成了字符串。

示例程序 B

爲示例程序 A 打印的字符串增長回車換行。

 1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) (#var)
5
6int main(void)
7
{
8    rt_kprintf("hello rt-thread\n");
9
10    rt_kprintf(mkstr(hello rt-thread\r\n));
11
12    return 0;
13}

有了示例 A 的基礎,示例 B 那就是 soeasy,直接在原有的基礎上增長 \r\n 轉義字符便可輸出回車換行。

輸出結果以下:

1hello rt-thread
2hello rt-thread
3msh />

示例程序 C

咱們在示例程序 B 中成功增長了回車換行的輸出,可是你有沒有想過一個問題,若是你又不少地方用到 mkstr 宏定義函數輸出信息,那你是否是每個地方都要增長 \r\n,這豈不是很累,有沒有好的方法?

好方法固然有,下面介紹下程序中經常使用的方式,利用 C 語言相鄰字符串自動拼接的特性(固然,這是編譯器支持的)。代碼以下:

 1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) (#var"\r\n")
5
6int main(void)
7
{
8    rt_kprintf("hello rt-thread\n");
9
10    rt_kprintf(mkstr(hello rt-thread));
11
12    return 0;
13}

以上代碼在 #var 後面增長了一個字符串 \r\n,咱們來看宏定義展開過程:

1-> rt_kprintf(mkstr(hello rt-thread));
2-> rt_kprintf("hello rt-thread""\r\n");
3-> rt_kprintf("hello rt-thread\r\n");

示例程序 D

預處理標記 # 的基本用法已經展現完了,但怎麼理解 「宏定義函數的參數與預處理標記 ‘#’ 之間出現的每個空格都會被刪除,並刪除第一個預處理標記以前和最後一個預處理標記以後的空白字符」?

請看下面的代碼:

 1#include <stdint.h>
2#include <rtthread.h>
3
4#define mkstr(var) ("aa"  #  var  "bb")
5
6int main(void)
7
{
8    rt_kprintf("hello rt-thread\n");
9
10    rt_kprintf(mkstr(hello rt-thread));
11
12    return 0;
13}

宏定義 mkstr(var) ("aa"  #  var  "bb\r\n") 中的 #  var 中間有兩個空格,根據定義,# 號與宏定義函數參數 var 中間的兩個空格會被刪除,可是 var 參數中的 hello rt-thread 中的空格不會被刪除。

繼續,根據定義,宏定義 mkstr(var) ("aa"  #  var  "bb\r\n") 中只有一個預處理標記 #,其做爲第一個和最後一個預處理標記,它前面和後面的空格都會被刪除。

所以,以上代碼預計輸出結果爲:

1hello rt-thread
2aahello rt-threadbb
3msh />

示例程序 E

在實際操做時,操做符 # 被經常使用於枚舉轉字符串。如下代碼截取自個人 FlexibleButton 按鍵庫 的示例程序。

 1#define ENUM_TO_STR(e) (#e)
2
3typedef enum
4{
5    USER_BUTTON_0 = 0,
6    USER_BUTTON_1,
7    USER_BUTTON_2,
8    USER_BUTTON_3,
9    USER_BUTTON_MAX
10user_button_t;
11
12static char *enum_btn_id_string[] = {
13    ENUM_TO_STR(USER_BUTTON_0),
14    ENUM_TO_STR(USER_BUTTON_1),
15    ENUM_TO_STR(USER_BUTTON_2),
16    ENUM_TO_STR(USER_BUTTON_3),
17    ENUM_TO_STR(USER_BUTTON_MAX),
18};

操做符 ‘##’

‘##’ 是預處理拼接標記。在宏定義展開的時候,將 ‘##’ 左邊的內容,與 ‘##’ 右邊的內容拼接到一塊兒。

注意,對於任何一種形式的宏定義,‘##’ 預處理標記都不該出如今替換列表的開頭或結尾。

關於替換列表

例如宏定義 ‘#define aa(x, y) (x##y)’ 後面的部分 ‘x##y’ 就是替換列表。

預處理拼接符 ## 經常使用於使用宏定義批量生成函數或者變量。

示例程序 F

 1#include <stdint.h>
2#include <rtthread.h>
3
4#define my_math(x, y) (x##e##y)
5
6int main(void)
7
{
8    rt_kprintf("hello rt-thread\n");
9
10    printf("%e\r\n", my_math(34));
11
12    return 0;
13}

以上代碼是科學計數法的格式輸出到控制檯,輸出內容以下:

1hello rt-thread
23.000000e+04
3msh />

示例程序 G

用預處理拼接符 ## 批量生成函數或者變量。

如下代碼截取自 RT-Thread finsh_api.h,該段代碼用於導出 Finsh 命令,代碼以下所示:

1#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc)                      \
2    const char __fsym_##cmd##_name[] SECTION(".rodata.name") = #cmd;    \
3    const char __fsym_##cmd##_desc[] SECTION(".rodata.name") = #desc;   \
4    RT_USED const struct finsh_syscall __fsym_##cmd SECTION("FSymTab")= \
5    {                           \
6        __fsym_##cmd##_name,    \
7        __fsym_##cmd##_desc,    \
8        (syscall_func)&name     \
9    };

該宏定義的應用,如 list_timer 命令,以下所示:

1FINSH_FUNCTION_EXPORT_CMD(list_timer, list_timer, list timer in system);

標識符 `VA_ARGS`

__VA_ARGS__ 是在 C99 中增長的新特性。雖然 C89 引入了一種標準機制,容許定義具備可變數量參數的函數,可是 C89 中不容許這種定義可變數量參數的方式出如今宏定義中。C99 中加入了 __VA_ARGS__ 關鍵字,用於支持在宏定義中定義可變數量參數,用於接收 ... 傳遞的多個參數。1

__VA_ARGS__ 只能出如今使用了省略號的像函數同樣的宏定義裏。例如 #define myprintf(...) fprintf(stderr, __VA_ARGS__)

解析不定參

經過宏定義,將多個參數傳遞給函數,那麼函數是如何解析不定參的呢?

這就須要使用標準庫頭文件 <stdarg.h> 中的三個宏,分別是 「va_start()」、「va_arg()」、「va_end()」,以及一個可變參類型 「va_list」,示例使用方式借用 RT-Thread 中的 rt_sprintf 的實現,代碼以下所示:

 1rt_int32_t rt_sprintf(char *buf, const char *format, ...)
2{
3    rt_int32_t n;
4    va_list arg_ptr;
5
6    va_start(arg_ptr, format);
7    n = rt_vsprintf(buf, format, arg_ptr);
8    va_end(arg_ptr);
9
10    return n;
11}
  • 首先使用 「va_list」 類型定義一個變量 「arg_ptr」

  • 而後調用 「va_start(arg_ptr, format);」 函數,第一個入參是 「va_list」 類型,第二個參數是 「rt_sprintf」 函數參數列表中的最後一個定參 「format」

  • 而後,經過調用 「rt_vsprintf」 函數,根據 「format」 來解析不定參,並將結果存放到 「buf」 中

  • 最後,使用 「va_end(arg_ptr);」 來釋放不定參列表佔用的資源

帶 ‘#’ 的標識符 `#VA_ARGS`

預處理標記 ‘#’ 用於將宏定義參數轉化爲字符串,所以 #__VA_ARGS__ 會被展開爲參數列表對應的字符串。

示例:

1#define showlist(...) put(#__VA_ARGS__)
2
3測試以下:
4showlist(The first, second, and third items.);
5showlist(arg1, arg2, arg3);
6
7輸出結果分別爲:
8The first, second, and third items.
9arg1, arg2, arg3

帶 ‘##’ 的標識符 `##VA_ARGS`

##__VA_ARGS__ 是 GNU 特性,不是 C99 標準的一部分,C 標準不建議這樣使用,但目前已經被大部分編譯器支持。

標識符 ##__VA_ARGS__ 的意義來自 ‘##’,主要爲了解決一下應用場景:

1#define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__)
2#define myprintf_b(fmt, ...) printf(fmt, ##__VA_ARGS__)
3
4應用:
5myprintf_a("hello");
6myprintf_b("hello");
7
8myprintf_a("hello: %s""world");
9myprintf_b("hello: %s""world");

這個時候,編譯器會報錯,以下所示:

1applications\main.c: In function 'main':
2applications\main.c:26:57error: expected expression before ')' token
3 #define myprintf_a(fmt, ...) printf(fmt, __VA_ARGS__)
4                                                         ^
5applications\main.c:36:5: note: in expansion of macro 'myprintf_a'
6     myprintf_a("hello");

爲何呢?

咱們展開 myprintf_a("hello"); 以後爲 printf("hello",)。由於沒有不定參,因此,__VA_ARGS__ 展開爲空白字符,這個時候,printf 函數中就多了一個 ‘,’(逗號),致使編譯報錯。而 ##__VA_ARGS__ 在展開的時候,由於 ‘##’ 找不到鏈接對象,會將 ‘##’ 以前的空白字符和 ‘,’(逗號)刪除,這個時候 printf 函數就沒有了多餘的 ‘,’(逗號)。

參考

相關文章
相關標籤/搜索