目录
- `🍁日志`
- `🍂日志分模块实现讲解`
- `🍃日志等级的实现`
- `🥥日志时间`
- *时间的获取*
- `🌈文件名与行号的获取`
- `📚日志内容`
- `vsnprintf函数`
- `🌾日志打印的优化处理`
- `🍁将日志打印函数变为宏函数`
- `C语言宏的可变参数`
- `📕将日志内容保存到文件中`
- `🚀日志整体代码实现`
🍁日志
🍂日志分模块实现讲解
- 日志一般需要一下的内容:
日志等级
,日志打印的时间
,日志打印所在的文件名
,日志打印的所在代码行号
,日志内容
。
🍃日志等级的实现
将日志等级枚举出来,然后将用户传入的等级转为字符串即可。
代码实现:
//日志等级枚举
enum Level
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
//将日志等级转为字符串
string LevelToString(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOW";
}
}
🥥日志时间
时间的获取
time函数
- 功能:获取一个时间戳
- 返回值:time_t类型
- 参数:一般设置为nullptr
localtime_r函数
- 功能:可以将一个时间戳转化为年月日时分秒。
- 参数:
- 参数一:传入一个time_t的指针(也就是调用time函数的返回值的地址)。
- 参数二:一个struct tm类型的结构体,里面包含的字段如下:
代码实现:
//将时间转为字符串
string timeToString(struct tm *stm){
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%d-%d-%d %d:%d:%d",
stm->tm_year + 1900, stm->tm_mon + 1, stm->tm_mday,
stm->tm_hour, stm->tm_min, stm->tm_sec);
return timebuffer;
}
//时间
time_t curtime = time(nullptr);
struct tm stm;
localtime_r(&curtime, &stm);
string timestr = timeToString(&stm);
🌈文件名与行号的获取
使用预处理器宏
(__FILE__和__LINE__
)来获取当前文件的名称和行号。这些宏在编译时由预处理器替换为相应的文件名和行号字符串
。
代码示例:
#include <stdio.h>
void printLocation() {
printf("File: %s, Line: %d\n", __FILE__, __LINE__);
}
int main() {
printLocation(); // 输出当前文件名和行号
return 0;
}
📚日志内容
- 利用可变参数获取
提取可变参数的内容的示例:
void test(int num, ...)
{
va_list args;
va_start(args, num);
while (num--)
{
int data = va_arg(args, int);
cout << data << endl;
}
va_end(args);
}
test(3,10,20,30);
根据上面的例子对可变参数原理
进行简单介绍:
- test函数在调用的时候,会进行传参,传参的时候都是从右向左进行实例化的,在实例化的时候就会从右向左一次入栈,最右侧的固定参数(也就是离可变参数最近的一个参数),即int num会在离可变部分最近的位置,上面的函数是就是要将10,20,30依次提取出来。
- 函数内部就是:定义了一个指针,
args
,实际上va_list
类型是void*
,然后利用num参数和va_start宏将args指针初始化,实际上是args = &num-sizeof(num);
这样args就指向了可变参数的第一个参数,然后利用va_arg宏将第一个可变参数提取出来,实际是int data = *((int *)args);
然后会自动在数字上
加上sizeof(int),即args-=sizeof(int)
;就这样依次提取出来,最后利用va_end将args置nullptr
;
图解:
像上面示例那样提取,太麻烦了,下面介绍一个函数:
vsnprintf函数
- 函数介绍:
- 功能:可以将可变参数部分按照指定的格式,提取出来放到指定的字符串中(或则指定大小的字符串中)
利用vsnprintf函数是日志内容的获取:
代码实现:
//日志内容,多参数
void log(int level, string filename, int line, const char *format, ...)
{
//......
//日志内容,多参数的实现
va_list args;
va_start(args, format);
char ContentStr[1024];
vsnprintf(ContentStr, sizeof(ContentStr), format, args);
va_end(args);
//......
}
🌾日志打印的优化处理
- 因为每次打印日志的时候,都会自己传入行号与所在文件的文件名,比较麻烦;
🍁将日志打印函数变为宏函数
C语言宏的可变参数
如果你需要定义一个包含多条语句的多参数宏,你可以使用\来连接多行,或者更常见的是,使用do { … } while (0)结构来包围宏体。这种方式有助于避免在使用宏时可能出现的语义错误。
当你定义一个可变参数宏时,宏的参数列表中的最后一个参数必须是省略号...
,它表示宏可以接受任意数量的附加参数。在宏体内部,__VA_ARGS__
是一个特殊的标识符,它会被替换为宏调用时传递给宏的所有附加参数(如果有的话)。##__VA_ARGS__
作用是,让可变参数部分可以不传入参数。
代码实现:
// LOG(DEBUG, " Content is :%s %d %f ", "helloworld", 10, 3.14);
#define LOG(level, format, ...) \
do \
{ \
log(level, __FILE__, __LINE__, format, ##__VA_ARGS__); \
} while (0)
📕将日志内容保存到文件中
代码实现:
//将日志存放到文件中,利用的C++的文件读写操作
void IsSaveFile(string &message)
{
// 创建一个ofstream对象,与文件"example.txt"关联
// 如果文件不存在,会自动创建;如果文件已存在,会被覆盖
ofstream out(FILENAME, ios::app);
// 检查文件是否成功打开
if (!out)
{
return;
}
// 向文件写入内容
out << message << endl;
//关闭文件
out.close();
}
//将日志放到一个string里面
string message = "[ " + levelstr + " ] [ " + timestr + " ] [ "
+ filename + " ] [ " + to_string(line) + " ] [ "
+ ContentStr + " ]" + "\0";
🚀日志整体代码实现
Log.hpp
#pragma
#include <iostream>
#include <string>
#include <ctime>
#include <cstdarg>
#include <unistd.h>
#include <fstream>
using namespace std;
#define FILENAME "LOG.txt" //保存LOG的文件名
bool IsSave = false; //标记是否需要保存到文件
// LOG(DEBUG, " Content is :%s %d %f ", "helloworld", 10, 3.14);
#define LOG(level, format, ...) \
do \
{ \
log(level, __FILE__, __LINE__, format, ##__VA_ARGS__); \
} while (0)
//两各接口,修改IsSave的值,便于外部修改
#define EnIsSave() \
do \
{ \
IsSave = true; \
} while(0)
#define EnIsPrint() \
do \
{ \
IsSave = false; \
} while(0)
//日志等级枚举
enum Level
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
//将日志等级转为字符串
string LevelToString(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOW";
}
}
//将时间转为字符串
string timeToString(struct tm *stm)
{
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%d-%d-%d %d:%d:%d",
stm->tm_year + 1900, stm->tm_mon + 1, stm->tm_mday,
stm->tm_hour, stm->tm_min, stm->tm_sec);
return timebuffer;
}
//如果是多线程打印日志,打印到显示器,显示器是公共资源(临界资源),需要保护
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
//将日志存放到文件中,利用的C++的文件读写操作
void IsSaveFile(string &message)
{
ofstream out(FILENAME, ios::app);
if (!out)
{
return;
}
out << message << endl;
out.close();
}
// 日志的等级 时间 文件 行号 日志内容
void log(int level, string filename, int line, const char *format, ...)
{
//等级
string levelstr = LevelToString(level);
//时间
time_t curtime = time(nullptr);
struct tm stm;
localtime_r(&curtime, &stm);
string timestr = timeToString(&stm);
//日志内容,多参数
va_list args;
va_start(args, format);
char ContentStr[1024];
vsnprintf(ContentStr, sizeof(ContentStr), format, args);
va_end(args);
//将日志放到一个string里面
string message = "[ " + levelstr + " ] [ " + timestr + " ] [ " + filename + " ] [ " + to_string(line) + " ] [ " + ContentStr + " ]" + "\0";
//保护临界资源
//加锁
pthread_mutex_lock(&mutex);
if (!IsSave)
{
cout << "[ "
<< levelstr << " ] [ "
<< timestr << " ] [ "
<< filename << " ] [ "
<< line << " ] [ "
<< ContentStr << " ]"
<< endl;
}
else
{
IsSaveFile(message);
}
//释放锁
pthread_mutex_unlock(&mutex);
}