之前对 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-宏的用法