C++日志输出库:spdlog
spdlog使用
- 1. 简介
- 2. 使用
- 2.1 基础使用示例
- 3. 核心功能详解
- 3.1 日志格式化
- 3.2 文件日志(含滚动日志)
- 3.3 多日志器与日志分流
- 3.4 异步日志(提升性能)
- 3.5 动态调整日志级别
- 4. 常用函数与变量
- 4.1 日志记录器logger与输出目标sink函数
- 日志记录器(Logger)相关函数
- 输出目标(Sink)相关函数
- 组合使用 Logger 和 Sink
- 4.2 常用函数
- init_thread_pool()
- 使用步骤
- 关键配置与注意事项
- 示例
- flush_every()
- 使用方法
- 适用场景
- 4.3 参数
- __VA_ARGS__
- 核心语法
- 典型使用场景
- 总结
- 5. 注意事项
1. 简介
spdlog
是一个高性能、单头文件(header-only) 的 C++ 日志库,支持多线程、多种日志输出目标(控制台、文件、滚动日志等),且易于集成和扩展。它是目前 C++ 项目中最流行的日志库之一,广泛用于桌面应用、服务器程序和嵌入式开发。
2. 使用
直接下载头文件: 从 spdlog 官网 下载 include/spdlog
目录,复制到项目中,直接包含:
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h" // 彩色控制台输出
2.1 基础使用示例
简单示例
- 控制台输出日志信息
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h" // 彩色控制台日志int main() {// 1. 创建彩色控制台日志器(单例模式,名称为 "console")auto console_logger = spdlog::stdout_color_mt("console");// 2. 设置全局日志级别(默认是 info,低于该级别的日志不输出)spdlog::set_level(spdlog::level::trace); // 输出所有级别日志// 3. 输出不同级别的日志(支持 printf 风格和 C++ 流风格)spdlog::trace("这是 trace 日志(最详细,用于调试)");spdlog::debug("这是 debug 日志(开发调试),整数: {}", 123); // C++ 流风格spdlog::info("这是 info 日志(常规信息),浮点数: {:.2f}", 3.14);spdlog::warn("这是 warn 日志(警告,非致命错误)");spdlog::error("这是 error 日志(错误,需处理),字符串: {}", "test");spdlog::critical("这是 critical 日志(严重错误,程序可能崩溃)");// 4. 使用指定的日志器(而非全局日志器)console_logger->info("使用 console 日志器输出");// 5. 关闭所有日志器(释放资源,可选)spdlog::shutdown();return 0;
}
输出效果(控制台彩色显示,不同级别日志颜色不同,如 error
为红色,warn
为黄色):
[2024-05-20 15:30:00.123] [trace] [main.cpp:12] 这是 trace 日志(最详细,用于调试)
[2024-05-20 15:30:00.123] [debug] [main.cpp:13] 这是 debug 日志(开发调试),整数: 123
[2024-05-20 15:30:00.123] [info] [main.cpp:14] 这是 info 日志(常规信息),浮点数: 3.14
[2024-05-20 15:30:00.123] [warn] [main.cpp:15] 这是 warn 日志(警告,非致命错误)
[2024-05-20 15:30:00.123] [error] [main.cpp:16] 这是 error 日志(错误,需处理),字符串: test
[2024-05-20 15:30:00.123] [critical] [main.cpp:17] 这是 critical 日志(严重错误,程序可能崩溃)
[2024-05-20 15:30:00.123] [info] [main.cpp:19] 使用 console 日志器输出
3. 核心功能详解
3.1 日志格式化
spdlog 支持自定义日志格式,默认格式包含 时间戳、日志级别、文件名、行号、日志内容。可通过 set_pattern
调整格式:
// 自定义格式:[时间] [级别] [文件:行号] 内容
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%l] [%s:%#] %v");
// %Y-%m-%d:日期,%H:%M:%S:时间,%l:日志级别(小写),%s:文件名,%#:行号,%v:日志内容
常用格式占位符:
占位符 | 含义 | 示例 |
---|---|---|
%Y-%m-%d | 日期(年-月-日) | 2024-05-20 |
%H:%M:%S.%f | 时间(时:分:秒.毫秒) | 15:30:00.123 |
%l | 日志级别(小写) | trace/debug/info |
%L | 日志级别(大写) | TRACE/DEBUG/INFO |
%s | 文件名 | main.cpp |
%# | 行号 | 12 |
%v | 日志内容 | 这是 info 日志 |
%t | 线程 ID | 1234 |
3.2 文件日志(含滚动日志)
除控制台外,spdlog 支持将日志写入文件,且支持滚动日志(按文件大小或时间切割,避免单个文件过大)。
示例1:普通文件日志
#include "spdlog/sinks/basic_file_sink.h"// 创建文件日志器(日志写入 "app.log",若文件存在则追加)
auto file_logger = spdlog::basic_logger_mt("file_logger", "app.log");
file_logger->info("这是写入文件的日志");
示例2:按大小滚动的日志
#include "spdlog/sinks/rotating_file_sink.h"// 配置:单个文件最大 10MB,最多保留 5 个备份文件(app.log.1, app.log.2, ..., app.log.5)
auto rotating_logger = spdlog::rotating_logger_mt("rotating_logger", // 日志器名称"app_rotating.log", // 基础文件名10 * 1024 * 1024, // 单个文件最大大小(10MB)5 // 备份文件数量
);
rotating_logger->warn("这是滚动日志的警告信息");
示例3:按时间滚动的日志
#include "spdlog/sinks/daily_file_sink.h"// 配置:每天 00:00 切割日志,最多保留 7 天的日志
auto daily_logger = spdlog::daily_logger_mt("daily_logger", // 日志器名称"app_daily.log", // 基础文件名(切割后为 app_daily.log.2024-05-20)0, // 切割小时(0 = 凌晨)0 // 切割分钟(0 = 整点)
);
daily_logger->error("这是按时间滚动的错误日志");
3.3 多日志器与日志分流
spdlog 支持创建多个独立的日志器,分别输出到不同目标(如控制台+文件、不同文件)。
示例:同时输出到控制台和文件
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/basic_file_sink.h"// 1. 创建控制台 sink 和文件 sink
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log");// 2. 创建多 sink 日志器(同时输出到两个 sink)
std::vector<spdlog::sink_ptr> sinks = {console_sink, file_sink};
auto multi_sink_logger = std::make_shared<spdlog::logger>("multi_logger", sinks.begin(), sinks.end());// 3. 设置日志器为全局日志器(可选)
spdlog::set_default_logger(multi_sink_logger);// 4. 输出日志(同时在控制台和文件中显示)
spdlog::info("同时输出到控制台和文件的日志");
3.4 异步日志(提升性能)
默认情况下,spdlog 使用同步日志(日志输出阻塞主线程)。对于高性能场景(如高并发服务器),可启用异步日志(日志写入后台线程,不阻塞主线程)。
#include "spdlog/async.h"
#include "spdlog/sinks/stdout_color_sinks.h"int main() {// 1. 初始化异步日志(必须在创建日志器前调用)// 配置:队列大小 8192,后台线程 1 个,刷新间隔 3 秒spdlog::init_thread_pool(8192, 1);// 2. 创建异步控制台日志器(_mt 表示多线程安全,_st 表示单线程)auto async_console = spdlog::basic_logger_mt<spdlog::async_factory>("async_console", "async.log");// 3. 输出日志(主线程不阻塞,日志由后台线程写入)async_console->info("这是异步日志,不阻塞主线程");// 4. 关闭异步日志(确保后台线程完成写入,可选)spdlog::shutdown();return 0;
}
3.5 动态调整日志级别
可在运行时动态修改日志级别,方便在生产环境临时开启调试日志(无需重启程序)。
// 初始级别为 info(仅输出 info 及以上)
spdlog::set_level(spdlog::level::info);
spdlog::debug("这行日志不会输出(级别低于 info)");// 动态调整为 debug(输出 debug 及以上)
spdlog::set_level(spdlog::level::debug);
spdlog::debug("这行日志会输出(级别已调整为 debug)");// 按日志器单独调整级别(不影响全局)
auto file_logger = spdlog::basic_logger_mt("file_logger", "app.log");
file_logger->set_level(spdlog::level::warn); // 仅输出 warn 及以上
file_logger->info("这行日志不会写入文件(级别低于 warn)");
file_logger->warn("这行日志会写入文件(级别为 warn)");
4. 常用函数与变量
4.1 日志记录器logger与输出目标sink函数
spdlog 库中,日志记录器(Logger)和输出目标(Sink)是两个核心概念,分别对应不同的功能函数。以下是常用的相关函数分类列举:
日志记录器(Logger)相关函数
日志记录器是日志的入口,负责接收日志消息并转发到关联的 Sink。
-
- 创建日志记录器
- 函数后缀
_mt
表示多线程安全(multi-thread),_st
表示单线程(single-thread,如stdout_logger_st
)。 - 自定义日志器需手动传入 Sink,灵活度更高。
函数原型 | 功能描述 |
---|---|
std::shared_ptr<logger> spdlog::stdout_logger_mt(const std::string& name) | 创建多线程安全的控制台(stdout)日志器 |
std::shared_ptr<logger> spdlog::stderr_logger_mt(const std::string& name) | 创建多线程安全的错误控制台(stderr)日志器 |
std::shared_ptr<logger> spdlog::stdout_color_logger_mt(const std::string& name) | 创建多线程安全的彩色控制台日志器 |
std::shared_ptr<logger> spdlog::basic_logger_mt(const std::string& name, const filename_t& filename) | 创建多线程安全的基本文件日志器(日志追加到文件) |
std::shared_ptr<logger> spdlog::rotating_logger_mt(const std::string& name, const filename_t& filename, size_t max_size, size_t max_files) | 创建多线程安全的滚动文件日志器(按大小切割) |
std::shared_ptr<logger> spdlog::daily_logger_mt(const std::string& name, const filename_t& filename, int hour = 0, int minute = 0) | 创建多线程安全的每日滚动日志器(按时间切割) |
std::shared_ptr<logger> spdlog::logger(const std::string& name, sink_ptr single_sink) | 自定义日志器(关联单个 Sink) |
std::shared_ptr<logger> spdlog::logger(const std::string& name, sinks_init_list sinks) | 自定义日志器(关联多个 Sink) |
-
- 日志记录器管理
函数原型 | 功能描述 |
---|---|
std::shared_ptr<logger> spdlog::get(const std::string& name) | 根据名称获取已创建的日志器 |
void spdlog::set_default_logger(std::shared_ptr<logger> logger) | 设置全局默认日志器(用于 spdlog::info() 等全局函数) |
void spdlog::drop(const std::string& name) | 销毁指定名称的日志器 |
void spdlog::drop_all() | 销毁所有日志器 |
void spdlog::shutdown() | 销毁所有日志器并释放资源(建议程序退出前调用) |
-
- 日志记录器配置
函数原型(logger 成员函数) | 功能描述 |
---|---|
void set_level(level::level_enum log_level) | 设置日志级别(如 level::debug ) |
void set_pattern(const std::string& pattern, pattern_time_type time_type = pattern_time_type::local) | 设置日志格式(如 "%Y-%m-%d %H:%M:%S [%l] %v" ) |
void set_formatter(std::unique_ptr<formatter> formatter) | 设置自定义日志格式化器 |
void flush_on(level::level_enum log_level) | 当日志级别达到指定值时自动刷新(如 flush_on(level::err) ) |
void flush() | 手动刷新日志(立即写入输出目标) |
输出目标(Sink)相关函数
Sink 是日志的输出目标(如控制台、文件),日志记录器需关联一个或多个 Sink 才能输出日志。
-
- 常用 Sink 创建
- Sink 同样有
_mt
(多线程)和_st
(单线程)版本,需与日志器线程安全类型匹配。
函数/类 | 功能描述 |
---|---|
std::make_shared<sinks::stdout_sink_mt>() | 多线程安全的标准输出(stdout)Sink |
std::make_shared<sinks::stderr_sink_mt>() | 多线程安全的标准错误(stderr)Sink |
std::make_shared<sinks::stdout_color_sink_mt>() | 多线程安全的彩色控制台 Sink |
std::make_shared<sinks::basic_file_sink_mt>(const filename_t& filename, bool truncate = false) | 多线程安全的基本文件 Sink(truncate=true 表示覆盖文件) |
std::make_shared<sinks::rotating_file_sink_mt>(const filename_t& base_filename, size_t max_size, size_t max_files, bool truncate = false) | 多线程安全的滚动文件 Sink(按大小切割) |
std::make_shared<sinks::daily_file_sink_mt>(const filename_t& base_filename, int hour, int minute, bool truncate = false) | 多线程安全的每日滚动文件 Sink(按时间切割) |
std::make_shared<sinks::syslog_sink_mt>(const std::string& ident, int option = 0, int facility = LOG_USER) | 多线程安全的系统日志 Sink(如 Linux syslog) |
-
- Sink 配置(成员函数)
函数原型 | 功能描述 |
---|---|
void set_level(level::level_enum log_level) | 设置当前 Sink 的日志级别(仅处理该级别及以上的日志) |
void set_formatter(std::unique_ptr<formatter> formatter) | 为当前 Sink 设置独立的日志格式(覆盖日志器的全局格式) |
void flush() | 手动刷新该 Sink 的日志缓存 |
组合使用 Logger 和 Sink
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sink.h"
#include "spdlog/sinks/basic_file_sink.h"int main() {// 1. 创建两个 Sink(彩色控制台 + 文件)auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log");// 2. 为 Sink 单独设置日志级别(可选)console_sink->set_level(spdlog::level::info); // 控制台只输出 info 及以上file_sink->set_level(spdlog::level::debug); // 文件输出 debug 及以上// 3. 创建日志器并关联两个 Sinkspdlog::sinks_init_list sinks = {console_sink, file_sink};auto logger = std::make_shared<spdlog::logger>("multi_sink_logger", sinks);// 4. 配置日志器logger->set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v"); // 全局格式logger->set_level(spdlog::level::debug); // 日志器最低级别// 5. 输出日志(同时到控制台和文件)logger->debug("debug 日志(仅文件输出)");logger->info("info 日志(控制台和文件均输出)");return 0;
}
总结
- Logger 函数:负责创建、管理日志记录器,以及配置日志级别、格式等全局属性。
- Sink 函数:负责创建具体的输出目标(控制台/文件等),并支持独立配置级别和格式。
- 灵活组合 Logger 和 Sink 可实现复杂的日志需求(如多目标输出、分级过滤)。
4.2 常用函数
init_thread_pool()
在 spdlog 日志库中,init_thread_pool()
是用于初始化异步日志线程池的函数,主要用于支持异步日志模式(将日志写入操作放入后台线程执行,避免阻塞主线程)。以下是其详细说明和使用方法:
异步日志原理:普通同步日志会在调用日志函数(如 info()
、error()
)时直接写入输出目标(控制台/文件),可能阻塞主线程;异步日志则将日志消息先存入内存队列,由后台线程异步处理写入,主线程可立即返回。
init_thread_pool()
:负责创建后台线程和消息队列,为所有异步日志器提供底层支持。必须在创建第一个异步日志器前调用。
void spdlog::init_thread_pool(size_t queue_size, size_t thread_count = 1);
queue_size
:日志消息队列的最大容量(单位:条)。若队列满,新日志会根据策略处理(默认阻塞或丢弃,取决于配置)。thread_count
:后台线程数量(默认 1,通常无需修改,多线程可能引发锁竞争)。
使用步骤
1. 初始化线程池
在程序启动时(创建任何异步日志器前)调用 init_thread_pool()
:
#include "spdlog/async.h" // 异步日志核心头文件
#include "spdlog/sinks/stdout_color_sinks.h"int main() {// 初始化异步线程池:队列容量 8192 条,1 个后台线程spdlog::init_thread_pool(8192, 1);// 后续创建异步日志器...return 0;
}
2. 创建异步日志器
使用 spdlog::async_factory
或 spdlog::async_factory_st
创建异步日志器(_mt
表示多线程安全,_st
表示单线程):
// 创建异步控制台日志器(多线程安全)
auto async_console = spdlog::stdout_color_logger_mt<spdlog::async_factory>("async_console");// 创建异步滚动文件日志器
auto async_file = spdlog::rotating_logger_mt<spdlog::async_factory>("async_file", // 日志器名称"logs/async.log", // 日志文件路径1024 * 1024 * 5, // 单个文件大小(5MB)3 // 备份文件数量
);
3. 使用异步日志器
调用日志函数的方式与同步日志器一致,但内部会通过线程池异步处理:
async_console->info("这是一条异步日志(不会阻塞主线程)");
async_file->error("错误代码: {}", 404);
4. 程序退出时清理
调用 spdlog::shutdown()
确保后台线程完成所有日志写入并释放资源:
// 程序退出前
spdlog::shutdown();
关键配置与注意事项
1. 队列大小选择
- 过小可能导致队列满(日志丢失或阻塞),过大会浪费内存。
- 建议值:中小型程序
4096
~8192
,高并发程序16384
~65536
。
2. 线程数量
- 通常设为
1
即可:多线程写入同一目标(如文件)会引入锁竞争,反而降低性能。 - 特殊场景(如多个独立日志文件)可适当增加,但需测试性能影响。
3. 日志满队列策略
默认情况下,队列满时会阻塞主线程直到有空闲位置。可通过 set_async_mode()
自定义策略:
// 初始化线程池时指定满队列策略(非阻塞,丢弃新日志)
spdlog::init_thread_pool(8192, 1);
spdlog::set_async_mode(spdlog::async_overflow_policy::overrun_oldest);
// overrun_oldest:丢弃最旧的日志;block:阻塞(默认)
4. 与同步日志器的共存
- 异步线程池初始化后,仍可创建同步日志器(不影响)。
- 同步日志器的写入操作会直接执行,不受线程池控制。
5. 线程安全
- 异步日志器默认线程安全(
_mt
后缀),可在多线程中放心使用。 - 单线程场景可使用
_st
后缀的日志器(性能略高,但不线程安全)。
示例
#include "spdlog/async.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include <thread>
#include <chrono>void async_log_demo() {// 初始化异步线程池spdlog::init_thread_pool(8192, 1);// 设置队列满时策略:覆盖最旧日志spdlog::set_async_mode(spdlog::async_overflow_policy::overrun_oldest);// 创建异步控制台日志器auto console = spdlog::stdout_color_logger_mt<spdlog::async_factory>("console");// 创建异步文件日志器auto file_logger = spdlog::rotating_logger_mt<spdlog::async_factory>("file_logger", "logs/async.log", 1024*1024*5, 3);// 多线程输出日志(模拟高并发)auto log_task = [&](int thread_id) {for (int i = 0; i < 100; ++i) {console->info("线程 {}: 第 {} 条日志", thread_id, i);file_logger->debug("线程 {}: 调试信息 {}", thread_id, i);}};std::thread t1(log_task, 1);std::thread t2(log_task, 2);t1.join();t2.join();// 关闭日志器,确保所有日志写入完成spdlog::shutdown();
}int main() {async_log_demo();return 0;
}
flush_every()
spdlog::flush_every()
是 spdlog 库中用于配置定时自动刷新日志的函数,主要用于异步日志模式,确保日志消息在指定时间间隔内被强制写入输出目标(如文件),避免因程序崩溃导致日志丢失。
在异步日志模式中,日志消息先存入内存队列,由后台线程异步处理。默认情况下,后台线程会在队列有消息时尽快处理,但极端情况下(如日志量极少),消息可能长时间停留在内存中。
flush_every()
用于设置最大间隔时间,无论队列是否有消息,后台线程都会每隔指定时间自动刷新一次日志缓存,将所有未写入的消息强制输出到目标(如文件)。
void spdlog::flush_every(std::chrono::duration<int64_t, std::nano> interval);
interval
:自动刷新的时间间隔,需使用 C++ 标准库的std::chrono
时间单位(如std::chrono::seconds(3)
表示 3 秒)。
使用方法
需在初始化异步线程池后、创建异步日志器前调用:
#include "spdlog/async.h"
#include "spdlog/sinks/basic_file_sink.h"
#include <chrono> // 用于时间单位int main() {// 1. 初始化异步线程池spdlog::init_thread_pool(8192, 1);// 2. 设置定时刷新:每 5 秒自动刷新一次spdlog::flush_every(std::chrono::seconds(5));// 3. 创建异步日志器(写入文件)auto async_file_logger = spdlog::basic_logger_mt<spdlog::async_factory>("async_file", "logs/auto_flush.log");// 4. 输出日志(即使日志量少,也会每 5 秒自动写入文件)async_file_logger->info("这条日志会在 5 秒内被刷新到文件");// 模拟程序运行(超过 5 秒,确保刷新生效)std::this_thread::sleep_for(std::chrono::seconds(10));// 5. 程序退出前关闭日志器spdlog::shutdown();return 0;
}
- 仅适用于异步日志:
flush_every()
是异步日志线程池的配置,对同步日志器无效。 - 补充刷新机制:与
logger->flush_on(level)
(按日志级别刷新)不冲突,二者可同时生效:flush_on(level)
:当日志级别达到指定值(如error
)时立即刷新。flush_every(interval)
:无论级别,定时强制刷新。
- 全局生效:一旦设置,对所有后续创建的异步日志器均有效。
适用场景
- 低频率日志场景:如程序长时间运行但日志量极少(如每隔几分钟一条),避免日志长时间滞留内存。
- 高可靠性需求:如金融交易、系统监控,确保日志及时写入磁盘,即使程序意外崩溃也能保留大部分日志。
- 文件日志优化:减少磁盘 I/O 次数的同时,平衡日志实时性(避免频繁刷新影响性能,也避免太久不刷新导致丢失)。
注意事项
-
时间间隔选择:
- 过短(如 100ms)会增加磁盘 I/O 频率,影响性能。
- 过长(如 1 小时)可能导致崩溃时丢失大量日志。
- 建议值:普通场景 3~30 秒,关键场景 1~5 秒。
-
与手动刷新配合:仍可通过
logger->flush()
手动强制刷新,不受定时刷新影响。 -
线程池依赖:必须在
init_thread_pool()
之后调用,否则配置不生效。
spdlog::flush_every()
是异步日志模式下保障日志可靠性的重要函数,通过定时自动刷新机制,平衡了性能与日志完整性。在对日志实时性有要求的场景(如长期运行的服务程序),建议结合业务需求设置合理的刷新间隔。
4.3 参数
VA_ARGS
__VA_ARGS__
是 C/C++ 中的一个预处理器宏(Preprocessor Macro),用于在可变参数宏(Variadic Macros) 中表示“所有传入的可变参数”。它允许定义能够接收不确定数量参数的宏,是实现灵活日志打印、函数包装等功能的常用工具。
可变参数宏:指定义时参数数量不固定的宏,语法上通过 ...
(省略号)声明可变参数部分。
__VA_ARGS__
:在宏的展开体中,__VA_ARGS__
会被直接替换为调用宏时传入的所有可变参数。
适用场景:日志打印(如 LOG("错误码: %d, 信息: %s", 404, "文件不存在")
)、函数调用包装、批量代码生成等。
核心语法
- 基础可变参数宏定义
// 定义格式:宏名(固定参数, ...) -> 展开体中用 __VA_ARGS__ 代替可变参数
#define 宏名(固定参数, ...) 展开逻辑中使用 __VA_ARGS__
- 无固定参数的可变参数宏
若宏没有固定参数,直接用...
声明,且__VA_ARGS__
需紧跟在##
后(避免空参数时的语法错误,C99 及以上支持):
// 正确:无固定参数的可变参数宏
#define LOG(...) printf(__VA_ARGS__)
典型使用场景
- 场景1:简单日志打印(最常用)
通过__VA_ARGS__
实现支持任意格式的日志输出,无需重复写printf
的格式控制逻辑:
#include <stdio.h>// 定义日志宏:打印时间(固定前缀)+ 可变参数(日志内容)
#define LOG_INFO(...) \printf("[INFO] %s:%d: ", __FILE__, __LINE__); // 固定前缀(文件名、行号)\printf(__VA_ARGS__); // 可变参数(日志内容)\printf("\n") // 换行int main() {int user_id = 123;char* username = "Alice";// 调用宏:传入 2 个可变参数(格式字符串 + 具体值)LOG_INFO("用户登录: id=%d, name=%s", user_id, username);// 调用宏:传入 1 个可变参数(仅字符串)LOG_INFO("程序启动成功");return 0;
}
展开结果(假设代码在 main.c
的第 15、18 行):
// 第15行调用展开为:
printf("[INFO] main.c:15: "); printf("用户登录: id=%d, name=%s", 123, "Alice"); printf("\n");// 第18行调用展开为:
printf("[INFO] main.c:18: "); printf("程序启动成功"); printf("\n");
运行输出:
[INFO] main.c:15: 用户登录: id=123, name=Alice
[INFO] main.c:18: 程序启动成功
- 场景2:函数包装(简化复杂调用)
用宏包装带多个参数的函数,减少重复代码。例如包装printf
并添加错误检查:
#include <stdio.h>
#include <errno.h>// 包装 printf,打印错误信息(若返回值 <0,提示错误码)
#define SAFE_PRINTF(...) \do { \int ret = printf(__VA_ARGS__); // 用 __VA_ARGS__ 传递所有参数给 printf \if (ret < 0) { \fprintf(stderr, "打印失败!错误码: %d\n", errno); \} \} while(0) // do-while(0) 确保宏在条件语句中正常展开int main() {SAFE_PRINTF("Hello, %s!\n", "World"); // 正常调用SAFE_PRINTF("数值: %d, 浮点数: %.2f\n", 100, 3.14); // 多参数调用return 0;
}
- 场景3:支持空参数(
##__VA_ARGS__
)
若宏可能被无参数调用(如LOG("仅提示")
),直接用__VA_ARGS__
可能导致语法错误(如多余的逗号)。此时需用##
(宏连接符)消除空参数的影响:
// 错误示例:无参数调用时会展开为 printf("日志: " ); (多余逗号)
#define LOG_BAD(msg, ...) printf("日志: " msg, __VA_ARGS__)// 正确示例:用 ##__VA_ARGS__ 消除空参数的逗号
#define LOG_GOOD(msg, ...) printf("日志: " msg, ##__VA_ARGS__)int main() {// LOG_BAD("测试"); // 错误:展开为 printf("日志: " "测试", ); (语法错误)LOG_GOOD("测试"); // 正确:展开为 printf("日志: " "测试");LOG_GOOD("用户: %s", "Bob"); // 正确:展开为 printf("日志: " "用户: %s", "Bob");return 0;
}
##
的作用:当__VA_ARGS__
为空时,##
会自动删除前面的逗号,避免语法错误。- 兼容性:
##__VA_ARGS__
是 GNU 扩展(GCC、Clang 支持),C99 标准不直接支持,但主流编译器(包括 VS2019+)均兼容。
总结
__VA_ARGS__
是 C/C++ 预处理器的核心特性,通过可变参数宏实现“一次定义,多参数调用”,尤其适合日志打印、函数包装等场景。使用时需注意:
- 用
##__VA_ARGS__
处理空参数,避免语法错误; - 结合预定义宏(
__FILE__
、__LINE__
)增强调试信息; - C++ 中优先选择可变参数模板,确保类型安全。
5. 注意事项
-
线程安全后缀:
- 日志器创建函数的后缀
_mt
(multi-thread)表示多线程安全,_st
(single-thread)表示单线程。多线程环境必须使用_mt
后缀,否则可能导致数据竞争。 - 示例:
stdout_color_mt()
(多线程安全控制台日志器)、basic_file_sink_st()
(单线程文件日志器)。
- 日志器创建函数的后缀
-
日志器生命周期:
- 避免在日志器销毁后继续使用(如全局日志器被
shutdown
后)。 - 建议在程序退出前调用
spdlog::shutdown()
,确保所有日志(尤其是异步日志)被写入目标。
- 避免在日志器销毁后继续使用(如全局日志器被
-
字符编码:
- Windows 下默认使用 ANSI 编码,若需输出 Unicode 字符(如中文),需使用宽字符版本的 sink(如
spdlog::sinks::stdout_color_sink_wmt
),并配合L"中文日志"
格式:auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_wmt>(); auto logger = std::make_shared<spdlog::logger>("unicode_logger", console_sink); logger->info(L"这是中文日志(宽字符)");
- Windows 下默认使用 ANSI 编码,若需输出 Unicode 字符(如中文),需使用宽字符版本的 sink(如
-
性能优化:
- 异步日志适合高并发场景,但会增加少量内存开销(队列和后台线程)。
- 避免在日志中输出大量数据(如大字符串、二进制数据),以免影响性能。