c++日志宏 INFO(...)
🧱 核心功能设计
我们希望 INFO(…) 具备以下能力:
✅ 自动记录当前文件名(__FILE__)
✅ 自动记录当前行号(__LINE__)
✅ 支持 printf 风格的格式化(如 %s, %d)
✅ 输出带颜色(绿色)
✅ 封装在命名空间中,避免命名冲突
💡 实现原理
我们将使用 C++ 的三大“黑科技”:
#define 宏:简化调用
__FILE__ 和 __LINE__:自动插入源码位置
va_list / va_start / vprintf:处理可变参数
// main.cpp#include <cstdio> // 必须包含,因为用了 printf, va_list 等
#include <cstdarg> // 有些编译器需要这个头文件来支持 va_list// ========================
// 1. 定义命名空间 logger
// ========================
namespace Application {
namespace logger {// 宏定义:INFO(...)// 当你写 INFO(...) 时,它会被自动替换为 __printf,并带上文件名和行号#define INFO(...) Application::logger::__printf(__FILE__, __LINE__, __VA_ARGS__)// 实际的日志打印函数void __printf(const char* file, int line, const char* fmt, ...) {va_list vl; // 声明一个可变参数列表变量va_start(vl, fmt); // 初始化,告诉它参数从 fmt 开始// 打印绿色的 [文件名:行号]:printf("\e[32m[%s:%d]:\e[0m ", file, line);// 打印用户自定义的消息,比如 "Hello %s"vprintf(fmt, vl);// 换行printf("\n");// 清理va_end(vl);}} // namespace logger
} // namespace Application// ========================
// 2. 主函数:使用 INFO
// ========================
int main() {// 这是我们想打印的日志INFO("Hello %s", "World");// 再打印一条INFO("User %s is %d years old", "Alice", 25);return 0;
}
我们想实现一个像 printf 一样的函数,但能自动加上 [文件名:行号] 前缀:
INFO("Hello %s, age: %d", "Alice", 25);输出结果为:
[main.cpp:42]: Hello World
[main.cpp:45]: User Alice is 25 years old
这需要函数能接收任意数量和类型的参数,这就是“可变参数”。
🔧 三大法宝:va_list, va_start, vprintf
✅ 1. va_list vl;
作用:声明一个“可变参数列表”的指针变量,用来遍历后面的参数。
类比:就像 int *p; 是指向整数的指针,va_list vl; 是指向“可变参数”的指针。
void __printf(const char* file, int line, const char* fmt, ...) {va_list vl; // 声明一个“参数指针”// ...
}
此时 vl 还没有初始化,不能用。
✅ 2. va_start(vl, fmt);
作用:初始化 va_list,告诉它“可变参数从哪个参数后面开始”。是后面!!!!!!!!!!!!!!!
参数:
vl:你声明的 va_list 变量
fmt:最后一个固定参数的名字(这里是 fmt)
va_start(vl, fmt);
✅ 执行后,vl 就指向了第一个可变参数 “Alice”。
🔍 为什么是 fmt?
因为函数参数是这样排列的:
file → "main.cpp"
line → 18
fmt → "Hello %s, age: %d"
... → "Alice", 25 ← 可变参数从这里开始
所以 va_start(vl, fmt) 的意思是:“vl 从 fmt 后面的第一个参数开始读”。
✅ 3. vprintf(fmt, vl);
作用:打印格式化字符串,但使用 va_list 而不是直接的参数列表。参数:fmt:格式化字符串,比如 "Hello %s, age: %d"vl:已经初始化的 va_list,指向可变参数
vprintf(fmt, vl);
它会:
看 fmt 中有 %s 和 %d
从 vl 指向的位置读取第一个参数 "Alice"(匹配 %s)
继续读取下一个参数 25(匹配 %d)
输出:Hello Alice, age: 25
假设我们调用:
INFO("Hello %s, age: %d", "Alice", 25);
等价于:
Application::logger::__printf("main.cpp", 18, "Hello %s, age: %d", "Alice", 25);
进入函数:
void __printf(const char* file, int line, const char* fmt, ...) {
fmt 对应于 “Hello %s, age: %d”
vl 现在指向第一个可变参数 “Alice”
内部机制:vl 是一个指针,它知道如何从栈中一步步读取 “Alice”(const char*),然后是 25(int)
vprintf 内部会:
解析 fmt = “Hello %s, age: %d”
遇到 %s → 从 vl 读取一个 const char* → 得到 “Alice”
遇到 %d → 从 vl 读取一个 int → 得到 25
输出:Hello Alice, age: 25
栈(Stack)中的参数布局:+------------------+
| "main.cpp" | ← file
+------------------+
| 18 | ← line
+------------------+
| "Hello %s..." | ← fmt
+------------------+
| "Alice" | ← 第一个可变参数
+------------------+
| 25 | ← 第二个可变参数
+------------------+↑vl 经过 va_start 后指向这里