欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 八卦 > C++项目 —— 基于多设计模式下的同步异步日志系统(3)(日志器类)

C++项目 —— 基于多设计模式下的同步异步日志系统(3)(日志器类)

2025/4/22 23:17:26 来源:https://blog.csdn.net/qq_67693066/article/details/147309275  浏览:    关键词:C++项目 —— 基于多设计模式下的同步异步日志系统(3)(日志器类)

C++项目 —— 基于多设计模式下的同步&异步日志系统(3)(日志器类)

  • 整体思想设计
  • 日志消息的构造
    • C语言式的不定参
      • 函数的作用
      • 函数的具体实现逻辑
        • 1. 日志等级检查
        • 2. 初始化可变参数列表
        • 3. 格式化日志消息
        • 4. 释放参数列表
        • 5. 序列化和输出日志
      • 函数的关键点总结
  • vasprintf
    • `vasprintf` 函数详解
    • 函数原型
    • 参数说明
    • 返回值
    • 关键特性
    • 使用示例
    • 内存管理注意事项
    • 平台兼容性
    • 与相关函数的对比
    • C++ 中的替代方案
    • 总结
  • 同步日志器
  • 扩充
    • using的用法
      • 1. 类型别名(Type Aliases)
      • 2. 命名空间引入(Namespace Directives)
      • 3. 继承中的用法
      • 4. 类型转换(Type Traits)
      • 5. 模板编程中的依赖类型
      • 对比 `typedef` 与 `using`
      • 最佳实践建议

我们之前的两次博客,已经把日志器模块的一些基本组成要素已经搭建完成了,包括基本工具类的创建,格式化消息类,日志落地方向类的编写也已经完成了。如果还有小伙伴不熟悉这些,可以先看看我的前两次博客:

https://blog.csdn.net/qq_67693066/article/details/147190387?spm=1011.2415.3001.5331

https://blog.csdn.net/qq_67693066/article/details/147162921?spm=1011.2415.3001.5331

我们今天的任务主要是将前面的我们所编写的类组合起来,组合成一个实实在在的日志器,同时日志器也分两个方向,分为同步日志器和异步日志器。

整体思想设计

跟我们之前设计日志落地方向的时候一样,我们也是设计一个基类日志器,然后继承分化成两类不同的日志器,一个是同步日志器,另一个是异步日志器

#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include "utils.hpp"
#include "message.hpp"
#include "utils.hpp"
#include "level.hpp"
#include "sink.hpp"
#include <atomic>
#include <mutex>
#include <iostream>
#include <memory>
#include <ctime>
#include <vector>
#include <cassert>
#include <sstream>namespace logs
{class BaseLogger{public:BaseLogger(const std::string& logger_name,Loglevel::value level,Formetter::ptr &formetter,std::vector<BaseSink::ptr> &sinks)    :_logger_name(logger_name),_level(level),_formetter(formetter),_sinks(sinks.begin(), sinks.end()){}protected:std::mutex _mutex; //锁std::string logger_name; //日志器名称std::atomic<logs::Loglevel> _level; //日志等级Formetter::ptr _formetter; //格式化消息指针std::vector<BaseSink::ptr> _sink; //落地方向   };class SyncLogger : protected BaseLogger{};class AsyncLogger : protected BaseLogger{};
}#endif

这样我们就把大概的架子搭好了,我们这时候先把转化日志消息的这个功能做好:

 void serialize(Loglevel::value level,const std::string& file_name,size_t line,const std::string& logger_name,char* str){//1.构造msg对象logs::logMsg msg(level,file_name,line,logger_name,str);//2.利用Formetter进行消息格式化std::stringstream ss;_formetter->format(ss,str);//3.落地方向的输出log(ss.str().c_str(), ss.str().size());}

日志消息的构造

我们日志器最重要的一个部分就是对不同日志等级消息进行输出,所以我们要对不同的日志等级设计接口,使他们能够输出对应自身的日志消息:

我们拿debug来举例,我们要设计对应的接口,使得对应消息进入debug接口能够被格式化组织出来,按照我们想要的方向进行输出:

  /*完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串---然后进行输出*/void debug(){}

这里我们要考虑一个问题,就是我们传入的消息可能不是固定的,参数可能不是固定的,所以我们的接口参数个数就不能写死。这里我们我们要介绍一下C语言风格的不定参:

C语言式的不定参

在C语言中,函数可以接受不定数量的参数,这称为可变参数函数(variadic functions)。标准库中的printf()scanf()就是典型的例子。我们可以举一个简单的例子:

double average(int count, ...)
{va_list ap;  声明参数列表变量int j = 0;double sum = 0;va_start(ap,count); //count是最后一个参数for (j = 0; j < count; j++){sum += va_arg(ap, int); //依次获得int类型的参数}va_end(ap);return sum / count;
}

这个average函数可以接受若干参数,像这里,我就声明接受5个参数。

我们可以把这样的思想用到我们日志器不同等级日志打印上:

       void debug(const std::string& file,size_t line,const std::string fmt,...){//如果限制输出的日志等级比debug高,则直接返回if(Loglevel::value::DEBUG < _limt_level){return;}//声明参数列表变量va_list ap;va_start(ap,fmt); // 初始化fmt是最后一个固定的参数char* res; //声明缓冲区int ret = vasprintf(&res,fmt.c_str(),ap);if(ret == -1){std::cout << "vasprintf failed!\n";return;}va_end(ap); //释放参数列表变量serialize(Loglevel::value::DEBUG, file, line, res);free(res);}

这个函数是一个日志输出工具的一部分,用于生成和输出格式化的调试日志消息。以下是对其作用的详细解释:


函数的作用

这个 debug 函数的主要目的是:

  1. 判断是否需要输出日志:根据当前的日志等级限制 _limt_level,决定是否需要输出 DEBUG 级别的日志消息。
  2. 构造日志消息:通过接收一个格式化字符串(类似于 printf 的方式)和可变参数列表,生成最终的日志消息内容。
  3. 格式化日志消息:将传入的格式化字符串和参数组合成一个完整的日志消息字符串。
  4. 序列化和输出日志:将日志消息传递给另一个函数(如 serialize),进行进一步处理或输出。

函数的具体实现逻辑

1. 日志等级检查
if(Loglevel::value::DEBUG < _limt_level)
{return;
}
  • 这部分代码首先检查当前的日志等级限制 _limt_level 是否允许输出 DEBUG 级别的日志。
  • 如果 _limt_level 的值比 DEBUG 级别更高(例如设置为 INFOERROR),则直接返回,不执行后续的日志记录操作。这样可以避免不必要的日志生成和输出。
2. 初始化可变参数列表
va_list ap;
va_start(ap, fmt);
  • 使用 va_list 类型变量 ap 来存储可变参数列表。
  • va_start 初始化 ap,并指定 fmt 是最后一个固定的参数(即可变参数列表从 fmt 后面开始)。
3. 格式化日志消息
char* res;
int ret = vasprintf(&res, fmt.c_str(), ap);
  • 使用 vasprintf 函数将格式化字符串 fmt 和可变参数列表 ap 转换为一个动态分配的字符串 res
  • vasprintf 是 C 标准库中的一个扩展函数,它会根据格式化字符串和参数生成结果字符串,并自动分配足够的内存。
  • 如果 vasprintf 返回 -1,表示格式化失败,程序会输出错误信息并直接返回。
4. 释放参数列表
va_end(ap);
  • 在完成对可变参数列表的操作后,调用 va_end 释放 ap,以确保资源被正确清理。
5. 序列化和输出日志
serialize(Loglevel::value::DEBUG, file, line, res);
free(res);
  • 调用 serialize 函数,将日志等级(DEBUG)、文件名(file)、行号(line)以及格式化后的日志消息(res)传递给它。
  • serialize 函数可能会将这些信息进一步处理(例如添加时间戳、线程 ID 等),然后输出到文件、控制台或其他目标。
  • 最后,使用 free(res) 释放由 vasprintf 分配的内存,避免内存泄漏。

函数的关键点总结

  1. 日志等级过滤

    • 通过 _limt_level 控制日志输出的行为,避免输出不必要的日志消息,提高性能。
  2. 格式化日志消息

    • 使用 vasprintf 动态生成格式化的日志消息,支持类似 printf 的占位符语法(如 %s, %d 等)。
  3. 资源管理

    • 使用 va_startva_end 管理可变参数列表。
    • 使用 free 释放动态分配的内存,防止内存泄漏。
  4. 日志输出

    • 将日志消息传递给 serialize 函数进行进一步处理和输出。

vasprintf

vasprintf 函数详解

vasprintf 是一个非常有用的 C 库函数(GNU 扩展),用于安全地格式化字符串并自动分配内存。下面我将全面介绍这个函数。

函数原型

int vasprintf(char **strp, const char *format, va_list ap);

参数说明

  • strp:指向字符指针的指针,函数会将分配的缓冲区地址存储在这里
  • format:格式化字符串(与 printf 风格相同)
  • ap:可变参数列表(通过 va_start 初始化)

返回值

  • 成功时:返回写入的字符数(不包括结尾的 null 字符)
  • 失败时:返回 -1,并且不修改 *strp

关键特性

  1. 自动内存分配

    • 函数会根据需要自动分配足够大的内存
    • 调用者负责后续释放这块内存
  2. 安全性

    • 避免了缓冲区溢出风险
    • 不需要预先猜测缓冲区大小
  3. vsprintf 的关系

    • 类似于 vsprintf,但自动处理内存分配
    • 类似于 asprintf 的可变参数版本

使用示例

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>void log_message(const char *format, ...) {va_list args;va_start(args, format);char *buffer = NULL;int len = vasprintf(&buffer, format, args);va_end(args);if (len != -1) {printf("Formatted message: %s\n", buffer);free(buffer);  // 必须释放分配的内存} else {printf("Formatting failed\n");}
}int main() {log_message("Current value: %d, name: %s", 42, "example");return 0;
}

内存管理注意事项

  1. 必须释放内存

    char *str = NULL;
    vasprintf(&str, ...);
    // 使用str...
    free(str);  // 必须调用free释放
    
  2. 错误处理

    • 总是检查返回值是否为-1
    • 失败时不要尝试使用或释放缓冲区

平台兼容性

  1. 支持平台

    • GNU/Linux 系统
    • 大多数 Unix-like 系统
  2. Windows 替代方案

    • _vasprintf(微软实现)
    • 或使用 _vscprintf + malloc + vsprintf 组合

与相关函数的对比

函数自动分配内存安全性需要缓冲区大小参数
vsprintf不安全 (可能溢出)
vsnprintf安全
vasprintf安全

C++ 中的替代方案

现代 C++ 可以使用以下替代方案:

  1. C++20 std::format

    #include <format>
    std::string msg = std::format("Value: {}", 42);
    
  2. fmt

    #include <fmt/core.h>
    std::string msg = fmt::format("Value: {}", 42);
    
  3. 字符串流

    #include <sstream>
    std::ostringstream oss;
    oss << "Value: " << 42;
    std::string msg = oss.str();
    

总结

vasprintf 是一个方便且安全的字符串格式化函数,特别适合需要动态构建字符串的场景。它的主要优点是自动内存管理和避免缓冲区溢出,但需要注意正确释放分配的内存和考虑跨平台兼容性。

以上的阐述足够让大家具体了解这个debug函数的具体用法和语法细节,其他等级的日志输出函数我们如法炮制就行了:

给大家贴上完整代码:

#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include "utils.hpp"
#include "message.hpp"
#include "utils.hpp"
#include "level.hpp"
#include "sink.hpp"
#include <atomic>
#include <mutex>
#include <iostream>
#include <memory>
#include <ctime>
#include <vector>
#include <cassert>
#include <sstream>
#include<stdarg.h>namespace logs
{class BaseLogger{public:using ptr = std::shared_ptr<BaseLogger>;BaseLogger(const std::string& logger_name,Loglevel::value limt_level,Formetter::ptr &formetter,std::vector<BaseSink::ptr> &sinks)    :_logger_name(logger_name),_limt_level(limt_level),_formetter(formetter),_sinks(sinks.begin(), sinks.end()){}const std::string& get_logger_name(){return _logger_name;}/*完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串---然后进行输出*/void debug(const std::string& file,size_t line,const std::string fmt,...){//如果限制输出的日志等级比debug高,则直接返回if(Loglevel::value::DEBUG < _limt_level){return;}//声明参数列表变量va_list ap;va_start(ap,fmt); // 初始化fmt是最后一个固定的参数char* res; //声明缓冲区int ret = vasprintf(&res,fmt.c_str(),ap);if(ret == -1){std::cout << "vasprintf failed!\n";return;}va_end(ap); //释放参数列表变量serialize(Loglevel::value::DEBUG, file, line, res);free(res);}void info(const std::string& file,size_t line,const std::string fmt,...){//如果限制输出的日志等级比debug高,则直接返回if(Loglevel::value::INFO < _limt_level){return;}//声明参数列表变量va_list ap;va_start(ap,fmt); // 初始化fmt是最后一个固定的参数char* res; //声明缓冲区int ret = vasprintf(&res,fmt.c_str(),ap);if(ret == -1){std::cout << "vasprintf failed!\n";return;}va_end(ap); //释放参数列表变量serialize(Loglevel::value::INFO, file, line, res);free(res);}void warn(const std::string& file,size_t line,const std::string fmt,...){//如果限制输出的日志等级比debug高,则直接返回if(Loglevel::value::WARN < _limt_level){return;}//声明参数列表变量va_list ap;va_start(ap,fmt); // 初始化fmt是最后一个固定的参数char* res; //声明缓冲区int ret = vasprintf(&res,fmt.c_str(),ap);if(ret == -1){std::cout << "vasprintf failed!\n";return;}va_end(ap); //释放参数列表变量serialize(Loglevel::value::WARN, file, line, res);free(res);}void error(const std::string &file, size_t line, const std::string &fmt, ...){// 1.通过传入的参数构造一个日志对象,进行日志的格式化,最终落地if (Loglevel::value::ERROR < _limt_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!\n";return;}va_end(ap);serialize(Loglevel::value::ERROR, file, line, res);free(res);}void fatal(const std::string &file, size_t line, const std::string &fmt, ...){// 1.通过传入的参数构造一个日志对象,进行日志的格式化,最终落地if (Loglevel::value::FATAL < _limt_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!\n";return;}va_end(ap);serialize(Loglevel::value::FATAL, file, line, res);free(res);}protected:void serialize(Loglevel::value level,const std::string& file_name,size_t line,char* str){//1.构造msg对象logs::logMsg msg(level,file_name,line,_logger_name,str);//2.利用Formetter进行消息格式化std::stringstream ss;_formetter->format(ss,msg);//3.落地方向的输出log(ss.str().c_str(), ss.str().size());}/*抽象接口完成实际的落地输出---不同的日志器会有不同的落地方式*/virtual void log(const char *data, size_t len) = 0;protected:std::mutex _mutex; //锁std::string _logger_name; //日志器名称std::atomic<Loglevel::value> _limt_level; //日志等级Formetter::ptr _formetter; //格式化消息指针std::vector<BaseSink::ptr> _sinks; //落地方向   };class SyncLogger : public BaseLogger{};class AsyncLogger : public BaseLogger{};
}#endif

同步日志器

同步日志器比较简单,我们继承基础的日志器然后把接口log实现一下就可以了:

    class SyncLogger : public BaseLogger{public:SyncLogger(const std::string& logger_name,Loglevel::value limt_level,Formetter::ptr &formetter,std::vector<BaseSink::ptr> &sinks):BaseLogger(logger_name, limt_level, formetter, sinks) {}protected:void log(const char *data, size_t len){//1.上锁std::unique_lock<std::mutex> _lock(std::mutex);if (_sinks.empty())return;for (auto &sink : _sinks){sink->log(data, len);}}};

我们可以来测试一下:

#include "utils.hpp"
#include "level.hpp"
#include "message.hpp"
#include "fometter.hpp"
#include "sink.hpp"
#include "logger.hpp"int main()
{// 1. 创建 Formatter 对象(假设构造函数接受格式字符串)logs::Formetter formatter("abc[%d{%H:%M:%S}][%c]%T%m%n");// 2. 创建智能指针logs::Formetter::ptr fmt_ptr = std::make_shared<logs::Formetter>(formatter);auto st1 = logs::SinkFactory::create<logs::StdoutSink>();std::vector<logs::BaseSink::ptr> sinks = {st1};std::string logger_name = "synclogger";logs::BaseLogger::ptr logger(new logs::SyncLogger(logger_name, logs::Loglevel::value::DEBUG, fmt_ptr, sinks));logger->debug("main.cc", 53, "%s","格式化功能测试....");
}

在这里插入图片描述

扩充

using的用法

在 C++ 中,using 是一个多功能关键字,主要有以下几种用法:

1. 类型别名(Type Aliases)

  • 替代 typedef,更直观地定义类型别名
using IntPtr = int*;          // 等价于 typedef int* IntPtr;
using StringVector = std::vector<std::string>;

模板别名typedef 无法实现):

template<typename T>
using Vec = std::vector<T>;   // Vec<int> 等价于 std::vector<int>

2. 命名空间引入(Namespace Directives)

  • 引入整个命名空间(谨慎使用):
using namespace std;  // 引入 std 命名空间
  • 引入特定成员:
using std::cout;      // 只引入 cout

3. 继承中的用法

  • 引入基类成员(解决名称隐藏问题):
class Base {
public:void func() {}
};class Derived : private Base {
public:using Base::func;  // 将基类的 func 引入到 public 区域
};
  • 继承构造函数(C++11 起):
class Derived : public Base {
public:using Base::Base;  // 继承基类的所有构造函数
};

4. 类型转换(Type Traits)

decltype 配合定义复杂类型:

using ResultType = decltype(a + b);  // 根据表达式推断类型

5. 模板编程中的依赖类型

指定模板依赖的类型名:

template<typename T>
class Widget {using ValueType = typename T::value_type;  // 明确 value_type 是类型
};

对比 typedefusing

特性typedefusing
语法直观性较晦涩更清晰(类似赋值)
支持模板别名❌ 不支持✅ 支持
可读性类型名在末尾类型名在左侧
函数指针别名可支持但语法复杂更简洁

函数指针别名示例

// typedef 写法
typedef void (*FuncPtr)(int, int);// using 写法
using FuncPtr = void (*)(int, int);

最佳实践建议

  1. 优先使用 using(现代 C++ 推荐)
  2. 避免全局 using namespace(易引发命名冲突)
  3. 模板编程中必须用 usingtypedef 无法替代)
  4. 合理使用继承中的 using(解决重载或访问控制问题)

通过灵活运用 using,可以显著提升代码的可读性和可维护性。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词