2

C 语言中的黑魔法:宏

 7 months ago
source link: https://muyuuuu.github.io/2024/02/03/define-macro/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

之前对 C 语言中宏定义的认知十分简单,包括但不限于停留在以下浅薄的层面:

#define PI 3.14
#define add(a, b) a + b

上述代码完全是大学课本中的用法。但当我看到实际项目中宏的用法后完全是一头雾水,所以自己也要写出那种高逼格让别人看不太懂的代码。宏远远比我想象的要强大,所以本文为每个宏技巧都配备了一个实用场景。

  • 字符串化操作符,实现一个简单的自动化测试样例
  • 字符串连接,实现一个具备计时功能的宏
  • X 宏,实现根据输入执行不同的函数
  • 特殊宏 __VA_ARGS__,实现一个简单的日志函数

字符串化操作符

#include <iostream>

#define str(a) #a

int main() {
std::cout << str(FUNC); // 输出 FUNC
return 0;
}

上述宏 str 通过单井号的形式实现了字符串化操作符,将传入的参数字符串化。

简单测试框架

C 语言有一些预定义的宏,比如 __LINE__ 表示当前行号,__FILE__ 表示当前的文件名,基于这一基础,我们实现一个简单的测试程序。在测试程序时,打印测试用例、文件名、行号、以及是否通过测试。

#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__ 会打印当前的文件名和行号

输出如下:

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) 的用法还是比较常见的,多用于在一个宏定义中出现多条语句的情境中。那我们来分析一下为什么要这么用:

如果我们这样定义:

#define SS \
stmt1; \
stmt2;

在以下的使用场景中:

if (cond)
SS;
stmt3;

宏展开后,会变成:

if (cond)
stmt1;
stmt2;
;
stmt3;

所以不管 cond 是真是假,stmt2 语句都会执行。而我们自己的意图肯定是,只有 cond 为真的时候,stmt1stmt2 才会执行。造成上述结果的原因是调用者希望宏中的多条语句是绑定在一起执行的,那我们给宏加上花括号试一试:

#define SS { \
stmt1; \
stmt2; \
}

但是在下面这种情况下,还是会存在一些错误:

if (cond)
SS;
else
stmt3;

这样宏展开的结果为:

if (cond) { 
stmt1;
stmt2;
}
;
else
stmt3;

直接导致编译错误,而出错的原因是 else 前面多一个分号。当然也可以在使用 SS 的地方后面不加分号,但是在 C 语言中通常我们习惯性的会在语句后面加一个分号。鉴于上面的这些原因,就有人想出了 do-while(0) 式的用法:

#define SS \
do { \
stmt1; \
stmt2; \
} while(0)

字符串连接

#include <iostream>

#define define_val(tag) \
int a_##tag = 77

int main() {
define_val(MAX);
std::cout << a_MAX;
return 0;
}

上面代码的意思是,将 a_ 和传入的 tag 连接在一起,形成一个新的字符串使用。上述代码中完全没有直接出现 a_MAX 这个字符串,但我们依然可以使用。

这样做的一点点好处是:比如现在有 100 个模块分散在项目的各个角落,需要给模块计时。那么每次都定义起始时间、结束时间,并且计算执行时间。这些操作都是重复的,为了精简重复的操作,我们可以使用这个宏技巧来实现。如下所示的代码,我们把宏放到头文件,所以用户只需要两行代码就可以快速的完成计时功能。

测试函数执行时间的宏

#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);
}

输出如下:

loop_func_20 cost 202.44ms 

__VA_ARGS__ 是一个预处理器宏,用于表示可变参数列表。它通常用于定义可变参数的宏,例如 printf 函数。在宏定义中,__VA_ARGS__ 表示可变参数列表的部分,可以在宏展开时将其替换为实际的参数列表。官方定义较为玄幻,直接看代码吧:

#include <stdio.h>

#define LOG(format, ...) printf(format, ##__VA_ARGS__)

int main() {
LOG("===== info =====\n"); // 0 参数
LOG("data is %d\n", 2); // 1 个参数
return 0;
}

一个简单的打日志函数

给上述代码加一些辅助信息:

#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;
}
LOG("BASE", "Nothing");

宏展开为:

printf("[%s] [%s %s %d] " "Nothing", "Base", "demo.cpp", "main", 7);  

注意,Nothing 这个信息是在 format 中,因此第一个 %s 先输出 tag,所以最终输出为:

[BASE] [test.cpp main 8] Nothing

同理,第二个宏展开后的输出为:

[BASE] [test.cpp main 7]  ? info diff >= 2 : 0.1000 2

X 宏的使用

根据指令执行不同的函数,待整理……

#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;

/*定义字符串列表用作Log打印*/
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;
}
  1. X-宏的用法

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK