news 2026/5/28 6:12:21

shm待整理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
shm待整理

MyLogger 完整详解

一、设计思想

为什么需要 MyLogger?

项目中有大量地方要记录日志:用户注册、登录、摄像头操作……如果每个地方都直接用std::cout,会有这些问题:

问题coutMyLogger
同时输出到控制台+文件❌ 只能选一个✅ 一次调用,两处输出
日志级别过滤❌ 全部输出✅ 可设级别,只输出重要的
线程安全❌ 多线程 cout 交错✅ spdlog 内部加锁
格式统一❌ 每人写法不同✅ 统一格式
性能❌ 同步阻塞✅ spdlog 异步可选

为什么用单例?

整个程序只需要一个日志对象:

  • 日志文件只有一个,不需要多个 logger 各写各的
  • 日志级别全局统一,改一处全局生效
  • 避免重复创建 spdlog 对象的开销

为什么不直接用 spdlog,要包一层?

你的代码 → MyLogger → spdlog

这叫适配器模式(Adapter),好处是:

  1. 换库成本低:如果将来不用 spdlog 换成 glog/log4cpp,只改 MyLogger.cc,调用方零改动
  2. 接口更简洁MyLogger::getInstance()->info(...)spdlog::get("SmartHome")->info(...)更直观
  3. 统一初始化:sink 创建、格式设置、级别配置都封装在构造函数里,不会散落各处

二、MyLogger.hpp 逐行详解

头文件守卫

#ifndef__MY_LOGGER_H__// 如果没定义过这个宏#define__MY_LOGGER_H__// 就定义它// ... 头文件内容 ...#endif// 结束

防止头文件被重复 include。如果A.cc同时 include 了B.hppC.hpp,而C.hpp也 include 了B.hpp,没有守卫就会重复定义编译报错。

spdlog 头文件

#include"spdlog/spdlog.h"// 核心头文件:logger 类、级别定义#include"spdlog/sinks/stdout_color_sinks.h"// 控制台彩色输出 sink#include"spdlog/sinks/basic_file_sink.h"// 基本文件输出 sink#include"spdlog/async.h"// 异步日志支持(预留,当前未用)

spdlog 的架构是Logger → Sink模型:

Logger(日志记录器) ├── Sink 1(控制台) ← stdout_color_sink_mt └── Sink 2(文件) ← basic_file_sink_mt
  • Logger:对外接口,接收日志消息,分发给所有 Sink
  • Sink:输出目标,每个 Sink 决定日志写到哪里、什么格式
  • _mt后缀:multi-thread(线程安全版本),内部有 mutex 保护

标准库头文件

#include<string>// string 类#include<memory>// shared_ptr 智能指针

类型别名

usingstd::string;usingstd::shared_ptr;

避免每次都写std::string,代码更简洁。只 using 常用的,不using namespace std,避免命名污染。

类定义开始

classMyLogger{

禁止拷贝和赋值

MyLogger(constMyLogger&)=delete;// 删除拷贝构造MyLogger&operator=(constMyLogger&)=delete;// 删除赋值运算符

单例模式的核心要求:全局只能有一个实例。如果允许拷贝:

autologger1=MyLogger::getInstance();autologger2=*logger1;// 拷贝出第二个对象 → 违反单例

= delete让编译器直接拒绝这种写法,编译期报错。

getInstance —— 获取单例

staticMyLogger*getInstance(conststring&logFile="");
  • static:静态成员函数,不需要对象就能调用(MyLogger::getInstance()
  • logFile = "":默认参数,只有首次调用时需要传入路径,后续调用忽略
  • 返回指针而不是引用,因为调用习惯是getInstance()->info(...)

六个日志级别接口

template<typename...Args>voidtrace(spdlog::format_string_t<Args...>fmt,Args&&...args){_logger->trace(fmt,std::forward<Args>(args)...);}

这 6 个函数(trace/debug/info/warn/error/critical)结构完全一样,以info为例拆解:

部分含义
template<typename... Args>变参模板,接受任意个数参数
spdlog::format_string_t<Args...> fmt格式串,编译期检查{}和参数数量是否匹配
Args&&... args万能引用,左值右值都能接
_logger->info(...)委托给 spdlog 的 logger 对象
std::forward<Args>(args)...完美转发,保持参数的值类别

为什么 6 个函数而不是 1 个?因为 spdlog 的trace/debug/info/...是不同的成员函数,不是参数。日志级别在编译时就确定了,这样 spdlog 可以在编译期优化掉低于设定级别的日志(零开销)。

setLevel —— 运行时设置级别

voidsetLevel(conststring&level);

声明在 .hpp,实现在 .cc。非模板函数,可以放 .cc。

flush —— 手动刷新

voidflush();

日志先写在内存缓冲区,flush()强制写入磁盘。程序崩溃前调用,防止丢日志。

私有构造函数

MyLogger(conststring&logFile);

private意味着外部不能MyLogger logger("xxx"),只能通过getInstance()获取。这是单例的关键:控制实例创建权

私有成员

shared_ptr<spdlog::logger>_logger;// spdlog 日志记录器staticMyLogger*_pInstance;// 单例指针
  • _logger:用shared_ptr管理,因为 spdlog 内部的注册机制也持有 logger 的引用,shared_ptr保证生命周期正确
  • _pInstance:静态指针,指向唯一的实例。配合getInstance中的局部静态变量使用

三、MyLogger.cc 逐行详解

静态成员初始化

MyLogger*MyLogger::_pInstance=nullptr;

类的静态成员变量必须在类外定义并初始化(C++ 规则)。_pInstance属于类,不属于任何对象,全局只有一份。

构造函数

MyLogger::MyLogger(conststring&logFile){

私有构造,只有getInstance()内部会调用。

try-catch 块
try{// ... 所有初始化代码 ...}catch(constspdlog::spdlog_ex&ex){std::cerr<<"MyLogger 初始化失败: "<<ex.what()<<std::endl;}

spdlog 初始化可能失败(比如日志文件路径不存在、无写权限),它抛出spdlog_ex异常。用 try-catch 捕获,防止程序直接崩溃。用cerr而不是_logger输出,因为 logger 还没创建成功。

Sink 1:控制台输出
autoconsoleSink=std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
  • stdout_color_sink_mt:输出到 stdout(标准输出),带 ANSI 颜色
  • _mt:multi-thread,内部有 mutex,多线程安全
  • make_shared:比new更高效(一次分配内存),且异常安全
consoleSink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");

设置日志格式,各占位符含义:

占位符输出示例
%Y4位年份2025
%m月份09
%d日期20
%H小时(24h)15
%M分钟30
%S00
%e毫秒123
%l日志级别info
%v消息内容用户注册成功
%^颜色开始标记(不可见)
%$颜色结束标记(不可见)

%^%l%$的效果:级别文字会被着色(info=绿色,warn=黄色,error=红色),在终端中一目了然。

实际输出效果:

[2025-09-20 15:30:00.123] [info] 用户 admin 注册成功
Sink 2:文件输出
autofileSink=std::make_shared<spdlog::sinks::basic_file_sink_mt>(logFile,true);
  • basic_file_sink_mt:写入单个文件
  • 第一个参数logFile:文件路径
  • 第二个参数truetruncate 模式——每次程序启动清空旧日志

为什么用 truncate?监控系统的日志量大,保留上次运行的日志意义不大。如果想保留历史,改为false(追加模式)。

fileSink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

文件格式和控制台几乎一样,但没有%^%$——因为文件不支持 ANSI 颜色码,写入颜色标记只会变成乱码。

组合 Sink 创建 Logger
std::vector<spdlog::sink_ptr>sinks{consoleSink,fileSink};_logger=std::make_shared<spdlog::logger>("SmartHome",sinks.begin(),sinks.end());
  • sink_ptr就是shared_ptr<sink>的别名
  • "SmartHome"是 logger 的名字,用于 spdlog 内部注册和检索
  • 传入两个 sink 的迭代器范围,logger 会把每条日志同时发给两个 sink

数据流:

info("port={}", 8000) → _logger 收到消息 → consoleSink 写入 stdout(带颜色) → fileSink 写入文件(无颜色)
设置日志级别
_logger->set_level(spdlog::level::info);

级别从低到高:trace < debug < info < warn < err < critical

设为info意味着:tracedebug级别的日志不会被输出。这是性能优化——低于设定级别的日志在should_log()检查时就直接跳过,连格式化都不做。

trace("xxx") → should_log() 返回 false → 直接跳过,零开销 info("xxx") → should_log() 返回 true → 格式化 + 输出
设置自动刷新级别
_logger->flush_on(spdlog::level::warn);

当日志级别 ≥ warn 时,立即刷新到磁盘,不等缓冲区满。

为什么?warn/error/critical通常意味着出了问题,程序可能马上崩溃。如果只写在缓冲区里没落盘,崩溃后就丢了。info级别的日志可以等缓冲区满再批量写入,不影响性能。

定时刷新
spdlog::flush_every(std::chrono::seconds(3));

每 3 秒自动把所有 logger 的缓冲区刷到磁盘。这是兜底策略——即使没有 warn 级别的日志,info 日志最多也只延迟 3 秒落盘。

注册全局默认 logger
spdlog::set_default_logger(_logger);

把我们的 logger 注册为 spdlog 的全局默认。之后可以直接用spdlog::info(...),不需要通过 logger 对象。我们项目里没用到这个特性,但注册了也没坏处,方便将来扩展。

getInstance 实现

MyLogger*MyLogger::getInstance(conststring&logFile){staticMyLoggerinstance(logFile);// 局部静态变量_pInstance=&instance;return_pInstance;}

这是整个单例的核心,逐行分析:

static MyLogger instance(logFile);

  • static局部变量的生命周期:首次执行到此处时构造,程序结束时析构
  • C++11 保证:如果多个线程同时首次到达,编译器自动加锁,只构造一次
  • 第二次调用时,instance已经存在,构造函数不会再次执行,logFile参数被忽略

_pInstance = &instance;

  • 把局部静态变量的地址存到_pInstance
  • 这样外部既可以通过getInstance()返回值访问,也可以通过_pInstance访问
  • 这行其实不是必须的,因为instance本身就是持久的。保留是为了兼容旧代码风格

为什么不用new

// 饿汉式(new 版本)staticMyLogger*getInstance(){staticMyLogger*p=newMyLogger("xxx");returnp;}

new出来的对象永远不会被析构(除非手动 delete),程序结束时可能丢最后几条日志。而局部静态变量在程序退出时会自动析构,spdlog 的析构函数会 flush 缓冲区,保证日志不丢。

setLevel 实现

voidMyLogger::setLevel(conststring&level){if(level=="trace")_logger->set_level(spdlog::level::trace);elseif(level=="debug")_logger->set_level(spdlog::level::debug);elseif(level=="info")_logger->set_level(spdlog::level::info);elseif(level=="warn")_logger->set_level(spdlog::level::warn);elseif(level=="error")_logger->set_level(spdlog::level::err);elseif(level=="critical")_logger->set_level(spdlog::level::critical);else_logger->set_level(spdlog::level::info);}

字符串 → 枚举的映射。注意 spdlog 中 error 级别的枚举值是spdlog::level::err(不是error),这是 spdlog 的命名习惯。

最后的else是兜底:传入无法识别的字符串时,默认设为 info 级别。

flush 实现

voidMyLogger::flush(){_logger->flush();}

直接委托给 spdlog。手动调用场景:程序即将退出、或者关键操作后想确保日志落盘。


四、整体架构图

调用方 │ │ MyLogger::getInstance()->info("用户 {} 登录", name) ▼ MyLogger(单例,适配器层) │ │ _logger->info(fmt, forward(args)...) ▼ spdlog::logger "SmartHome" │ ├──→ stdout_color_sink_mt ──→ 终端(彩色) │ 格式: [时间] [^级别$] 消息 │ └──→ basic_file_sink_mt ──→ server.log(纯文本) 格式: [时间] [级别] 消息

MyLogger 不做任何实质性的日志工作,它只是一个"中间人"——把调用转发给 spdlog,同时封装了初始化逻辑和单例管理。这就是适配器模式的价值:调用方不需要知道 spdlog 的 sink、pattern、level 这些细节。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 6:10:11

跟着 MDN 学CSS day_22:(从混乱到精美HTML表格样式化完全指南)

在网页设计的众多任务中&#xff0c;为HTML表格编写样式或许并不是最令人兴奋的工作。表格常常被认为是呆板的、数据密集型的元素&#xff0c;与创意和视觉表现力相去甚远。然而&#xff0c;几乎每一个涉及数据展示的项目都绕不开表格。 一个未经样式处理的表格&#xff0c;其默…

作者头像 李华
网站建设 2026/5/28 6:08:56

E2E-Fly:融合强化学习与可微仿真,实现无人机端到端敏捷飞行控制

1. 项目概述&#xff1a;为什么我们需要E2E-Fly&#xff1f;如果你玩过无人机&#xff0c;或者看过那些顶尖团队在无人机竞速赛中的表现&#xff0c;一定会被它们那种近乎极限的敏捷性和精准控制所震撼。它们能在复杂的障碍赛道中高速穿行&#xff0c;完成急转弯、翻滚、定点悬…

作者头像 李华
网站建设 2026/5/28 6:08:26

E语言软件

链接&#xff1a;https://pan.quark.cn/s/2f4deda166f5

作者头像 李华
网站建设 2026/5/28 6:07:25

极限编程:从工程实践到团队学习系统的深度解析

1. 从工程实践到学习系统&#xff1a;重新审视极限编程的核心价值在我职业生涯的早期&#xff0c;和许多软件工程师一样&#xff0c;我将极限编程&#xff08;Extreme Programming&#xff0c;简称XP&#xff09;视为一套严格甚至有些激进的工程实践集合。它的那些规则——结对…

作者头像 李华