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

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

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

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

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

整体思想设计

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

cpp 复制代码
#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

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

cpp 复制代码
 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接口能够被格式化组织出来,按照我们想要的方向进行输出:

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

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

C语言式的不定参

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

cpp 复制代码
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个参数。

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

cpp 复制代码
       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. 日志等级检查
cpp 复制代码
if(Loglevel::value::DEBUG < _limt_level)
{
    return;
}
  • 这部分代码首先检查当前的日志等级限制 _limt_level 是否允许输出 DEBUG 级别的日志。
  • 如果 _limt_level 的值比 DEBUG 级别更高(例如设置为 INFOERROR),则直接返回,不执行后续的日志记录操作。这样可以避免不必要的日志生成和输出。
2. 初始化可变参数列表
cpp 复制代码
va_list ap;
va_start(ap, fmt);
  • 使用 va_list 类型变量 ap 来存储可变参数列表。
  • va_start 初始化 ap,并指定 fmt 是最后一个固定的参数(即可变参数列表从 fmt 后面开始)。
3. 格式化日志消息
cpp 复制代码
char* res;
int ret = vasprintf(&res, fmt.c_str(), ap);
  • 使用 vasprintf 函数将格式化字符串 fmt 和可变参数列表 ap 转换为一个动态分配的字符串 res
  • vasprintf 是 C 标准库中的一个扩展函数,它会根据格式化字符串和参数生成结果字符串,并自动分配足够的内存。
  • 如果 vasprintf 返回 -1,表示格式化失败,程序会输出错误信息并直接返回。
4. 释放参数列表
cpp 复制代码
va_end(ap);
  • 在完成对可变参数列表的操作后,调用 va_end 释放 ap,以确保资源被正确清理。
5. 序列化和输出日志
cpp 复制代码
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 扩展),用于安全地格式化字符串并自动分配内存。下面我将全面介绍这个函数。

函数原型

c 复制代码
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 的可变参数版本

使用示例

c 复制代码
#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. 必须释放内存

    c 复制代码
    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

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

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

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

总结

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

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

给大家贴上完整代码:

cpp 复制代码
#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实现一下就可以了:

cpp 复制代码
    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);
                }
            }
    };

我们可以来测试一下:

cpp 复制代码
#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,更直观地定义类型别名
cpp 复制代码
using IntPtr = int*;          // 等价于 typedef int* IntPtr;
using StringVector = std::vector<std::string>;

模板别名typedef 无法实现):

cpp 复制代码
template<typename T>
using Vec = std::vector<T>;   // Vec<int> 等价于 std::vector<int>

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

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

3. 继承中的用法

  • 引入基类成员(解决名称隐藏问题):
cpp 复制代码
class Base {
public:
    void func() {}
};

class Derived : private Base {
public:
    using Base::func;  // 将基类的 func 引入到 public 区域
};
  • 继承构造函数(C++11 起):
cpp 复制代码
class Derived : public Base {
public:
    using Base::Base;  // 继承基类的所有构造函数
};

4. 类型转换(Type Traits)

decltype 配合定义复杂类型:

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

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

指定模板依赖的类型名:

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

对比 typedefusing

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

函数指针别名示例

cpp 复制代码
// typedef 写法
typedef void (*FuncPtr)(int, int);

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

最佳实践建议

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

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

相关推荐
shinelord明16 分钟前
【软件系统架构】事件驱动架构
数据结构·设计模式·架构·系统架构·软件工程
照海19Gin28 分钟前
数据结构中的宝藏秘籍之广义表
c语言·数据结构·算法
光算科技1 小时前
服务器在国外国内用户访问慢会影响谷歌排名吗?
运维·服务器·c++
小oo呆1 小时前
【自然语言处理与大模型】模型压缩技术之剪枝
算法·机器学习·剪枝
大炮筒1 小时前
CPPlist初识
数据结构·c++·list
bloxd yzh1 小时前
筛选法(埃氏筛法)C++
数据结构·算法
浅陌sss1 小时前
设计模式 --- 装饰器模式
设计模式·c#
拓端研究室TRL1 小时前
Python+AI提示词比特币数据预测:Logistic逻辑回归、SVC及XGB特征工程优化实践
开发语言·人工智能·python·算法·逻辑回归
学习同学2 小时前
设计模式 建造者模式
qt·设计模式·建造者模式
点云SLAM2 小时前
C++中的算术转换、其他隐式类型转换和显示转换详解
c++·static_cast·dynamic_cast·c++中的类型转换·算术类型转换·其他隐式类型转换·显示类型转换