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
函数的主要目的是:
- 判断是否需要输出日志:根据当前的日志等级限制
_limt_level
,决定是否需要输出DEBUG
级别的日志消息。 - 构造日志消息:通过接收一个格式化字符串(类似于
printf
的方式)和可变参数列表,生成最终的日志消息内容。 - 格式化日志消息:将传入的格式化字符串和参数组合成一个完整的日志消息字符串。
- 序列化和输出日志:将日志消息传递给另一个函数(如
serialize
),进行进一步处理或输出。
函数的具体实现逻辑
1. 日志等级检查
if(Loglevel::value::DEBUG < _limt_level)
{return;
}
- 这部分代码首先检查当前的日志等级限制
_limt_level
是否允许输出DEBUG
级别的日志。 - 如果
_limt_level
的值比DEBUG
级别更高(例如设置为INFO
或ERROR
),则直接返回,不执行后续的日志记录操作。这样可以避免不必要的日志生成和输出。
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
分配的内存,避免内存泄漏。
函数的关键点总结
-
日志等级过滤:
- 通过
_limt_level
控制日志输出的行为,避免输出不必要的日志消息,提高性能。
- 通过
-
格式化日志消息:
- 使用
vasprintf
动态生成格式化的日志消息,支持类似printf
的占位符语法(如%s
,%d
等)。
- 使用
-
资源管理:
- 使用
va_start
和va_end
管理可变参数列表。 - 使用
free
释放动态分配的内存,防止内存泄漏。
- 使用
-
日志输出:
- 将日志消息传递给
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
关键特性
-
自动内存分配
- 函数会根据需要自动分配足够大的内存
- 调用者负责后续释放这块内存
-
安全性
- 避免了缓冲区溢出风险
- 不需要预先猜测缓冲区大小
-
与
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;
}
内存管理注意事项
-
必须释放内存
char *str = NULL; vasprintf(&str, ...); // 使用str... free(str); // 必须调用free释放
-
错误处理
- 总是检查返回值是否为-1
- 失败时不要尝试使用或释放缓冲区
平台兼容性
-
支持平台
- GNU/Linux 系统
- 大多数 Unix-like 系统
-
Windows 替代方案
_vasprintf
(微软实现)- 或使用
_vscprintf
+malloc
+vsprintf
组合
与相关函数的对比
函数 | 自动分配内存 | 安全性 | 需要缓冲区大小参数 |
---|---|---|---|
vsprintf | 否 | 不安全 (可能溢出) | 否 |
vsnprintf | 否 | 安全 | 是 |
vasprintf | 是 | 安全 | 否 |
C++ 中的替代方案
现代 C++ 可以使用以下替代方案:
-
C++20
std::format
#include <format> std::string msg = std::format("Value: {}", 42);
-
fmt
库#include <fmt/core.h> std::string msg = fmt::format("Value: {}", 42);
-
字符串流
#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 是类型
};
对比 typedef
与 using
特性 | typedef | using |
---|---|---|
语法直观性 | 较晦涩 | 更清晰(类似赋值) |
支持模板别名 | ❌ 不支持 | ✅ 支持 |
可读性 | 类型名在末尾 | 类型名在左侧 |
函数指针别名 | 可支持但语法复杂 | 更简洁 |
函数指针别名示例:
// typedef 写法
typedef void (*FuncPtr)(int, int);// using 写法
using FuncPtr = void (*)(int, int);
最佳实践建议
- 优先使用
using
(现代 C++ 推荐) - 避免全局
using namespace
(易引发命名冲突) - 模板编程中必须用
using
(typedef
无法替代) - 合理使用继承中的
using
(解决重载或访问控制问题)
通过灵活运用 using
,可以显著提升代码的可读性和可维护性。