冷知識:預處理字元串操作符

以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」//mp.weixin.qq.com/s/Xr2pFCJ4j0DZYo2PO6-KQg


當年學習C語言的第一門課就提到過標記(Token)的概念,不過,相信在多年之後你再次聽到這個術語時會一臉懵逼,比如我。

因此特地翻了翻資料,整理下來這些筆記。

在C語言中什麼是標記?

標記是程式語言處理的基本單元,也叫最小劃分元素,比如關鍵字、操作符、變數名、函數名、字元串、數值等等。
下面舉例說明一下:

printf("hello world!");

對上面的語句進行標記劃分,可分為5個標記,如下:

printf              // 函數名
(                   // 左小括弧操作符
"hello world!"      // 字元串
)                   // 右小括弧操作符
;                   // 分號

預處理字元串操作符

在C語言中,預處理字元串操作符有兩個,###

# 字元串化操作符

用途是,將標記(Token)轉成字元串。

Syntax:

#define TOKEN_NAME(param) #param

Basic Usage:

#include <stdio.h>

#define MACRO_NAME(param)  #param

int main()
{
    printf(MACRO_NAME(hello world));

    return 0;
}

Output:

hello world

在項目實踐中,用宏定義的值的同時也需要將宏名轉成字元串使用,對日誌的輸出尤其管用。

Best Practice:

#include <stdio.h>

#define NAME(param)  #param

#define LEN_MAX     10

int main()
{
    int array[LEN_MAX] = {0};
    int index = 10;
    if (index >= LEN_MAX) {
        printf("error: %s:%d is over %s:%d\n", NAME(index), index, NAME(LEN_MAX), LEN_MAX);
    } else {
        printf("read %s[%d]=%d\n", NAME(array), index, array[index]);
    }

    return 0;
}

Output:

error: index:15 is over LEN_MAX:10

如果修改如下:

int index = 9;

Output:

read array[9]=0

## 標記(Token)連接操作符

用途是,將##前後的標記(Token)串接成新的單一標記。

syntax:

#define TOKEN_CONCATENATE(param1, param2) param1##param

Basic Usage:

#include <stdio.h>

#define TOKEN_CONCATENATE(param1, param2) param1##param2

int main()
{
    printf("%d\n", TOKEN_CONCATENATE(12, 34));

    return 0;
}

Output:

1234

通常,編碼實踐中,程式碼中會出現一些書寫看上去雷同的片段,極其啰嗦冗餘。為了壓縮源碼篇幅,可以參考程式碼生成器的思想,在預編譯階段用宏定義程式碼片段展開替換,同時根據輸入的參數用##組合各種標記。

假設有個需求是聲明定義一組同一類型的結構體的變數,並初始化其內部成員。既然聲明定義的這些變數屬於同一類型的結構體,那麼按照直接編碼的方式,就會有多次重複的程式碼片段出現,裡邊包括了聲明定義語句,以及初始化各個成員的語句,不同的只是變數名或者參數而已。

舉個栗子,下面基於同一類型的結構體,聲明定義兩個變數,並初始化,看程式碼

#include <stdio.h>
#include <string.h>

#define NAME(param)     #param

typedef struct {
    char *data;
    int   data_size;  /* number of byte real */
    int   max_size;   /* maximnm data size.*/
} my_type;

#define my_type_create(name, size) \
    char name ## _ ## data[size] = {0}; \
    my_type name; \
    memset(&name, 0x00, sizeof(name)); \
    name.data = name ## _ ## data; \
    name.max_size = size; \
    printf("variable name=%s\nmember data=%s, data_size=%d, max_size=%d\n", \
            NAME(name), NAME(name ## _ ## data), name.data_size, name.max_size); \

int main() {
    my_type_create(var1, 10)
    my_type_create(var2, 20)
}

上面的程式碼中,定義了宏my_type_create,內部實現了結構體變數的聲明定義,以及內部成員的初始化。如果按照直接編碼的方式,程式碼量相對於上面的程式碼量會虛增n-1倍,n=變數的個數。

在main函數中,調用宏的時候輸入參數var和10,那麼在編譯預處理階段,根據輸入的參數,宏my_type_create會展開為以下的程式碼段。

char var_data[10] = {0}; \
my_type var; \
memset(&var, 0x00, sizeof(var)); \
var.data = var_data; \
var.max_size = 10; \
printf("variable name=%s\nmember data=%s, data_size=%d, max_size=%d", \
        「var」, var_data, var.data_size, var.max_size); \

Output:

variable name=var1
member data=var1_data, data_size=0, max_size=10
variable name=var2
member data=var2_data, data_size=0, max_size=20

## 還有個特殊的用途

在宏定義中,也支援用...代表可變參數。

#define MY_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)

由於可變參數數目不確定,所以沒有具體的標記。於是為了引用可變參數,語言層面提供了可變宏(Variadic macros)__VA_ARGS__來引用它。

但是,在宏定義時,如果直接使用__VA_ARGS__來引用可變參數,一旦可變參數為空就會引起編譯器報錯,看看下面的例子

#include <stdio.h>

#define LOG_INFO(fmt, ...) printf("[I]" fmt "\n", __VA_ARGS__)

int main() {
  LOG_INFO("info...");
  LOG_INFO("%s, %s", "Hello", "world");
}

Output:

main.c: In function 『main』:
main.c:3:62: error: expected expression before 『)』 token
    3 | #define LOG_INFO(fmt, ...) printf("[I]" fmt "\n", __VA_ARGS__)
      |                                                              ^
main.c:6:3: note: in expansion of macro 『LOG_INFO』
    6 |   LOG_INFO("info...");
      |   ^~~~~~~~

為了解決上面的問題,在__VA_ARGS__前面添加上##,這樣的目的是告訴預處理器,如果可變參數為空,那麼前面緊跟者的逗號,在宏定義展開時會被清理掉。

Tags: