写C++的项目,日志库这一块几乎没有什么可争的,spdlog是绝大多数人的默认答案。体积小、头文件轻、格式化顺手、同步异步都支持,性能在同级别库中又足够能打。但很多Windows用户第一次接触spdlog,卡住的地方往往不是怎么用,而是怎么把它编译出来:CMake选项看着不多,选错了就是链接错误;VS里拿到的lib和调用方runtime不一致,也会莫名其妙报错。两年前我第一次在Windows上编spdlog就花了半个晚上,一路踩过来发现好多坑其实是共通的,所以这篇打算把整个过程整理清楚,拿一个v1.15.x版本的spdlog做例子,从下载源码开始,到集成进你自己的C++项目,全部过一遍。
1. 先想明白:你真的需要编译版spdlog吗
1.1 Header-only vs 编译版,到底怎么选
spdlog的特殊之处在于它支持header-only模式,include/spdlog目录下就是全部实现,理论上只要把头文件路径加进工程,就能直接#include <spdlog/spdlog.h>开用,不需要编译任何库。这种模式对于小工具、单可执行文件、日志量不大的项目非常友好,省去了CMake配置和运行时一致性的麻烦,是最快的上手路径。
但实际工作中,我遇到不少场景是header-only撑不住的。
第一个是编译时间。spdlog的模板格式化依赖fmt,每个包含spdlog头文件的编译单元都会实例化大量模板代码。小项目无所谓,一旦工程里有几十个模块都要打日志,你会明显感觉到全量编译变慢,增量编译也时不时被头文件改动拖垮。这时候把spdlog预编译成静态库,只在链接阶段挂上去,编译负担会小很多。
第二个是DLL边界问题。假如你的产品分成好几个动态库,每个DLL都用自己的header-only spdlog,log sink、线程池、日志统计就是各自一份,异步队列在不同模块之间也是隔离的。排查问题的时候,你可能会看到来自两个模块的日志顺序错乱,甚至各自输出到不同文件。而把spdlog编译成一个共享的dll,所有模块用同一份sink和线程池,日志行为就统一了。
第三个是团队规范。预编译版本可以把spdlog版本、fmt版本、字符集选项都锁定在一个构建脚本里,实习生和新同事拿到的是同一个库文件,不会因为各自引入不同代码造成诡异的ABI问题。
所以我的建议很简单:个人小项目、快速验证、不想折腾,直接用header-only;多模块工程、团队协作、追求稳定可复现的构建,就老老实实编译成库。下面这套流程就是针对后者。
1.2 2026年的spdlog需要哪些前置环境
2026年了,spdlog在Windows上编译这件事没有变得更复杂,反而因为Visual Studio和CMake的迭代变得更顺了。目前spdlog稳定分支还在v1.15.x,只要你拿到的不是特别激进的master分支,下面这套流程基本都能直接用。就算过几个月出了新tag,只要没有发布2.0这种大重写,CMake选项和代码接口不会有颠覆性变化。
编译spdlog需要准备这些:
- Visual Studio 2019或2022,必须安装“使用C++的桌面开发”工作负载,里面包含MSVC编译器、Windows SDK和CMake工具。
- CMake 3.20以上。Visual Studio自带CMake,你也可以在命令行单独装一个最新版,不用纠结版本够不够新。
- 一个称手的终端,Windows Terminal或者PowerShell都行,后面命令行编译步骤要用。
- C++标准建议至少C++17。spdlog官方说C++11能支持,但fmt模板在C++11下编译又慢又容易遇到模板推导奇怪的坑,2026年的工具链默认都支持C++17,直接抬高标准省心。
说到前置环境,不少朋友会问要不要单独准备fmt。这里明确一下:新版spdlog默认内置fmt,源码包里的include/spdlog/fmt目录就是内嵌的fmt头文件,库内部已经处理好了编译依赖,不需要你额外安装任何fmt组件。只有在极少数自定义构建场景下才考虑SPDLOG_FMT_EXTERNAL选项,普通项目别碰。
2. 下载源码与编译方案选型
2.1 从哪下载、下哪个版本
获取spdlog源码的常规渠道是GitHub仓库,具体到Windows命令行操作,我推荐用git clone指定分支,而不是直接下载全部代码。克隆出来的目录干净,后面方便写构建脚本。
git clone --branch v1.15.2 --depth 1 https://github.com/gabime/spdlog.git--depth 1是浅克隆,只把对应tag的代码拉下来,省流量也省时间。如果你不想装Git,直接去仓库的Releases页面下载对应tag的.zip或.tar.gz压缩包也行,注意不要下最新master分支的快照,master上经常有临时提交,可能把构建流程弄出偏差。
特别多说一句,有些网络环境慢,或者公司内网无法直接访问外部仓库,可以在内网专线环境提前把源码包准备好,或者使用镜像站同步。源码本身没有平台限制,拿到手能用就行。
解压或者克隆完成后,你会看到这样的目录结构:
include/spdlog:全部头文件,header-only模式的大部分实现就在这里。src/spdlog.cpp:编译版的核心源文件,它把重要的sink、registry、async逻辑编译进库。example/:官方示例工程。tests/:单元测试,编译库的时候通常不需要。cmake/:CMake辅助模块。CMakeLists.txt:构建入口。
搞清楚这个结构很重要:后面编译出来的库本质上是把src/spdlog.cpp和一些公共sink编译成lib,而头文件是你写代码时需要的接口定义。库和头文件必须配套使用,版本混搭是很多链接错误的源头。
2.2 核心CMake选项一次讲明白
spdlog的CMake选项不算多,但每一个都有坑。我把常用的列出来,直接给结论:
| CMake选项 | 作用 | 我的建议 |
|---|---|---|
SPDLOG_BUILD_EXAMPLE | 是否编译官方示例 | 第一次编译建议OFF,省编译时间 |
SPDLOG_BUILD_TESTS | 是否编译单元测试 | 普通使用OFF |
SPDLOG_BUILD_SHARED | 编译动态库而非静态库 | 需要DLL共享时ON |
SPDLOG_INSTALL | 是否生成install安装规则 | 建议ON,方便后续find_package |
SPDLOG_NO_EXCEPTIONS | 禁用C++异常 | 保持默认OFF |
SPDLOG_WCHAR_CHARS | 启用wchar_t日志接口 | Windows下按需开启 |
SPDLOG_WCHAR_FILENAMES | 允许sink接收宽字符文件名 | 需要处理中文路径时尝试 |
SPDLOG_USE_STD_FORMAT | 用C++20 std::format替代内置fmt | C++20项目才考虑 |
为什么我建议把SPDLOG_INSTALL设为ON?因为Windows下你多半不会一直停留在源码目录里使用,而是希望把编译出来的产物放到一个公共路径,供多个工程引用。这个选项会生成install规则,让你一键把头文件、库文件和CMake配置文件拷贝到指定目录,后续find_package(spdlog)才能找到它。
SPDLOG_BUILD_EXAMPLE和SPDLOG_BUILD_TESTS默认值比较复杂,不同版本可能不一样,但原则是一样的:编译库本身不需要它们。确认为OFF可以少编一堆用不到的东西。
2.3 运行时库MT/MD是最大的隐性问题
Windows下链接C++库,最烦、最容易踩的就是运行时库不一致。VS工程里有个设置叫“运行库”,选项有/MT、/MTd、/MD、/MDd,对应多线程静态链接和动态链接的Release/Debug版本。
这个概念我打个比方:MSVC运行时相当于一套公共基础设施,你的exe和spdlog.pdb好比两个承包商,如果他们各自用的是不同版本的基础设施,接水管的时候接口对不上,链接器就会直接报错。实际情况中非常典型的是LNK2038,错误文本里有RuntimeLibrary mismatch,几乎都是这个原因。
因此,编译spdlog的时候一定要和最终调用它项目的“运行库”保持一致:
- 你主力工程用
/MD、/MDd,编译spdlog也得用动态运行时。 - 你主力工程用
/MT、/MTd,编译spdlog也得用静态运行时。
注意,/MT和/MD本身是可以交叉的,但由于CRT的全局状态不同,链接阶段拷出来的符号实现可能重复或冲突,最后导致各种古怪问题。我在实际项目里遇到过同事把spdlog编成/MD,主程序用/MT,一链接就报错,后来统一成/MD才消停。所以编译spdlog前,先想清楚你的工程统一用哪个运行库。
CMake的Visual Studio生成器默认会按主机的配置生成多套运行库选择(常驻/MD或/MTd),但静态库本身不会把运行库打包进去,最终是否一致取决于调用工程。最稳妥的做法是:在最终工程的链接器设置里,保持与自己其他第三方库一致,不要临时改spdlog一家的。
3. 实操:从源码到lib和dll
3.1 小白友好路线:CMake GUI + Visual Studio
如果你是第一次碰CMake,推荐先用CMake GUI走一遍,好处是每一步都看得到选项,不容易漏配置。
打开cmake-gui,在“Where is the source code”填spdlog源码目录,在“Where to build the binaries”填一个构建目录,比如spdlog/build-vs2022-x64。点Configure,弹出生成器选择窗口,选“Visual Studio 17 2022”,平台下拉选x64,下面的可选工具集默认就行,点Finish。
这时候CMake会扫描出所有选项,红的条目就是本次新增的。我们要做的事:
- 把
SPDLOG_BUILD_EXAMPLE取消勾选。 - 把
SPDLOG_BUILD_TESTS取消勾选。 - 把
SPDLOG_INSTALL勾上。 - 点
CMAKE_INSTALL_PREFIX条目,把它改成你希望安装到的绝对路径,例如C:/libs/spdlog。Windows下路径反斜杠最好换成正斜杠,否则某些脚本可能出问题。
确认没问题后点Generate,再点Open Project,VS会直接打开生成的s spdlog.sln。在VS解决方案管理器里找INSTALL项目,右键“重新生成”。注意顶部配置选Release x64,别用默认Debug编一套存起来,除非你确实需要调试版本的库。
生成完成后,去你的CMAKE_INSTALL_PREFIX目录看,里面应该有include和lib两个核心目录,以及lib/cmake/spdlog里的CMake配置文件。整个过程中如果卡在Configure阶段报错,十有八九是VS生成器没选对或者Windows SDK缺失,重新检查工作负载安装即可。
3.2 命令行方式:适合脚本化和批量构建
命令行方式是团队常用路线,因为好写脚本,版本升级时改动很小。推荐在“Developer PowerShell for VS 2022”里执行,这样环境变量自动带上了CMake、MSVC等工具。
首先是Visual Studio多配置生成器:
cmake -S . -B build-win64 -G "Visual Studio 17 2022" -A x64 ` -DSPDLOG_BUILD_EXAMPLE=OFF ` -DSPDLOG_BUILD_TESTS=OFF ` -DSPDLOG_INSTALL=ON ` -DCMAKE_INSTALL_PREFIX=C:/libs/spdlog-S指向spdlog源码目录,-B指向构建目录,-G指定VS生成器,-A指定64位平台。注意,这种生成器下-DCMAKE_BUILD_TYPE=Release是无效的,编译时用--config指定配置:
cmake --build build-win64 --config Release --target install--target install会直接执行安装,把产物放到C:/libs/spdlog。等价地你也可以手动打开VS编译INSTALL项目。
如果你更喜欢Linux那种单配置方式,可以用Ninja:
cmake -S . -B build-ninja -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` -DSPDLOG_BUILD_EXAMPLE=OFF ` -DSPDLOG_BUILD_TESTS=OFF ` -DSPDLOG_INSTALL=ON ` -DCMAKE_INSTALL_PREFIX=C:/libs/spdlog cmake --build build-ninja --target installNinja生成器下配置是单次的,CMAKE_BUILD_TYPE=Release会直接生效,后面不需要--config。缺点是不能一套配置同时编Debug和Release。我个人的习惯是团队脚本用VS生成器,个人速试时用Ninja,速度确实爽。
安装后的目录大概是这样的:
C:/libs/spdlog/ ├── include/spdlog/ ├── lib/ │ ├── cmake/spdlog/ │ └── spdlog.lib └── share/静态库编译出来一般是spdlog.lib,这个就是后面工程要链接的东西。
3.3 动态库DLL怎么编
需要做dll的场景主要有两个:一是多个exe/dll需要共享同一个spdlog实现,二是希望日志库和业务代码完全解耦,未来单独更新spdlog。编译动态库只需在上面的CMake配置中加一个选项:
-DSPDLOG_BUILD_SHARED=ON编译完成后,lib目录下会出现spdlog.lib(导入库)和spdlog.dll(动态库)。注意,这里spdlog.lib不是完整静态库,而是一个导入库,真正代码在spdlog.dll里。
用动态库的时候,使用方必须定义一个宏SPDLOG_SHARED_LIB,否则头文件里spdlog的导出导入宏走的是静态路径,链接时会出现一堆__declspec(dllimport)相关的符号找不到。这个错误极其常见,我见过不少人卡在这一步。
部署上,dll如果配置的是/MD,运行时还要带上VC运行库。另外dll必须和应用exe放在一起,或者放在系统能找到的路径,否则运行时会报找不到spdlog.dll。大型项目建议把dll统一丢到一个公共运行时目录,初始化时用SetDllDirectory或CMake的RUNTIME_OUTPUT_DIRECTORY控制拷贝行为。
4. 把编译好的spdlog集成进工程并跑起来
4.1 CMake集成方式:find_package
编译库最大的好处就是可以find_package。假设你按上面步骤把spdlog安装到了C:/libs/spdlog,新建一个自己的项目,CMakeLists可以这么写:
cmake_minimum_required(VERSION 3.20) project(spdlog_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关键:告诉CMake去哪找spdlog list(APPEND CMAKE_PREFIX_PATH "C:/libs/spdlog") find_package(spdlog REQUIRED) add_executable(app main.cpp) target_link_libraries(app PRIVATE spdlog::spdlog) # 如果你编译的是动态库,必须打开下面这行 # target_compile_definitions(app PRIVATE SPDLOG_SHARED_LIB)这里spdlog::spdlog是CMake导入目标,不仅带着include路径,还把spdlog的宏定义、链接库、依赖关系都封装好了。写代码时直接#include <spdlog/spdlog.h>就能用。
build方式:
cmake -S . -B build -DCMAKE_PREFIX_PATH=C:/libs/spdlog cmake --build build --config Release如果你的spdlog是按照默认静态库编译的,上面代码不需要额外宏;如果用了动态库,记得把target_compile_definitions那行打开,否则链接必炸。
4.2 Visual Studio传统方式:手动配VC++目录
有些人还在用老式VS工程,不写CMake,也没问题。整体操作三步:
- 打开工程属性,
VC++目录 -> 包含目录里加上C:/libs/spdlog/include。 VC++目录 -> 库目录里加上C:/libs/spdlog/lib。链接器 -> 输入 -> 附加依赖项里加spdlog.lib。
这个老流程最大的维护成本是:换了spdlog版本要手动改路径,不同工程之间全靠复制粘贴。另外,如果库是dll版本,还要在“预处理器 -> 预处理器定义”里加上SPDLOG_SHARED_LIB。相比之下CMake的导入目标杀掉这些脏活。
4.3 一个完整可运行的最小demo
写完配置,我们来写一个能验证库好不好用的demo。新建main.cpp:
#include <spdlog/spdlog.h> #include <spdlog/sinks/basic_file_sink.h> #include <spdlog/sinks/rotating_file_sink.h> #include <spdlog/async.h> #include <spdlog/sinks/stdout_color_sinks.h> int main() { // 基本用法:默认logger输出到控制台 spdlog::set_level(spdlog::level::debug); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v"); spdlog::info("Hello, {}!", "spdlog"); spdlog::debug("debug message, number={}", 42); spdlog::warn("this is a warning"); // 文件logger:写入单一日志文件 auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/basic.log"); file_logger->info("file logger message"); // 轮转文件logger:单文件5MB,保留3个备份 auto rot_logger = spdlog::rotating_logger_mt("rot_logger", "logs/rot.log", 5 * 1024 * 1024, 3); for (int i = 0; i < 100; ++i) { rot_logger->info("rotating message {}", i); } // 异步logger:先初始化线程池,队列8192条,1个后台线程 spdlog::init_thread_pool(8192, 1); auto async_logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>( "async_logger", "logs/async.log"); async_logger->info("async message {}", 1); async_logger->info("async message {}", 2); // 程序结束时必须调用shutdown,否则异步队列可能来不及flush spdlog::shutdown(); return 0; }解释一下各段代码的意图:
basic_logger_mt是最普通的文件logger,适合单进程写一个固定文件。rotating_logger_mt会在文件达到大小上限后自动切新文件,避免单个日志文件无限膨胀。- 异步logger必须配合
init_thread_pool使用,队列长度按你的峰值日志量估计,8192一般够用。如果队列被写满,async_logger->info会临时降级为同步写,以保证日志不丢,但代价是调用线程卡顿。 shutdown()也是很多新手容易漏的点,它会把还没写盘的日志flush掉,并销毁线程池。不调用的话,main函数结束时异步线程可能在忙,日志确实可能丢一部分。
编译运行后,你会看到控制台彩色输出,logs目录下生成basic.log、rot.log、async.log三个文件。到这里,编译好的spdlog库就真正为你的工程服务了。
5. Windows上常见的坑和我的处理习惯
5.1 高频编译链接错误速查表
我搜集了几个Windows下最常遇到、论坛里反复出现的报错,直接整理成表格,你可以收藏起来当排查手册:
| 报错信息 | 常见原因 | 解决办法 |
|---|---|---|
C4996 'fopen': This function or variable may be unsafe | VS安全检查导致spdlog内部fopen编译不通过 | 目标工程加_CRT_SECURE_NO_WARNINGS预定义 |
LNK2038: mismatch detected for 'RuntimeLibrary' | 运行时库/MT和/MD不匹配 | 统一所有编译单元的运行库设置 |
无法解析的外部符号 fmt::v...::vformat | spdlog版本和外部fmt版本冲突 | 使用内置fmt,或必须在同一spdlog版本下 |
LNK2001: 无法解析的外部符号 spdlog::details::... | 动态库编译但未定义SPDLOG_SHARED_LIB | 编译器预处理加SPDLOG_SHARED_LIB |
cannot open file 'spdlog.lib' | 库目录没配好,或编的是dll但路径没加上 | 检查VC++目录/CMake linker路径 |
| 控制台中文乱码 | Windows控制台代码页不是UTF-8 | chcp 65001或代码里用SetConsoleOutputCP(CP_UTF8) |
其中C4996在Debug配置下更容易出现,往往是VS把安全警告升级为了“致命”,其实加一个宏就能静音。LNK2038是最需要理解的,它不像其他错误有明确的外部符号,而是运行时库级别的校验,问题往往不在spdlog本身,而在调用方工程的其他第三方库身上,需要全局排序。
5.2 性能一线经验和日志调优
spdlog标称性能很好,但前提是你正确地使用了它。实际测试中,Release配置下同步写文件能达到每秒几十万到上百万条的量级,写控制台会慢很多,Debug配置更是直接掉一个档次。所以做性能对比一定要在Release下做,否则数据失真。
几个实际经验:
- 不要把文件logging当成零成本。每条日志都走磁盘IO,普通机械盘和固态盘差别很大,异步队列能有效把磁盘IO和业务线程解耦。
- 用
flush_on控制flush策略。默认spdlog不一定每条日志都刷盘,这是性能关键。想要可靠性,可以spdlog::flush_on(spdlog::level::err),只在错误级别时强制刷盘;如果每条都flush,吞吐会大幅下降。 - 控制台logger因为要处理彩色输出和终端交互,是性能瓶颈,生产环境不要用控制台logger记大量日志。
- 异步线程数量不是越多越好。我见过有人开8个异步线程记日志,结果锁竞争反而比单线程还差。日志队列是IO密集型,1~2个后台线程通常已经足够。
5.3 几个让我省心的长期习惯
最后分享几个我自己形成的习惯,算不上标准答案,但确实降低了日常维护成本:
第一,把编译好的spdlog固定放在一个公共目录,比如C:/libs/spdlog,然后在自己的CMake预设里统一CMAKE_PREFIX_PATH。这样每个新工程只要引同一套库,不用临时去翻源码目录。升级spdlog时,在同一路径重新编译安装一次,所有工程自动切到新版。
第二,源码用浅克隆拉到本地后,我会顺手把CMakeLists.txt里选过的选项记录成一个build.cmd或者CMakePresets.json,下次升级版本直接照着跑,不浪费时间去回忆当时怎么选的。CMakePresets这种文件最好放进版本库,团队其他人也能复用。
第三,强烈建议不要把第三方库的源码直接拖进主工程一起编。头文件库还好,spdlog这种编译库如果混源码,很容易出现某个人改了spdlog某个宏导致全局行为变化。编译成库、锁版本、代码只依赖spdlog::spdlog接口,是走编译版路线最大的收益。
如果你只是做个小工具,直接#include <spdlog/spdlog.h>的header-only模式完全够用。但一旦踏进团队协作、模块化架构、日志需要统一管理的场景,按本文流程把spdlog在Windows上编译成库安装好,后面每一步都会顺畅很多。实测下来这整套流程在Visual Studio 2022和最新spdlog版本上是稳定可复现的,放心照着做就行。