👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、日志的概念
- 二、储备知识之C式风格的可变参数
- 三、获取时间
- 四、实现打印日志函数
- 五、封装成类并实现将日志信息打印到文件里(完整代码)
一、日志的概念
在编程中,日志是指程序在运行时生成的记录信息和生成对应记录的时间。这些记录信息可以包括程序的状态、错误消息、警告、调试信息等。通过日志,程序员可以更轻松地跟踪程序的执行过程、诊断问题并监视系统的运行情况。
常见的日志等级包括:
-
info
:常规信息 -
warning
:指示可能会引起问题的情况,但程序仍然可以继续执行。 -
error
:指示程序发生了错误,可能需要立即处理,但程序仍然能够继续执行。 -
fatal
:指示程序出现了致命问题,可能导致程序无法继续执行。 -
debug
:提供关于程序详细执行过程的信息,通常用于调试目的。
二、储备知识之C式风格的可变参数
在C语言中,可变参数函数是一种允许函数接受不定数量参数的机制。比方说printf
就可以接受不定数量参数。
#include <stdio.h>
int printf(const char *format, ...);
实现这种功能需要使用stdarg.h
头文件提供的一些宏。这些宏包括:
va_list
:可以理解为一个用于存储所以可变参数的容器。va_start
:是一个宏函数,它的作用是初始化一个va_list
对象,使其指向可变参数列表的第一个参数。以下是它的原型
void va_start(va_list ap, last_arg);
其中
- 第一个参数是类型为
va_list
的对象 - 第二个参数是一个固定参数,即可变参数列表之前的那个参数。因此,可变参数之前必须要有至少一个具体的参数。
va_arg
:是一个宏函数,访问可变参数列表中的下一个参数,它的具体实现会有指针的自增操作。
type va_arg(va_list ap, type);
- 第一个参数是类型为
va_list
的对象。 - 第二个参数是你希望从可变参数列表中获取的参数类型。
va_end
:清理va_list
对象。
比方说定义一个可变参数函数,计算所有参数的和,用于演示如何编写和使用可变参数函数:
【程序结果】
三、获取时间
日志中包含时间是非常重要的,因为它可以帮助程序员准确地定位和跟踪问题。获取时间的方法有很多种,如time
函数、clock
函数、gettimeofday
函数、strftime
函数等。
这里我以localtime
函数为例,以上函数的具体用法大家可以自行搜索。
#include <time.h>
struct tm *localtime(const time_t *timep);
localtime
函数可以将time_t
类型的时间戳转换为struct tm
类型,而struct tm
类型有如下成员变量
需要注意的是:在C语言的struct tm
结构体中,年份(tm_year
)的起始值为1900
,月份(tm_mon
)的起始值为0
。这意味着,如果你想要获取实际的年份和月份,需要对tm_year
和tm_mon
进行一些调整。
-
tm_year
表示从1900
年开始经过的年数。因此,要获取实际的年份,需要将其加上1900
,即tm_year + 1900
。 -
tm_mon
表示月份,范围从0
到11
,其中0
表示一月,1
表示二月,以此类推。因此,要获取实际的月份,需要将其加上1
,即tm_mon + 1
。
以下是代码示例:
【程序结果】
四、实现打印日志函数
有了以上的知识,我们就可以开始实现打印日志函数了。
首先规定日志的格式:[时间] [等级] [用户自定义内容]
代码如下(含详细注释)
【函数解析】
snprintf
函数:用于将格式化的数据写入字符数组中。它的声明通常如下:int snprintf(char *str, size_t size, const char *format, ...);
str
: 指向存储输出的字符数组。size
: 字符数组的大小。format
: 传递给格式化字符串的数据格式,与printf
类似。...
: 可变数量的参数,这些参数根据格式字符串进行格式化。
vsnprintf
函数:与snprintf
类似,但它使用va_list
类型的参数列表。这对于在函数内部处理可变参数特别有用。其声明通常如下:int vsnprintf(char *str, size_t size, const char *format, va_list ap);
str
: 指向存储输出的字符数组。size
: 字符数组的大小format
: 传递给格式化字符串的数据格式,与printf
类似。ap
:va_list
类型的参数列表,由va_start
、va_arg
和va_end
宏管理。
【复制即可用】
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
// 将日志等级用整数表示
#define Info 0 // 常规
#define Debug 1 // 调试
#define Warning 2 // 警告
#define Error 3 // 错误
#define Fatal 4 // 致命
// 因为我们这里的日志等级是用一个整数表示的
// 而最后日志打印时需要有具体是什么日志等级
// 因此我们可以封装一个函数将日志等级转化为字符串
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// level - 日志等级
// format - 格式化字符串的数据格式。类似于printf前半部分
// ... - 表示可变参数
void logmessage(int level, const char *format, ...)
{
// ====== 默认部分:日志等级 + 时间 =========
time_t _timestamp = time(NULL); // time函数会返回时间戳
// 再将time_t类型转化为struct tm类型
struct tm *_tm = localtime(&_timestamp);
char defaultPart[1024]; // 默认部分
// 打印的日志格式:[日志等级][时间]
snprintf(defaultPart, sizeof(defaultPart), "[%s][%d-%d-%d:%d:%d:%d]",
levelToString(level).c_str(), _tm->tm_year + 1900, _tm->tm_mon + 1, _tm->tm_mday,
_tm->tm_hour, _tm->tm_min, _tm->tm_sec);
// ====== 自定义部分:format内容 + 可变参数... =========
char self[1024];
va_list s;
va_start(s, format);
vsnprintf(self, sizeof(self), format, s);
va_end(s);
// ===== 将默认部分和自定义部分整合 =====
char logtxt[2048];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", defaultPart, self);
// ==== 信息全部在logtxt中,你可以打印出来,或者写到一个文件里 ======
printf("%s", logtxt);
}
五、封装成类并实现将日志信息打印到文件里(完整代码)
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
// 将日志等级用整数表示
#define Info 0 // 常规
#define Debug 1 // 调试
#define Warning 2 // 警告
#define Error 3 // 错误
#define Fatal 4 // 致命
#define Screen 1
#define OneFile 2
#define ClassFile 3
class log
{
public:
// 写一个默认构造函数, 默认打印是向屏幕打印
log()
{
printMethod = Screen;
logdir = "./logdir/"; // 你需要保证当前路径下有目录名为logdir
}
// 让用户选择打印方式
void Enable(int method)
{
printMethod = method;
}
// 因为我们这里的日志等级是用一个整数表示的
// 而最后日志打印时需要有具体是什么日志等级
// 因此我们可以封装一个函数将日志等级转化为字符串
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// level - 日志等级
// format - 格式化字符串的数据格式。类似于printf前半部分
// ... - 表示可变参数
void logmessage(int level, const char *format, ...)
{
// ====== 默认部分:日志等级 + 时间 =========
time_t _timestamp = time(NULL); // time函数会返回时间戳
// 再将time_t类型转化为struct tm类型
struct tm *_tm = localtime(&_timestamp);
char defaultPart[1024]; // 默认部分
// 打印的日志格式:[日志等级][时间]
snprintf(defaultPart, sizeof(defaultPart), "[%s][%d-%d-%d:%d:%d:%d]",
levelToString(level).c_str(), _tm->tm_year + 1900, _tm->tm_mon + 1, _tm->tm_mday,
_tm->tm_hour, _tm->tm_min, _tm->tm_sec);
// ====== 自定义部分:format内容 + 可变参数... =========
char self[1024];
va_list s;
va_start(s, format);
vsnprintf(self, sizeof(self), format, s);
va_end(s);
// ===== 将默认部分和自定义部分整合 =====
char logtxt[2048];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", defaultPart, self);
// ==== 信息全部在logtxt中,你可以打印出来,或者写到一个文件里 ======
// printf("%s", logtxt); // 直接打印
printLog(level, logtxt);
}
// 封装打印日志文件的方法:1. 向屏幕打印 2. 向文件打印 3. 分类打印
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case OneFile:
printOneFile("log.txt", logtxt);
break;
case ClassFile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
// 向一个文件写
void printOneFile(const std::string filename, const std::string &logtxt)
{
std::string _filename = logdir + filename; // ./logdir/log.txt
int fd = open(_filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
{
return;
}
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
// 文件分类写。比如Info信息放在一个文件中,Errno放在一个文件中...
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = "log.txt";
filename += '.';
filename += levelToString(level);
printOneFile(filename, logtxt);
}
private:
int printMethod;
std::string logdir; // 日志文件存放目录
};