:项目开篇+工具函数)
1项目结果做一个最小可用但符合工业级标准的日志库。不需要像 spdlog 那样极致性能但必须具备生产环境需要的核心特性同步 / 异步双模式、线程安全、跨平台、无第三方依赖、类型安全接口写完能直接用到自己的 C 项目里。2整体框架设计采用了经典的分层架构每个模块只负责单一职责模块间低耦合高内聚方便后续扩展和调试┌─────────────────────────────────────────────────┐ │ 接口层 (log.hpp) │ │ 对外提供最简洁的调用接口完全隐藏内部实现细节 │ ├─────────────────────────────────────────────────┤ │ 管理层 (logger.hpp) │ │ 日志器建造者、全局单例管理器统一管理所有日志器│ ├─────────────────────────────────────────────────┤ │ 核心层 (logger.hpp/looper.hpp) │ │ 同步/异步日志器核心逻辑负责日志的调度与输出 │ ├─────────────────────────────────────────────────┤ │ 基础层 (level.hpp/message.hpp/format.hpp/sink.hpp) │ │ 日志等级定义、消息封装、格式化、落地器基础模块 │ ├─────────────────────────────────────────────────┤ │ 工具层 (util.hpp/buffer.hpp) │ │ 通用工具函数、高性能缓冲区为所有上层模块服务 │ └─────────────────────────────────────────────────┘3环境准备编译器VS2022以上MSVC v143GCC 11.3(Ubuntu 22.04)构建工具Cmake3.20跨平台构建也可以直接使用makefile不过Windows不支持语言标准最高C11只用C11及其以下的标准保证最大兼容4项目初步工具函数util.hpp工具模块是整个日志库的基石所有上层模块都会直接或间接调用它。我把所有跨平台的通用函数、基础工具都放在这里严格禁止其他模块出现任何平台相关代码这样以后加新平台的时候只需要改这一个文件。(因为最高C11并没有使用C17的文件操控函数所以很多是通过系统调用实现的)1整体思路设计我没有把所有工具函数都做成全局函数而是按功能分成了两个静态类Date类负责所有时间相关的工具函数File类负责所有文件 / 目录相关的工具函数这样设计的好处是职责更清晰调用的时候也更直观比如Date::GetTime()、File::Exists()一看就知道是做什么的。而且静态类不需要实例化直接通过类名调用和全局函数一样方便不会污染全局命名空间。2头文件基础结构跨平台宏定义首先是头文件的基础结构和跨平台宏定义这是整个工具模块的基础#pragma once // 头文件保护防止重复包含 // 标准库头文件 #include iostream #include ctime #include string /* 跨平台兼容宏定义 */ #ifdef _WIN32 // Windows平台特有头文件 #include direct.h #include windows.h // Windows下的stat和mkdir函数名和Linux不同用宏统一 #define stat _stat // Windows的stat函数叫_stat #define mkdir _mkdir // Windows的mkdir函数叫_mkdir #else // Linux/macOS平台特有头文件 #include sys/stat.h #include sys/types.h #endif namespace my_log { namespace util { // 时间工具类 class Date { public: // 获取当前系统时间戳秒级 static time_t GetTime(); }; // 文件/目录工具类 class File { public: // 判断文件/目录是否存在 static bool Exists(const std::string pathname); // 获取文件所在的目录路径 static std::string Path(const std::string pathname); // 递归创建多级目录 static void createdirectory(const std::string pathname); }; } // namespace util } // namespace my_log设计思考用#pragma once而不是传统的#ifndef/#define/#endif更简洁而且所有现代编译器都支持跨平台宏定义是核心Windows 和 Linux 的很多系统 API 函数名不同比如stat在 Windows 下叫_statmkdir在 Windows 下叫_mkdir。我用宏定义把它们统一成了 Linux 的名字这样后面的代码就不需要再写平台判断了所有函数都声明为static不需要创建类的实例直接通过类名调用用嵌套命名空间my_log::util避免和其他库的命名冲突3时间工具类Data需求分析日志最基础的元信息就是时间目前只需要秒级时间戳就够了后续格式化模块会把它转换成 2024-05-25 12:34:56 这样的可读格式。如果以后需要更高精度可以再扩展成毫秒级。/** * brief 获取当前系统时间戳秒级 * return time_t 自1970-01-01 00:00:00 UTC以来的秒数 */ time_t Date::GetTime() { // time(nullptr)返回当前系统的秒级时间戳 // 这个函数是C标准库函数所有平台都支持完全跨平台 return time(nullptr); }设计思考这里用了最基础的 C 标准库函数time()而不是 C11 的std::chrono原因很简单足够用而且最简单time(nullptr)是完全跨平台的所有编译器和操作系统都支持没有任何兼容性问题秒级精度对于大多数日志场景来说已经足够了如果以后需要毫秒级可以很容易地改成std::chrono的实现4文件工具类File这是工具模块的核心包含三个最常用的文件操作函数判断文件是否存在、获取文件所在目录、递归创建多级目录。1判断文件是否存在需求分析在创建文件或目录之前必须先判断它是否已经存在。如果已经存在就不需要再创建了避免不必要的错误。/** * brief 判断文件或目录是否存在 * param pathname 文件或目录的路径 * return bool 存在返回true不存在返回false */ bool File::Exists(const std::string pathname) { // 定义stat结构体用于存储文件/目录的状态信息 struct stat st; // stat函数用于获取文件/目录的状态信息 // 成功返回0失败返回-1 // 这里的stat已经通过宏定义统一了Windows和Linux的函数名 return stat(pathname.c_str(), st) 0; }设计思考用标准库的stat函数来判断文件是否存在这是跨平台判断文件存在的标准方法stat函数不仅能判断文件存在还能判断是文件还是目录、获取文件大小、修改时间等信息非常强大这里的stat已经通过前面的宏定义统一了 Windows 和 Linux 的函数名所以代码里不需要再写平台判断踩过的坑一开始我在 Windows 下直接用了stat函数结果编译报错。问了AI才知道Windows 的 C 标准库把stat函数重命名成了_stat而且还有宽字符版本_wstat。所以必须用宏定义把stat统一成_stat否则 Windows 下编译不通过。2获取文件所在目录需求分析当我们要创建一个文件时比如./logs/2024/05/25/app.log我们需要先获取它所在的目录./logs/2024/05/25然后创建这个目录才能打开文件。这个函数的作用就是输入一个文件的完整路径返回它所在的目录路径。/** * brief 获取文件所在的目录路径 * param pathname 文件的完整路径 * return std::string 文件所在的目录路径 * example 输入 ./logs/2024/05/25/app.log返回 ./logs/2024/05/25 * example 输入 app.log返回 .当前目录 */ std::string File::Path(const std::string pathname) { // 查找最后一个路径分隔符的位置 // 同时查找 / 和 \兼容Windows和Linux的路径格式 size_t pos pathname.find_last_of(/\\); // 边界情况处理如果没有找到任何分隔符说明文件在当前目录 if (pos std::string::npos) return .; // 返回当前目录 // 从开头截取到最后一个分隔符的位置就是文件所在的目录 return pathname.substr(0, pos); }设计思考边界情况处理如果输入的是纯文件名没有任何分隔符说明文件在当前目录返回 .同样用find_last_of(/\\)同时处理 Windows 和 Linux 的路径分隔符3递归创建多级目录难点函数需求分析这是文件工具类中最复杂的一个函数。当我们要创建./logs/2024/05/25/app.log时如果./logs/2024/05/25这个目录不存在文件打开就会失败。所以我们需要一个函数能够递归创建多级目录并且跨平台兼容。/** * brief 递归创建多级目录 * param pathname 要创建的目录路径 * note 如果目录已经存在直接返回不做任何操作 */ void File::createdirectory(const std::string pathname) { // 提前判断如果路径为空或者目录已经存在直接返回 if (pathname.empty() || Exists(pathname)) return; std::string current_path; // 保存当前正在创建的子目录路径 size_t idx 0; // 当前查找的起始位置 size_t len pathname.size(); // 路径总长度 // 循环遍历路径逐级创建目录 while (idx len) { // 从idx位置开始查找下一个路径分隔符 size_t pos pathname.find_first_of(/\\, idx); // 如果没有找到更多分隔符说明到了最后一级目录 if (pos std::string::npos) { pos len; } // 截取从开头到当前分隔符的子路径 current_path pathname.substr(0, pos); // 如果子路径不为空并且不存在就创建它 if (!current_path.empty() !Exists(current_path)) { #ifdef _WIN32 // Windows下的_mkdir函数只需要一个参数不需要权限 // 用(void)n; 显式忽略返回值避免编译器警告 int n mkdir(current_path.c_str()); (void)n; #else // Linux下的mkdir函数需要第二个参数指定目录权限 // 0777表示所有用户都有读写执行权限会被系统的umask修改 mkdir(current_path.c_str(), 0777); #endif } // 移动到下一个分隔符的下一个位置继续查找 idx pos 1; } }设计思考提前判断优化函数开头先判断路径是否为空或者目录已经存在如果是就直接返回避免不必要的操作逐级创建逻辑把完整路径按分隔符拆分成多级子目录从第一级开始逐个创建跨平台处理Windows 的_mkdir函数只需要一个参数不需要权限Linux 的mkdir函数需要第二个参数指定目录权限。这里用平台判断分别处理显式忽略返回值Windows 下的mkdir函数有返回值但我们已经提前判断了目录不存在所以肯定会创建成功。用(void)n;显式忽略返回值避免编译器产生 未使用变量 的警告踩过的坑只创建最后一级目录一开始我图省事直接调用mkdir创建完整路径结果发现系统 API 只能创建单级目录。比如要创建./logs/2024/05/25如果./logs不存在直接创建就会失败。后来改成逐级创建才解决问题Windows 权限问题Windows 的_mkdir函数不需要权限参数而 Linux 的mkdir必须指定权限。一开始我忘了加平台判断导致 Windows 下编译错误空路径处理如果输入的路径为空直接返回避免后续逻辑出错5完整代码#pragma once #include iostream #include ctime #include string /* 跨平台兼容宏定义 */ #ifdef _WIN32 // Windows平台特有头文件 #include direct.h #include windows.h // Windows下的stat和mkdir函数名和Linux不同用宏统一 #define stat _stat // Windows的stat函数叫_stat #define mkdir _mkdir // Windows的mkdir函数叫_mkdir #else // Linux/macOS平台特有头文件 #include sys/stat.h #include sys/types.h #endif namespace my_log { namespace util { /** * brief 时间工具类 * 提供所有时间相关的工具函数 */ class Date { public: /** * brief 获取当前系统时间戳秒级 * return time_t 自1970-01-01 00:00:00 UTC以来的秒数 */ static time_t GetTime() { return time(nullptr); } }; /** * brief 文件/目录工具类 * 提供所有文件和目录相关的工具函数 */ class File { public: /** * brief 判断文件或目录是否存在 * param pathname 文件或目录的路径 * return bool 存在返回true不存在返回false */ static bool Exists(const std::string pathname) { struct stat st; return stat(pathname.c_str(), st) 0; } /** * brief 获取文件所在的目录路径 * param pathname 文件的完整路径 * return std::string 文件所在的目录路径 * example 输入 ./logs/2024/05/25/app.log返回 ./logs/2024/05/25 * example 输入 app.log返回 .当前目录 */ static std::string Path(const std::string pathname) { size_t pos pathname.find_last_of(/\\); if (pos std::string::npos) return .; return pathname.substr(0, pos); } /** * brief 递归创建多级目录 * param pathname 要创建的目录路径 * note 如果目录已经存在直接返回不做任何操作 */ static void createdirectory(const std::string pathname) { if (pathname.empty() || Exists(pathname)) return; std::string current_path; size_t idx 0; size_t len pathname.size(); while (idx len) { size_t pos pathname.find_first_of(/\\, idx); if (pos std::string::npos) { pos len; } current_path pathname.substr(0, pos); if (!current_path.empty() !Exists(current_path)) { #ifdef _WIN32 int n mkdir(current_path.c_str()); (void)n; #else mkdir(current_path.c_str(), 0777); #endif } idx pos 1; } } }; } // namespace util } // namespace my_log5测试代码AI生成#include util.hpp #include iostream int main() { std::cout 工具模块全面测试 \n std::endl; // 1. 测试时间戳获取 std::cout 1. 时间戳获取测试 std::endl; time_t now my_log::util::Date::GetTime(); std::cout 当前秒级时间戳 now std::endl; std::cout ✅ 时间戳获取正常\n std::endl; // 2. 测试文件存在判断 std::cout 2. 文件存在判断测试 std::endl; std::cout 当前文件存在 (my_log::util::File::Exists(test_util.cpp) ? ✅ 是 : ❌ 否) std::endl; std::cout 不存在的文件 (my_log::util::File::Exists(not_exist.txt) ? ❌ 是 : ✅ 否) std::endl; std::cout ✅ 文件存在判断正常\n std::endl; // 3. 测试文件目录获取 std::cout 3. 文件目录获取测试 std::endl; std::string test_cases[] { ./logs/2024/05/25/app.log, C:\\Users\\user\\project\\app.log, app.log, ../src/app.log, /home/user/app.log }; for (const auto path : test_cases) { std::string dir my_log::util::File::Path(path); std::cout 输入\ path \ std::endl; std::cout 输出\ dir \ std::endl; } std::cout ✅ 文件目录获取正常\n std::endl; // 4. 测试目录创建 std::cout 4. 目录创建测试 std::endl; std::string test_dir ./test_logs/2024/05/25; std::cout 创建目录 test_dir std::endl; my_log::util::File::createdirectory(test_dir); std::cout 目录是否存在 (my_log::util::File::Exists(test_dir) ? ✅ 是 : ❌ 否) std::endl; // 测试重复创建已存在的目录 std::cout 重复创建已存在目录... std::endl; my_log::util::File::createdirectory(test_dir); std::cout ✅ 重复创建无错误\n std::endl; std::cout 所有测试通过 std::endl; return 0; }6总结完成的工作完成了项目整体分层架构设计明确了每个模块的职责实现了工具模块util.hpp包含两个静态类Date类提供秒级时间戳获取功能File类提供文件存在判断、文件目录获取、递归创建多级目录功能解决了所有跨平台兼容性问题一套代码在 Windows 和 Linux 下都能正常编译运行编写了全面的测试用例覆盖所有正常情况和边界情况在 Windows 和 Linux 两个平台完成了跨平台测试