之前对 C 语言中宏定义的认知十分简单,包括但不限于停留在以下浅薄的层面:
| 12
 
 | #define PI 3.14#define add(a, b) a + b
 
 | 
上述代码完全是大学课本中的用法。但当我看到实际项目中宏的用法后完全是一头雾水,所以自己也要写出那种高逼格让别人看不太懂的代码。宏远远比我想象的要强大,所以本文为每个宏技巧都配备了一个实用场景。
- 字符串化操作符,实现一个简单的自动化测试样例
- 字符串连接,实现一个具备计时功能的宏
- X 宏,实现根据输入执行不同的函数
- 特殊宏 __VA_ARGS__,实现一个简单的日志函数
字符串化操作符
| 12
 3
 4
 5
 6
 7
 8
 
 | #include <iostream>
 #define str(a) #a
 
 int main() {
 std::cout << str(FUNC);
 return 0;
 }
 
 | 
上述宏 str 通过单井号的形式实现了字符串化操作符,将传入的参数字符串化。
简单测试框架
C 语言有一些预定义的宏,比如 __LINE__ 表示当前行号,__FILE__ 表示当前的文件名。基于这一基础,我们实现一个简单的测试程序。在测试程序时,打印测试用例、文件名、行号、以及是否通过测试。
| 12
 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__会打印当前的文件名和行号
输出如下:
| 12
 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) 的用法还是比较常见的。多用于在一个宏定义中出现多条语句的场景中,那我们来分析一下为什么要这么用。如果我们这样定义:
| 12
 3
 
 | #define SS \stmt1; \
 stmt2;
 
 | 
在以下的使用场景中:
宏展开后,会变成:
| 12
 3
 4
 5
 
 | if (cond)stmt1;
 stmt2;
 ;
 stmt3;
 
 | 
所以不管 cond 是真是假,stmt2 语句都会执行。而我们自己的意图肯定是,只有 cond 为真的时候,stmt1 和 stmt2 才会执行。那我们给宏加上花括号试一试:
| 12
 3
 4
 
 | #define SS { \stmt1; \
 stmt2; \
 }
 
 | 
但是在下面这种情况下,还是会存在一些错误:
| 12
 3
 4
 
 | if (cond)SS;
 else
 stmt3;
 
 | 
这样宏展开的结果为:
| 12
 3
 4
 5
 6
 7
 
 | if (cond) { stmt1;
 stmt2;
 }
 ;
 else
 stmt3;
 
 | 
直接导致编译错误,而出错的原因是 else 前面多一个分号。当然也可以在使用 SS 的地方后面不加分号,但是在 C 语言中通常我们习惯性的会在语句后面加一个分号。鉴于上面的这些原因,就有人想出了 do-while(0) 式的用法:
| 12
 3
 4
 5
 
 | #define SS \do { \
 stmt1; \
 stmt2; \
 } while(0)
 
 | 
字符串连接
| 12
 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 个模块分散在项目的各个角落,需要给各个模块计时统计性能。那么每次都定义起始时间、结束时间,并且计算执行时间,这些操作都是重复的。为了精简重复的操作,我们可以使用这个宏技巧来实现。如下所示的代码,我们把宏放到头文件,用户在引用头文件后,只需要两行代码就可以快速完成对模块的计时功能。
测试函数执行时间的宏
| 12
 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 函数将数据写出到文件。
我们大概写一下这俩函数的伪代码:
| 12
 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:
| 12
 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 字段去生成各个类型的函数:
| 12
 3
 
 | WRITE_DATA_TO_TXT(uint8_t, U8)      
 WRITE_DATA_TO_TXT(uint16_t, U16)
 
 | 
完整代码如下:
| 12
 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__ 表示可变参数列表部分,可以在宏展开时将其替换为实际的参数列表。官方定义较为玄幻,直接看代码吧:
| 12
 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;
 }
 
 | 
一个简单的打日志函数
给上述代码加一些辅助信息,就可以实现一个日志函数:
| 12
 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。首先定义这两个函数:
| 12
 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 定义到一个枚举变量中,不过是以宏的形式:
| 12
 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,则宏展开后的代码为:
| 12
 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, 表示取出宏的第二个元素。使用同样的方法,在定义一个函数数组:
| 12
 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 函数,就实现了简单的根据不同的输入指令执行不同的函数。完成代码如下:
| 12
 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-宏的用法