21 可变参数宏与日志封装

一、核心结论

... 接收可变数量的宏实参,__VA_ARGS__ 将这些实参放进宏替换结果。 宏只负责预处理展开,真正输出日志的是 printf 或 SDK 日志函数。

#include <stdio.h>
#define LOG(...) printf(__VA_ARGS__)

int main(void)
{
    LOG("BLE connected\n");
    LOG("RSSI=%d\n", -60);
    return 0;
}

输出:

BLE connected
RSSI=-60

二、宏展开流程图

LOG("RSSI=%d\n", -60)
           │
           ▼   预处理替换 __VA_ARGS__
printf("RSSI=%d\n", -60)
           │
           ▼   运行时 printf 格式化
        RSSI=-60
  • ...:定义可变参数部分。
  • __VA_ARGS__:引用对应的实参序列。
  • 不会在运行时拼接字符串,也不会由宏本身执行打印。

三、二极管对照:两种日志宏

#define LOG1(...)       printf(__VA_ARGS__)
#define LOG2(fmt, ...)  printf(fmt, __VA_ARGS__)
情况(严格 C11)LOG1(...)LOG2(fmt, ...)
只有格式字符串支持:LOG1("OK\n")不能省略可变参数部分
格式串加数值支持:LOG1("%d", 3)支持:LOG2("%d", 3)
简单转发日志推荐容易遇到尾随逗号问题

为什么 LOG2("OK\n") 不行?

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

/* 严格 C11:未提供所需的可变参数部分 */
/* LOG2("OK\n"); */

在严格 C11 中,对含有固定参数 fmt 的可变参数宏,调用时需要提供可变参数部分。即使某些扩展接受省略,简单替换也会留下无效代码:

printf("OK\n", );  // 多余的逗号,语法错误

本篇限定严格 C11,不讨论 GNU , ##__VA_ARGS__ 或 C23 __VA_OPT__。

四、TWS 固件中的简易日志入口

#include <stdio.h>

#define BLE_LOG(...) printf(__VA_ARGS__)

void on_connected(int handle)
{
    BLE_LOG("BLE connected\n");
    BLE_LOG("handle=%d\n", handle);
}

在实际固件中,printf 可由 SDK 的日志输出接口替代;ISR、实时音频任务等执行上下文是否允许同步打印,应遵守 SDK 要求。本篇不展开日志等级、延迟打印等其他话题。

五、速记

  1. ... 收参数,__VA_ARGS__ 转参数。
  2. LOG(...) 能转发只有格式字符串的调用,也能转发格式串与额外参数。
  3. 简单的 LOG(fmt, ...) 在严格 C11 下不能直接省略可变参数部分;否则还可能留下 printf(fmt, )。
  4. 宏在预处理时展开,日志函数在运行时输出。