之前对 C 语言中宏定义的认知十分简单,包括但不限于停留在以下浅薄的层面:
1 2
   | #define PI 3.14 #define add(a, b) a + b
   | 
 
上述代码完全是大学课本中的用法。但当我看到实际项目中宏的用法后完全是一头雾水,所以自己也要写出那种高逼格让别人看不太懂的代码。宏远远比我想象的要强大,所以本文为每个宏技巧都配备了一个实用场景。
- 字符串化操作符,实现一个简单的自动化测试样例
 
- 字符串连接,实现一个具备计时功能的宏
 
- X 宏,实现根据输入执行不同的函数
 
- 特殊宏 
__VA_ARGS__,实现一个简单的日志函数 
字符串化操作符
1 2 3 4 5 6 7 8
   | #include <iostream>
  #define str(a) #a
  int main() {     std::cout << str(FUNC);        return 0; }
   | 
 
上述宏 str 通过单井号的形式实现了字符串化操作符,将传入的参数字符串化。
简单测试框架
C 语言有一些预定义的宏,比如 __LINE__ 表示当前行号,__FILE__ 表示当前的文件名。基于这一基础,我们实现一个简单的测试程序。在测试程序时,打印测试用例、文件名、行号、以及是否通过测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   | #include <stdio.h>
  #define LOG_INFO(format) printf(format)
  #define __TO_STR__(x) #x ":" #define __TO_REAL__(x) __TO_STR__(x)
  #define __FILE_LINE__ __FILE__ ":" __TO_REAL__(__LINE__)
  #define CHECK_VAL(val) \     do { \         LOG_INFO(__FILE_LINE__ ":calling " #val "\n"); \         if (0 == (val)) { \             LOG_INFO(__FILE_LINE__ ":error \n"); \             goto fail; \         } else { \             LOG_INFO(__FILE_LINE__ ":passed \n"); \         } \     } while(0)
  int test_func() {     return 1; }
  int main() {
      int n_total = 2;     int n_passed = 0;
      CHECK_VAL(1 == test_func());     n_passed ++;
      CHECK_VAL(2 == test_func());     n_passed ++;
  fail:
      printf("################ summary ###################\n");     printf("passed: %d\n", n_passed);     printf("total: %d\n", n_total);
      return 0; }
   | 
 
#val 会打印测试样例 
__FILE_LINE__ 会打印当前的文件名和行号 
输出如下:
1 2 3 4 5 6 7
   | demo.cpp:30::calling 1 == test_func() demo.cpp:30::passed  demo.cpp:33::calling 2 == test_func() demo.cpp:33::error  ################ summary ################### passed: 1 total: 2
   | 
 
为什么用 do-while(0) ?
当时我看到这一用法也比较疑惑,但 do-while(0) 的用法还是比较常见的。多用于在一个宏定义中出现多条语句的场景中,那我们来分析一下为什么要这么用。如果我们这样定义:
1 2 3
   | #define SS \     stmt1; \     stmt2;
   | 
 
在以下的使用场景中:
宏展开后,会变成:
1 2 3 4 5
   | if (cond)     stmt1;     stmt2;     ;     stmt3;
   | 
 
所以不管 cond 是真是假,stmt2 语句都会执行。而我们自己的意图肯定是,只有 cond 为真的时候,stmt1 和 stmt2 才会执行。那我们给宏加上花括号试一试:
1 2 3 4
   | #define SS { \     stmt1; \     stmt2; \ }
  | 
 
但是在下面这种情况下,还是会存在一些错误:
1 2 3 4
   | if (cond)     SS; else     stmt3;
   | 
 
这样宏展开的结果为:
1 2 3 4 5 6 7
   | if (cond) {      stmt1;      stmt2;  } ; else     stmt3;
  | 
 
直接导致编译错误,而出错的原因是 else 前面多一个分号。当然也可以在使用 SS 的地方后面不加分号,但是在 C 语言中通常我们习惯性的会在语句后面加一个分号。鉴于上面的这些原因,就有人想出了 do-while(0) 式的用法:
1 2 3 4 5
   | #define SS \     do { \         stmt1; \         stmt2; \     } while(0)
   | 
 
字符串连接
1 2 3 4 5 6 7 8 9 10
   | #include <iostream>
  #define define_val(tag)  \     int a_##tag = 77
  int main() {     define_val(MAX);     std::cout << a_MAX;     return 0; }
   | 
 
上面代码的意思是,将 a_ 和传入的 tag 连接在一起,意思是:int a_MAX = 77; 的意思。上述代码中完全没有直接出现 a_MAX 这个字符串,但我们依然可以使用。
这样做的一点点好处是:比如现在有 100 个模块分散在项目的各个角落,需要给各个模块计时统计性能。那么每次都定义起始时间、结束时间,并且计算执行时间,这些操作都是重复的。为了精简重复的操作,我们可以使用这个宏技巧来实现。如下所示的代码,我们把宏放到头文件,用户在引用头文件后,只需要两行代码就可以快速完成对模块的计时功能。
测试函数执行时间的宏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
   | #include <stdio.h> #include <unistd.h> #include <sys/time.h>
  typedef struct Time {     double time; } Time;
  void GetTime(Time* T) {     struct timeval tv;     gettimeofday(&tv, NULL);     T->time = (tv.tv_sec * 1000.0) + (tv.tv_usec / 1000.0); }
  #define TIME_START(tag) \     Time tag##_start, tag##_end; \     do { \         GetTime(&(tag##_start)); \     } while(0)
  #define TIME_END(tag) \     do { \         GetTime(&(tag##_end)); \         printf(#tag " cost %.2f \n", tag##_end.time - tag##_start.time); \     } while(0)
  void func() {     usleep(10000); }
  int main() {
           TIME_START(loop_func_20);
      for (int i = 0; i < 20; i++) {         func();     }
           TIME_END(loop_func_20); }
   | 
 
输出如下:
1
   | loop_func_20 cost 202.44ms 
   | 
 
实现泛型
由于转专业,需要在大二补学大一的课程。在学 C# 的时候,舍友对泛型总结为五个字:「参数化类型」惊艳了连 C 都写不利索的我。就像 C++ 的模板一样,为一个函数只写一套代码,把类型看成参数,但是这个函数能支持各个类型。如果 C 要实现泛型,宏定义是必不可少的方案。
加入此时我们面临一个需求,有一个 matirx,如果数据类型是 u8,那么调用 WriteU8ToFile 函数将数据写到文件;如果数据类型是 u16,那么调用 WriteU16ToFile 函数将数据写出到文件。
我们大概写一下这俩函数的伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | typedef struct {     ...     void *data; } Matrix;
  WriteU8ToFile(Matrix *mat, const char *file) {     Fopen(file);
      uint8_t *data = (uint8_t *)(mat->data);     for (int i = 0; i < mat->size; i++) {         FWrite("%d", (int)data[i]);     }
      Fclose(file) }
  WriteU16ToFile(Matrix *mat, const char *file) {     Fopen(file);
      uint16_t *data = (uint16_t *)(mat->data);     for (int i = 0; i < mat->size; i++) {         FWrite("%d", (int)data[i]);     }
      Fclose(file) }
  | 
 
可以看到,除了函数名中的 U8 和 U16,以及函数体中的 uint8_t 和 uint16_t,其余内容完全一样。能不能像 C++ 的模板一样,写一套通用的代码,而不是将代码重复这么多次导致冗余?
我们可以用宏定义代替 U8 和 uint8_t:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | #define WRITE_DATA_TO_TXT(type, typeName)                                   \ void Write##typeName##DataToTxt(Matrix *src, const char *path) {            \     FILE *file_ptr = fopen(path, "w");                                      \     for (int i = 0; i < src->h; i++) {                                      \         type *data = (type *)src->data + i * src->pitch / sizeof(type);     \         for (int j = 0; j < src->w; j++) {                                  \             if (#typeName == "U8") {                                        \                 fprintf(file_ptr, "U8  %d\n", (int)data[j]);                \             } else {                                                        \                 fprintf(file_ptr, "U16 %d\n", (int)data[j]);                \             }                                                               \         }                                                                   \     }                                                                       \     fclose(file_ptr);                                                       \ }
   | 
 
这样,调用这个宏定义的函数时,可以通过设置 type 和 typeName 字段去生成各个类型的函数:
1 2 3
   | WRITE_DATA_TO_TXT(uint8_t, U8)      
  WRITE_DATA_TO_TXT(uint16_t, U16)    
   | 
 
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
   | #include <stdio.h> #include <stdint.h> #include <stdlib.h>
  typedef struct {     int w;     int h;     int pitch;     void *data; } Matrix;
  #define WRITE_DATA_TO_TXT(type, typeName)                                   \ void Write##typeName##DataToTxt(Matrix *src, const char *path) {            \     FILE *file_ptr = fopen(path, "w");                                      \     for (int i = 0; i < src->h; i++) {                                      \         type *data = (type *)src->data + i * src->pitch / sizeof(type);     \         for (int j = 0; j < src->w; j++) {                                  \             if (#typeName == "U8") {                                        \                 fprintf(file_ptr, "U8  %d\n", (int)data[j]);                \             } else {                                                        \                 fprintf(file_ptr, "U16 %d\n", (int)data[j]);                \             }                                                               \         }                                                                   \     }                                                                       \     fclose(file_ptr);                                                       \ }
  WRITE_DATA_TO_TXT(uint8_t, U8)
  WRITE_DATA_TO_TXT(uint16_t, U16)
  int main() {     Matrix u8Mat = {.w = 3, .h = 2, .pitch = 3 * sizeof(uint8_t), .data = malloc(3 * 2 * sizeof(uint8_t))};     uint8_t *u8Data = (uint8_t *)u8Mat.data;     u8Data[0] = 1; u8Data[1] = 2; u8Data[2] = 3;     u8Data[3] = 4; u8Data[4] = 5; u8Data[5] = 6;     WriteU8DataToTxt(&u8Mat, "u8_data.txt");
      Matrix u16Mat = {.w = 3, .h = 2, .pitch = 3 * sizeof(uint16_t), .data = malloc(3 * 2 * sizeof(uint16_t))};     uint16_t *u16Data = (uint16_t *)u16Mat.data;     u16Data[0] = 10; u16Data[1] = 20; u16Data[2] = 30;     u16Data[3] = 40; u16Data[4] = 50; u16Data[5] = 60;     WriteU16DataToTxt(&u16Mat, "u16_data.txt");
      free(u8Mat.data);     free(u16Mat.data);
      return 0; }
   | 
 
特殊宏
__VA_ARGS__ 是一个预处理器宏,用于表示可变参数列表。它通常用于定义可变参数的宏,例如 printf 函数。在宏定义中,__VA_ARGS__ 表示可变参数列表部分,可以在宏展开时将其替换为实际的参数列表。官方定义较为玄幻,直接看代码吧:
1 2 3 4 5 6 7 8 9
   | #include <stdio.h>
  #define LOG(format, ...) printf(format, ##__VA_ARGS__)
  int main() {     LOG("===== info =====\n");        LOG("data is %d\n", 2);           return 0; }
   | 
 
一个简单的打日志函数
给上述代码加一些辅助信息,就可以实现一个日志函数:
1 2 3 4 5 6 7 8 9 10
   | #include <stdio.h>
  #define LOG(tag, format, ...) \     printf("[%s] [%s %s %d] " format, tag, __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__)
  int main() {     LOG("BASE", "Nothing\n");     LOG("BASE", " ? info diff >= %d : %.4f %d\n", 2, 0.1, 2);     return 0; }
   | 
 
对于
宏展开为:
1
   | printf("[%s] [%s %s %d] " "Nothing", "Base", "demo.cpp", "main", 7);  
  | 
 
注意,Nothing 这个信息是在 format 中,因此第一个 %s 对应的是 tag,所以最终输出为:
1
   | [BASE] [test.cpp main 8] Nothing
   | 
 
同理,第二个宏展开后的输出为:
1
   | [BASE] [test.cpp main 7]  ? info diff >= 2 : 0.1000 2
   | 
 
- 注意:代码中使用 
##__VA_ARGS__ 而不是 __VA_ARGS__,这是因为 ##__VA_ARGS__ 用于在可变参数列表为空时删除前面的逗号。在 C 语言中,如果可变参数列表为空,则在逗号之后没有参数,这会导致编译错误。 
X 宏的使用
通过宏定义的方式,根据指令执行不同的函数。比如输入的指令是 CMD_LED_ON,执行的函数是 led_on;输入的指令是 CMD_LED_OFF,执行的函数是 led_off。首先定义这两个函数:
1 2 3 4 5 6 7 8 9
   | static void led_on(void* p) {     printf("%s \r\n", (char *)p); }
  static void led_off(void* p) {     printf("%s \r\n", (char *)p); }
   | 
 
将这两个指令 CMD_LED_ON 和 CMD_LED_OFF 定义到一个枚举变量中,不过是以宏的形式:
1 2 3 4 5 6 7 8 9 10 11 12
   | #define MACROS_TABLE                    \     X_MACROS(CMD_LED_ON,  led_on)       \     X_MACROS(CMD_LED_OFF, led_off)      \
 
  typedef enum {     #define X_MACROS(a, b) a,     MACROS_TABLE     #undef X_MACROS     CMD_MAX } cmd_e;
   | 
 
#define X_MACROS(a, b) a 表示取出 (a, b) 中的第一个元素 a,则宏展开后的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | typedef enum {     #define X_MACROS(a, b) a,     X_MACROS(CMD_LED_ON,  led_on)       \     X_MACROS(CMD_LED_OFF, led_off)      \     #undef X_MACROS }
  继续把 X_MACROS 展开得到:
 
  typedef enum {     CMD_LED_ON,     CMD_LED_OFF,     CMD_MAX } cmd_e;
   | 
 
#define X_MACROS(a, b) b, 表示取出宏的第二个元素。使用同样的方法,在定义一个函数数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | typedef void (*func)(void* p); const func func_table[] = {     #define X_MACROS(a, b) b,     MACROS_TABLE     #undef X_MACROS };
  宏展开为:
  const func func_table[] = {     led_on,     led_off };
   | 
 
此时,func_table[CMD_LED_ON] 指向了 led_on 函数,func_table[CMD_LED_OFF] 指向了 led_off 函数,就实现了简单的根据不同的输入指令执行不同的函数。完成代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
   | #include <stdio.h>
  #define MACROS_TABLE                    \     X_MACROS(CMD_LED_ON,  led_on)       \     X_MACROS(CMD_LED_OFF, led_off)      \
 
  typedef enum {     #define X_MACROS(a, b) a,     MACROS_TABLE     #undef X_MACROS     CMD_MAX } cmd_e;
 
  const char* cmd_str[] = {     #define X_MACROS(a, b) #a,     MACROS_TABLE     #undef X_MACROS };
  typedef void (*func)(void* p);
  static void led_on(void* p) {     printf("%s \r\n", (char *)p); }
  static void led_off(void* p) {     printf("%s \r\n", (char *)p); }
  const func func_table[] = {     #define X_MACROS(a, b) b,     MACROS_TABLE     #undef X_MACROS };
  static void cmd_handle(cmd_e cmd) {     if(cmd < CMD_MAX)     {         func_table[cmd]((void*)cmd_str[cmd]);     } }
  int main() {     cmd_handle(CMD_LED_ON);     cmd_handle(CMD_LED_OFF);     return 0; }
   | 
 
参考
- X-宏的用法