C语言中的宏日志打印语法以及相对printf的优点

文章目录

  • 宏日志打印解析
    • [一、核心语法拆解:`#define LOG(...) __log_info(VA_ARGS)`](#define LOG(...) __log_info(VA_ARGS)`)
      • [1. `#define LOG(...)`](#define LOG(...)`)
      • [2. `__log_info(VA_ARGS)`](#2. __log_info(__VA_ARGS__))
    • [二、配套的可变参数函数 `__log_info` 解析](#二、配套的可变参数函数 __log_info 解析)
      • [1. 函数签名:`static void __log_info(const char* format, ...)`](#1. 函数签名:static void __log_info(const char* format, ...))
      • [2. 可变参数处理三剑客(`<stdarg.h>` 标准库)](#2. 可变参数处理三剑客(<stdarg.h> 标准库))
      • [3. `vsnprintf(msg, sizeof(msg), format, args)`](#3. vsnprintf(msg, sizeof(msg), format, args))
      • [4. `fprintf(stdout, "%s\n", msg)`](#4. fprintf(stdout, "%s\n", msg))
    • 三、完整调用流程示例
    • 四、语法细节与注意事项
    • 五、总结
  • 相对printf的优点
    • [一、直接用 `printf` 的核心痛点](#一、直接用 printf 的核心痛点)
      • [1. 扩展性差:无法统一修改日志行为](#1. 扩展性差:无法统一修改日志行为)
      • [2. 安全性不足:无缓冲区限制,易溢出](#2. 安全性不足:无缓冲区限制,易溢出)
      • [3. 可维护性差:日志行为无法统一管控](#3. 可维护性差:日志行为无法统一管控)
      • [4. 跨平台/场景适配难](#4. 跨平台/场景适配难)
    • [二、封装成 `LOG` 宏 + `__log_info` 的核心优势](#二、封装成 LOG 宏 + __log_info 的核心优势)
    • [三、具体示例:封装后能轻松实现的功能(`printf` 做不到)](#三、具体示例:封装后能轻松实现的功能(printf 做不到))
      • [1. 一键关闭所有日志(上线时)](#1. 一键关闭所有日志(上线时))
      • [2. 给日志加时间戳/级别](#2. 给日志加时间戳/级别)
      • [3. 把日志输出到文件(而非屏幕)](#3. 把日志输出到文件(而非屏幕))
    • [四、总结:为什么不直接用 `printf`?](#四、总结:为什么不直接用 printf?)

宏日志打印解析

一、核心语法拆解:#define LOG(...) __log_info(__VA_ARGS__)

这是一个可变参数宏 (Variadic Macro),专门用来把不定数量的参数转发给可变参数函数 __log_info

1. #define LOG(...)

  • LOG:宏名,你在代码里写 LOG(...) 就会被替换成后面的内容。
  • (...):表示这是一个可变参数宏 ,可以接收任意数量、任意类型的参数(包括 0 个参数)。
  • 等价于:LOG 是一个"参数数量不限"的宏。

2. __log_info(__VA_ARGS__)

  • __VA_ARGS__:C 标准预定义宏,代表宏定义中 (...) 里的所有实际参数,会原封不动替换到宏体中。

  • 比如你写:

    c 复制代码
    LOG("hello %d", 123);

    预处理后会变成:

    c 复制代码
    __log_info("hello %d", 123);
  • 本质:LOG 只是一个语法糖 ,把调用转发给 __log_info 函数。


二、配套的可变参数函数 __log_info 解析

c 复制代码
static void __log_info(const char* format, ...) {
    char msg[1000];
    va_list args;
    va_start(args, format);
    vsnprintf(msg, sizeof(msg), format, args);
    fprintf(stdout, "%s\n", msg);
    va_end(args);
}

1. 函数签名:static void __log_info(const char* format, ...)

  • static:函数仅在当前编译单元可见(避免全局命名冲突)。
  • const char* format:第一个固定参数,是格式化字符串(类似 printf 的第一个参数)。
  • ...可变参数列表,表示后面可以跟任意数量、任意类型的参数。

2. 可变参数处理三剑客(<stdarg.h> 标准库)

  • va_list args:声明一个可变参数列表变量 ,用来遍历 ... 里的参数。
  • va_start(args, format):初始化 args,告诉编译器"可变参数从 format 之后开始"。
  • va_end(args):清理 args,必须和 va_start 成对出现,否则可能内存泄漏。

3. vsnprintf(msg, sizeof(msg), format, args)

  • 作用:把格式化字符串 format 和可变参数 args 格式化输出到缓冲区 msg,最多写 sizeof(msg) 字节(防止溢出)。
  • 对比 vsprintfvsnprintf 带长度限制,更安全,不会越界。
  • 这里 msg[1000] 是固定大小缓冲区,最多存 999 个字符 + 末尾 \0

4. fprintf(stdout, "%s\n", msg)

  • 把格式化好的字符串 msg 输出到标准输出(屏幕),并自动加换行。

三、完整调用流程示例

你在代码里写:

c 复制代码
LOG("a = %d, b = %s", 42, "test");

预处理阶段 → 宏展开:

c 复制代码
__log_info("a = %d, b = %s", 42, "test");

函数执行阶段

  1. format 指向 "a = %d, b = %s"
  2. va_start(args, format)args 指向后面的 42"test"
  3. vsnprintfformat + args 格式化成 "a = 42, b = test" 存入 msg
  4. fprintf 输出 a = 42, b = test\n 到屏幕
  5. va_end(args) 清理参数列表

四、语法细节与注意事项

  1. __VA_ARGS__ 兼容性

    • C99 及以后标准支持,C++11 也支持。

    • 若编译器较老,可能需要用 ##__VA_ARGS__ 处理 0 个参数的情况(比如 LOG()):

      c 复制代码
      #define LOG(...) __log_info(__VA_ARGS__)  // 不能传 0 个参数
      #define LOG(...) __log_info(##__VA_ARGS__) // 兼容 0 个参数(GCC 扩展)
  2. 缓冲区安全问题

    • msg[1000] 是固定大小,如果格式化后字符串超过 999 字节,vsnprintf 会截断,不会崩溃,但日志会不完整。
    • 更安全的做法是动态分配内存(malloc/snprintf 先算长度),或用更大的缓冲区。
  3. static 作用

    • __log_info 仅在当前 .c 文件可见,避免和其他文件的同名函数冲突。
    • 如果想在多个文件使用,需要去掉 static,并在头文件声明:void __log_info(const char* format, ...);

五、总结

语法元素 作用
#define LOG(...) 定义可变参数宏,接收任意数量参数
__VA_ARGS__ 代表宏中 ... 的所有实际参数,转发给函数
...(函数参数) 声明可变参数列表
va_list / va_start / va_end 遍历和处理可变参数的标准工具
vsnprintf 格式化可变参数到缓冲区,安全版本

一句话概括:这段代码用「可变参数宏 + 可变参数函数」实现了一个类似 printf 的日志打印接口,让你可以用 LOG("format", ...) 方便地打日志

相对printf的优点

一、直接用 printf 的核心痛点

printf 本身是基础的打印函数,但在实际项目中直接用会暴露很多问题:

1. 扩展性差:无法统一修改日志行为

如果项目中到处写 printf("xxx");,后续想加功能(比如:打印时间戳、日志级别、输出到文件/日志系统),需要逐行修改所有 printf,成本极高。

比如:想让日志带时间 → 直接改 printf 要写 printf("[%s] xxx", get_time());,几百处调用就要改几百次。

2. 安全性不足:无缓冲区限制,易溢出

printf 直接输出到标准输出,若格式化字符串超长/参数不匹配,可能导致:

  • 格式化参数错误(比如 printf("%d", "abc");):直接崩溃;
  • 无长度限制:若拼接超长字符串,可能触发缓冲区溢出(尤其嵌入式/高性能场景)。

3. 可维护性差:日志行为无法统一管控

  • 调试阶段需要打印日志,上线后想关闭所有日志:直接用 printf 要注释/删除所有调用,容易漏改;
  • 不同模块想区分日志级别(INFO/ERROR/DEBUG):printf 无法做到,只能靠字符串前缀手动区分,混乱且易错。

4. 跨平台/场景适配难

  • 嵌入式系统中,stdout 可能不是屏幕(比如串口、Flash),直接用 printf 无法适配,需要重定向;
  • 多线程场景:printf 不是线程安全的(部分平台),直接用可能导致日志错乱。

二、封装成 LOG 宏 + __log_info 的核心优势

对比直接用 printf,封装后的写法完美解决上述问题,且保留了 printf 式的易用性:

特性 直接用 printf 封装 LOG 宏 + __log_info
扩展性 几乎无扩展能力,改一处动全身 只需修改 __log_info 函数,即可全局修改日志行为(加时间、级别、输出目标)
安全性 无缓冲区限制,易溢出/崩溃 vsnprintf 限制缓冲区大小(msg[1000]),避免溢出
可维护性 日志开关/级别无法统一管控 加宏定义即可全局关闭日志(比如 #define LOG(...)),或区分级别
跨平台适配 依赖 stdout,适配成本高 只需改 __log_info 中的输出逻辑(比如换成串口打印)
易用性 原生支持格式化,易用但不灵活 完全兼容 printf 语法(LOG("xxx %d", 123)),易用且灵活

三、具体示例:封装后能轻松实现的功能(printf 做不到)

1. 一键关闭所有日志(上线时)

只需在宏定义处加条件编译:

c 复制代码
// 调试阶段:开启日志
#define DEBUG 1
#if DEBUG
#define LOG(...) __log_info(__VA_ARGS__)
#else
// 上线阶段:关闭所有日志,宏展开为空
#define LOG(...)
#endif

直接用 printf 做不到"一键关闭",只能手动注释。

2. 给日志加时间戳/级别

修改 __log_info 函数即可全局生效:

c 复制代码
static void __log_info(const char* format, ...) {
    // 1. 加时间戳
    time_t now = time(NULL);
    char time_str[32];
    strftime(time_str, sizeof(time_str), "[%Y-%m-%d %H:%M:%S]", localtime(&now));
    
    // 2. 格式化日志内容
    char msg[1000];
    va_list args;
    va_start(args, format);
    vsnprintf(msg, sizeof(msg), format, args);
    va_end(args);
    
    // 3. 输出:时间戳 + 日志内容
    fprintf(stdout, "%s [INFO] %s\n", time_str, msg);
}

调用 LOG("a = %d", 123) 会输出:[2026-03-20 17:00:00] [INFO] a = 123,直接用 printf 要每次手动拼时间戳。

3. 把日志输出到文件(而非屏幕)

只需改 fprintf 的第一个参数:

c 复制代码
// 打开日志文件
FILE* log_file = fopen("app.log", "a");
if (log_file) {
    fprintf(log_file, "%s\n", msg); // 输出到文件
    fclose(log_file);
}

直接用 printf 只能输出到屏幕,重定向需要改系统层面的输出(比如 ./app > log.txt),无法灵活控制。

四、总结:为什么不直接用 printf

printf 是"最小可用"的打印工具,但缺乏工程化所需的扩展性、安全性、可维护性 。封装成 LOG 宏 + 自定义函数:

  1. ✅ 兼容 printf 的所有格式化语法,不增加使用成本;
  2. ✅ 一次修改,全局生效(加时间、改输出目标、开关日志);
  3. ✅ 增加缓冲区限制,避免溢出崩溃;
  4. ✅ 便于后续扩展(比如日志分级、异步打印、按模块过滤)。

简单说:printf 适合小脚本/测试代码,而封装后的 LOG 适合实际项目开发------这是"能用"和"好用"的区别。

相关推荐
博语小屋19 分钟前
I/O 多路转接之epoll
运维·服务器·数据库
爱编码的小八嘎42 分钟前
C语言完美演绎5-3
c语言
山川行1 小时前
关于《项目C语言》专栏的总结
c语言·开发语言·数据结构·vscode·python·算法·visual studio code
呜喵王阿尔萨斯1 小时前
C and C++ code
c语言·开发语言·c++
星辰徐哥1 小时前
C语言游戏开发:Pygame、SDL、OpenGL深度解析
c语言·python·pygame
文静小土豆1 小时前
Linux 进程终止指南:理解 kill 与 kill -9 的核心区别与正确用法
linux·运维·服务器
IMPYLH1 小时前
Linux 的 df 命令
linux·运维·服务器
lzhdim1 小时前
SQL 入门 7:SQL 聚合与分组:函数、GROUP BY 与 ROLLUP
java·服务器·数据库·sql·mysql
wefg11 小时前
【Linux】会话、终端、前后台进程
linux·运维·服务器
zhixingheyi_tian1 小时前
Linux/Windows 免密登录
linux·运维·服务器