简介:这份资源是面向Windows平台C++开发者的glog日志库集成包,适合需要在Visual Studio 2017环境下为项目引入高效日志系统的中高级开发者。glog作为Google开源的跨平台日志工具,支持INFO、WARNING、ERROR、FATAL多级别输出、堆栈回溯、VLOG调试日志及日志速率限制等特性,能有效支撑大规模系统的运行状态跟踪与异常排查。资源包共13个文件,以5个cmake构建脚本和5个h头文件为主,另含1个lib静态库、1个dll动态库及1个pc配置文件,整体约80KB,覆盖从编译配置到链接使用的完整依赖。目前已有299人学习下载。借助其中的头文件与库文件,读者可快速完成glog在VS2017中的编译与集成,掌握日志初始化、宏调用及输出定制方法,为Windows应用构建稳定灵活的日志方案。
1. 为什么 Windows 上编译 glog 总翻车:一份能直接落地的日志库资源
如果你在 Windows 上写过 C++ 服务端程序,大概率经历过这样的场景:日志模块用 std::ofstream 手搓,跑几天之后文件句柄泄漏、多线程写日志内容交错、程序崩溃时最后几行关键信息没落盘。这时候你会想到 Google 的 glog——一个在 Linux 上几乎开箱即用的 C++ 日志库。但把 glog 搬到 Windows 上,事情就变得微妙了:CMake 配置报错、gflags 依赖找不到、动态库导出符号缺失、Debug 和 Release 混编直接链接失败。这份 glog for windows 资源,就是针对这些具体问题整理出来的可编译工程包,包含适配 Windows 的 CMake 脚本、预编译配置和最小验证示例。它适合两类人:一是需要在 Windows 原生环境集成 glog 的 C++ 开发者,二是想拿 glog 源码在 MSVC 下跑通、理解其跨平台差异的工程师。下面按「先搞清楚它是什么 → 怎么编译集成 → 坑在哪 → 怎么验证」的顺序拆开讲。
2. glog 在 Windows 上的编译链路:从源码到静态库的完整操作
2.1 先理解 glog 的依赖结构,别急着敲 cmake
glog 本身不是一个零依赖的库。它依赖 gflags 来处理命令行参数解析,虽然在较新版本中 gflags 变成了可选依赖,但如果你要用 glog 的--logtostderr、--v这类命令行控制能力,gflags 还是得编。Windows 上编译 glog 的第一道坎就在这里:gflags 和 glog 的 CMake 配置需要保持一致的运行时库选项(MT/MD),否则链接阶段会报LNK2038运行时库不匹配。
常见做法是先把 gflags 编译成静态库,再编译 glog。我一般会建一个统一的构建目录,把两个库的源码放在同级路径下,用同一个 CMake 工具链文件控制编译选项。这样做的原因是 Windows 下 MSVC 的运行时库选项(/MT对应静态运行时,/MD对应动态运行时)必须全局一致,混用就是给自己埋雷。
资源包里通常已经包含了适配好的 CMakeLists.txt,但你需要确认几个关键变量:CMAKE_INSTALL_PREFIX指向你打算安装的路径,BUILD_SHARED_LIBS决定编静态库还是动态库,WITH_GFLAGS控制是否启用 gflags 支持。这三个变量决定了后续集成方式,建议在第一次编译前就定好,不要中途改。
2.2 用 CMake 生成 MSVC 工程并编译
假设你已经把 glog 源码解压到D:\libs\glog,gflags 在D:\libs\gflags。打开 VS 自带的 x64 Native Tools Command Prompt,按下面的步骤走:
# 先编译 gflags,安装到统一前缀 cd D:\libs\gflags mkdir build && cd build cmake .. -G "Visual Studio 17 2022" -A x64 ^ -DCMAKE_INSTALL_PREFIX=D:\libs\install ^ -DBUILD_SHARED_LIBS=OFF ^ -DBUILD_TESTING=OFF cmake --build . --config Release --target install# 再编译 glog,指向同一个安装前缀 cd D:\libs\glog mkdir build && cd build cmake .. -G "Visual Studio 17 2022" -A x64 ^ -DCMAKE_INSTALL_PREFIX=D:\libs\install ^ -DCMAKE_PREFIX_PATH=D:\libs\install ^ -DBUILD_SHARED_LIBS=OFF ^ -DWITH_GFLAGS=ON ^ -DWITH_UNWIND=OFF ^ -DBUILD_TESTING=OFF cmake --build . --config Release --target install第一段命令里-G "Visual Studio 17 2022"指定生成器,-A x64指定目标架构,BUILD_SHARED_LIBS=OFF表示编静态库。CMAKE_INSTALL_PREFIX统一指向D:\libs\install,这样 gflags 的头文件和 lib 会装到同一个地方,glog 编译时通过CMAKE_PREFIX_PATH就能找到。
第二段命令里WITH_GFLAGS=ON启用 gflags 支持,WITH_UNWIND=OFF是因为 Windows 上没有 libunwind,开了反而报错。BUILD_TESTING=OFF跳过测试用例编译,能省不少时间,也避免测试代码在 Windows 上的兼容问题。
编译完成后,D:\libs\install下会有include和lib两个目录。include里是glog/logging.h等头文件,lib里是glog.lib和gflags_static.lib(或类似命名)。这两个 lib 文件就是你集成时要链接的。
注意:如果你用的是 Debug 配置,需要把上面的
--config Release改成--config Debug,并且确保你的项目也使用 Debug 配置。Debug 和 Release 的 lib 不能混用,这是 MSVC 的基本规则,但每年还是有人在这翻车。
2.3 在项目中集成 glog 并写第一行日志
编译出库之后,集成到自己的项目里只需要三步:配置头文件路径、配置 lib 路径、链接 lib。以 CMake 项目为例:
# 在你的项目 CMakeLists.txt 中 find_package(glog REQUIRED) find_package(gflags REQUIRED) add_executable(my_app main.cpp) target_link_libraries(my_app PRIVATE glog::glog gflags::gflags_static)如果find_package找不到,就手动指定路径:
set(GLOG_ROOT "D:/libs/install") target_include_directories(my_app PRIVATE ${GLOG_ROOT}/include) target_link_directories(my_app PRIVATE ${GLOG_ROOT}/lib) target_link_libraries(my_app PRIVATE glog gflags_static)然后写一个最小验证程序:
#include <glog/logging.h> int main(int argc, char* argv[]) { // 初始化 glog,argv[0] 作为日志文件名的前缀 google::InitGoogleLogging(argv[0]); // 设置日志输出目录 google::SetLogDestination(google::GLOG_INFO, "./logs/"); // 同时输出到 stderr,方便调试 FLAGS_logtostderr = true; FLAGS_minloglevel = 0; // 输出所有级别日志 LOG(INFO) << "glog on Windows is running"; LOG(WARNING) << "this is a warning message"; LOG(ERROR) << "this is an error message"; // 条件日志:只在满足条件时输出 int x = 42; LOG_IF(INFO, x > 40) << "x is greater than 40, value=" << x; google::ShutdownGoogleLogging(); return 0; }InitGoogleLogging必须在任何 LOG 宏之前调用,否则日志不会输出。SetLogDestination指定日志文件目录,不设置的话默认输出到当前目录。FLAGS_logtostderr = true让日志同时打到控制台,开发阶段建议开着。LOG_IF是 glog 的条件日志宏,第一个参数是级别,第二个是条件表达式,条件为真才输出。
编译运行后,你应该能在./logs/下看到类似my_app.exe.INFO.20250101-120000.12345.log的文件。文件名格式是「程序名.级别.日期-时间.进程ID.log」,这是 glog 的默认命名规则。
3. Windows 平台特有的坑:从链接错误到日志文件锁
3.1 链接阶段的三个高频报错
第一个报错是LNK2038: 检测到"RuntimeLibrary"的不匹配。原因是你编译 glog 时用的运行时库选项和项目不一致。解决方法是统一:要么都用/MT(静态运行时),要么都用/MD(动态运行时)。在 CMake 里对应CMAKE_MSVC_RUNTIME_LIBRARY变量,可以显式设置:
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")第二个报错是LNK2019: 无法解析的外部符号,符号名里带gflags。原因是 glog 编译时启用了 gflags 支持,但你的项目没有链接 gflags 的 lib。解决方法是在target_link_libraries里加上 gflags 的库文件。
第三个报错是LNK1104: 无法打开文件"glog.lib"。原因是 lib 路径没配对,或者 Debug/Release 目录搞混了。CMake 生成的 lib 通常在build/Release/或build/Debug/下,install之后才会到统一前缀的lib目录。确认你链接的是 install 之后的路径。
3.2 日志文件被占用导致程序无法重启
Windows 和 Linux 在文件句柄管理上有个显著差异:Windows 下如果一个进程打开了日志文件,另一个进程尝试写同一个文件会直接失败。glog 默认按「程序名.级别.日期-时间.进程ID」命名日志文件,进程 ID 不同文件名就不同,所以多进程写同一目录通常没问题。但如果你手动调用了SetLogDestination指定了固定文件名,或者程序异常退出没有调用ShutdownGoogleLogging,就可能出现文件句柄未释放的情况。
现象是程序重启后日志不输出,或者输出到控制台但文件里没有内容。原因是上一个进程的日志文件句柄没有关闭,Windows 不允许两个进程同时写同一个文件。解决方法有两个:一是确保程序正常退出时调用ShutdownGoogleLogging(),二是用SetLogFilenameExtension给日志文件名加扩展名区分,避免文件名冲突。
我一般会在main函数里用 RAII 方式管理 glog 的生命周期:
class GlogInitializer { public: GlogInitializer(const char* program) { google::InitGoogleLogging(program); google::SetLogDestination(google::GLOG_INFO, "./logs/"); FLAGS_logtostderr = false; FLAGS_minloglevel = 0; } ~GlogInitializer() { google::ShutdownGoogleLogging(); } }; int main(int argc, char* argv[]) { GlogInitializer glog_init(argv[0]); LOG(INFO) << "program started"; // ... 业务逻辑 return 0; }这样即使中间抛异常,析构函数也会保证ShutdownGoogleLogging被调用。
3.3 中文路径和中文日志内容乱码
glog 在 Windows 上写日志文件时,如果路径包含中文,或者日志内容包含中文,可能出现乱码。原因是 glog 内部使用fopen打开文件,Windows 的fopen默认使用 ANSI 编码,而你的源码文件可能是 UTF-8 编码。
解决路径中文问题:把日志目录设成纯英文路径,比如D:/logs/而不是D:/日志/。这是最省事的做法。
解决内容中文问题:在写日志之前把字符串转成 GBK 编码,或者用google::SetLogDestination之外的方式自定义日志输出。更彻底的做法是修改 glog 源码中的文件打开方式,用_wfopen替代fopen,但这需要改源码重新编译。我一般建议项目里统一用英文日志内容,中文日志通过其他渠道输出,避免在这件事上花太多时间。
3.4 Debug 和 Release 混编导致的玄学崩溃
这个坑比较隐蔽:你的项目是 Debug 配置,但链接的 glog 是 Release 版本。编译链接都能过,但运行时会随机崩溃,崩溃位置在 glog 内部的字符串处理或内存分配处。原因是 Debug 和 Release 的 STL 容器内存布局不同,std::string在两种配置下的对象大小可能不一样,跨模块传递就会踩内存。
现象是程序在 LOG 语句处崩溃,调用栈显示在std::basic_string的析构或拷贝构造里。原因是 glog 库和你的项目使用了不同的 STL 配置。解决方法是确保 glog 的编译配置和你的项目完全一致:Debug 对 Debug,Release 对 Release,/MT对/MT,/MD对/MD。
注意:如果你不确定项目用的是什么配置,可以在 VS 里看「项目属性 → C/C++ → 代码生成 → 运行时库」这一项。glog 编译时也要设成一样的值。
4. 验证 glog 集成是否成功:三个可复现的检查点
4.1 检查点一:日志文件是否按预期生成
编译运行第 2 章的最小示例后,去./logs/目录下看文件。成功的标志是:文件名格式为my_app.exe.INFO.日期-时间.进程ID.log,文件内容包含你写的三行日志,级别分别是 I、W、E。如果文件没生成,先检查FLAGS_logtostderr是不是设成了 true——设成 true 时日志只输出到控制台,不写文件。如果文件生成了但内容为空,检查FLAGS_minloglevel是不是设得太高,默认是 0(INFO),设成 1 就只输出 WARNING 及以上。
4.2 检查点二:日志级别控制是否生效
修改代码,在InitGoogleLogging之后加上:
FLAGS_minloglevel = 1; // 只输出 WARNING 及以上重新编译运行,你会发现 INFO 级别的日志不再输出,WARNING 和 ERROR 正常。这说明级别控制生效了。再把FLAGS_minloglevel改成 2,WARNING 也不输出了,只剩 ERROR。这个检查点的作用是确认 glog 的运行时配置链路是通的,不是只编译链接通过就完事。
4.3 检查点三:多线程环境下日志是否交错
写一个多线程测试:
#include <glog/logging.h> #include <thread> #include <vector> void worker(int id) { for (int i = 0; i < 100; ++i) { LOG(INFO) << "thread " << id << " iteration " << i; } } int main(int argc, char* argv[]) { google::InitGoogleLogging(argv[0]); google::SetLogDestination(google::GLOG_INFO, "./logs/"); FLAGS_logtostderr = false; std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back(worker, i); } for (auto& t : threads) { t.join(); } google::ShutdownGoogleLogging(); return 0; }运行后检查日志文件,每一行应该是完整的,不会出现两行内容交错在一起的情况。glog 内部用了互斥锁保证线程安全,这个检查点验证的是锁机制在 Windows 上是否正常工作。如果发现日志行交错,说明编译时没有启用线程支持,检查 CMake 配置里有没有-DCMAKE_THREAD_PREFER_PTHREAD=ON或类似选项。
5. 进阶用法:自定义日志前缀与日志滚动策略
5.1 用 LogSink 接管日志输出
glog 默认把日志写到文件和 stderr,但你可以通过LogSink接口把日志同时送到其他地方,比如 Windows 的调试输出窗口(OutputDebugString)或者你自己的日志收集模块。实现一个简单的 LogSink:
#include <glog/logging.h> #include <windows.h> class DebugOutputSink : public google::LogSink { public: void send(google::LogSeverity severity, const char* full_filename, const char* base_filename, int line, const struct ::tm* tm_time, const char* message, size_t message_len) override { // 拼接日志行 std::string log_line = std::string(base_filename) + ":" + std::to_string(line) + " " + message; // 输出到 VS 的调试窗口 OutputDebugStringA(log_line.c_str()); OutputDebugStringA("\n"); } }; // 在 InitGoogleLogging 之后注册 DebugOutputSink sink; google::AddLogSink(&sink); // 程序退出前移除 google::RemoveLogSink(&sink);send函数的参数含义:severity是日志级别,full_filename是完整文件路径,base_filename是文件名,line是行号,tm_time是时间结构体,message是日志内容,message_len是内容长度。你可以在这个函数里做任何处理,比如格式化后发给远程日志服务。
5.2 控制日志文件数量和大小
glog 本身不提供日志滚动功能,但提供了SetLogFilenameExtension和按级别分文件的能力。如果你需要限制日志文件总大小,常见做法是写一个定时任务,定期扫描日志目录,删除超过 N 天的文件。或者用FLAGS_logbufsecs控制缓冲区刷新间隔,默认是 30 秒,设成 0 表示每条日志立即刷新,但会影响性能。
FLAGS_logbufsecs = 0; // 立即刷新,调试时用 // 或者 FLAGS_logbufsecs = 10; // 10 秒刷新一次,生产环境用还有一个参数FLAGS_max_log_size控制单个日志文件的最大大小(单位 MB),默认是 1800。超过之后 glog 会自动切换到新文件。这个参数在长时间运行的服务里比较有用,避免单个日志文件过大导致打开缓慢。
5.3 一个我踩过的坑:FLAGS_log_dir 和 SetLogDestination 的优先级
glog 有两个地方可以设置日志目录:命令行参数--log_dir和代码里的google::SetLogDestination。如果两个都设了,SetLogDestination的优先级更高。但如果你用了 gflags 解析命令行,--log_dir会在InitGoogleLogging之后生效,可能覆盖你代码里的设置。我一般的做法是:要么只用代码设置,要么只用命令行参数,不要混用。混用的时候行为取决于调用顺序,很容易出现「日志不知道写到哪里去了」的情况。
从那以后我每次集成 glog 都强制走一遍「编译 → 最小示例 → 多线程验证 → 日志目录确认」这个流程,确认没问题再往项目里搬。希望帮到你。
本文还有配套的精品资源,点击获取