C语言 服务器编程-日志系统

news2024/12/26 22:48:26

日志系统的实现

  • 引言
  • 最简单的日志类 demo
  • 按天日志分类和超行日志分类
  • 日志信息分级
  • 同步和异步两种写入方式

引言

日志系统是通过文件来记录项目的 调试信息,运行状态,访问记录,产生的警告和错误的一个系统,是项目中非常重要的一部分. 程序员可以通过日志文件观测项目的运行信息,方便及时对项目进行调整.

最简单的日志类 demo

日志类一般使用单例模式实现:

Log.h:

class Log
{
private:
	
	Log() {};
	~Log();
public:
	bool init(const char* file_name);

	void write_log(const char* str);

	static Log* getinstance();

private:
	FILE* file;
};

Log.cpp:

Log::~Log()
{
	if (file != NULL)
    {
	   fflush(file);
	   fclose(file);
    }
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name)
{
	file = fopen(file_name,"a");
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(const char* str)
{
	if (file == NULL)
		return;

	fputs(str, file);
}

main.cpp:

#include"Log.h"

int main()
{
	Log::getinstance()->init("log.txt");
	Log::getinstance()->write_log("Hello World");
}

这个日志类实现了最简单的写日志的功能,但是实际应用时,需要在日志系统上开发出许多额外的功能来满足工作需要,有些时候还要进行日志的分类操作,因为你不能将所有的日志信息都塞到一个日志文件中,这样会大大降低可读性,接下来讲一下在这个最简单的日志类的基础上,怎么添加一些新功能.

按天日志分类和超行日志分类

先说两个比较简单的
按天分类和超行分类

按天分类:每一个日志按照天来分类(日志前加上当前的日期作为日志的前缀) 并且写日志前检查日志的创建时间,如果日志创建时间不是今天,那么就额外新创建一个日志,更新创建时间和行数,然后向新日志中写日志信息

超行分类:写日志前检查本次程序写入日志的行数,如果当前本次程序写入日志的行数已经到达上限,那么额外创建新的日志,更新创建时间,然后向新日志中写日志信息

为了实现这两个小功能,我们需要先向日志类中添加以下成员:

  1. 程序本次启动,写入日志文件的最大行数
  2. 程序本次启动,已经写入日志的行数
  3. 日志的创建时间
  4. 日志的路径名+文件名(创建新日志的时候,命名要跟之前的命名标准一样,最好是标准日志名+后缀的形式,这样便于标识)

更新后的日志类:

Log.h:

#pragma once
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>
using namespace std;

class Log
{
private:
	
	Log() ;
	~Log();
	
public:
	//初始化文件路径,文件最大行数
	bool init(const char* file_name,int split_lines= 5000000);

	void write_log(const char* str);

	static Log* getinstance();

private:
	FILE* file;
	char dir_name[128];//路径名
	char log_name[128];//日志名

	int m_split_lines; //日志文件最大行数(之前的日志行数不计,只记录本次程序启动写入的行数)
	long long m_count; //已经写入日志的行数
	int m_today;       //日志的创建时间
};

Log.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Log.h"

Log::Log()
{
	m_count = 0;
}

Log::~Log()
{
	if (file != NULL)
	{
		fflush(file);
		fclose(file);
	}
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name,int split_lines)
{
	m_split_lines = split_lines;       //设置最大行数

	time_t t = time(NULL);
	struct tm* sys_tm = localtime(&t);
	struct tm  my_tm  = *sys_tm;       //获取当前的时间

	const char* p = strrchr(file_name, '/');//这里需要注意下,windows和linux的路径上的 斜杠符浩方向是不同的,windows是\,linux是 / ,而且因为转义符号的原因,必须是 \\
	char log_full_name[256] = { 0 };

	if (p == NULL)  //判断是否输入了完整的路径+文件名,如果只输入了文件名
	{
	    strcpy(log_name, file_name);
		snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
	}
	else            //如果输入了完整的路径名+文件名
	{
		strcpy(log_name, p + 1);
		strncpy(dir_name, file_name, p - file_name + 1);
		                              //规范化命名
		snprintf(log_full_name,255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
	}

	m_today = my_tm.tm_mday;          //更新时间

	file = fopen(log_full_name,"a");  //打开文件,打开方式:追加
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(const char* str)
{
    if (file == NULL)
	   return;
	   
	time_t t = time(NULL); 
	struct tm* sys_tm = localtime(&t); 
	struct tm  my_tm = *sys_tm;       //获取当前的时间,用来后续跟日志的创建时间作对比

	m_count++;                        //日志行数+1

	if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //如果创建时间!=当前时间或者本次写入行数达到上限
	{ 
		char new_log[256] = { 0 };    //新日志的文件名
		fflush(file);
		fclose(file);

		char time_now[16] = { 0 };    //格式化当前的时间
		snprintf(time_now, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);

		if (m_today != my_tm.tm_mday) //如果是创建时间!=今天
		{ //这里解释一下,m_today在init函数被调用的时候一定会被设置成当天的时间,只有init和write函数的调用不在同一天中,才会出现这种情况
		
			snprintf(new_log, 255, "%s%s%s", dir_name, time_now, log_name);
			m_today = my_tm.tm_mday;  //更新创建时间
			m_count = 0;              //更新日志的行数
		}
		else                          //如果是行数达到本次我们规定的写入上限
		{
			snprintf(new_log,255,"%s%s%lld_%s", dir_name, time_now, m_count / m_split_lines,log_name);//加上版本后缀
		}

		file = fopen(new_log, "a");
	}

	fputs(str, file);
	fputs("\n", file);
}

运行的结果:
在这里插入图片描述
出现了一个以时间开头命名的日志,实现了按天分类

接下来我将一次性写入行数的上限调成5,看一下如果一次性写入超过了行数上限的运行结果是什么样:
在这里插入图片描述
出现了一个后缀_1的新文件

PS:这里有一个小BUG:因为m_count是每次运行程序都会重置的一个变量,所以上一次运行时可能因为输出的行数过多,创建了好多新日志,但是下一次运行程序时,还是从第一个日志开始打印的. 而且规定行数上限并不是日志中文件行数的上限,而是每次运行程序写入日志文件的行数上限,所以这个功能并不完美甚至说非常鸡肋暂时还没优化好,在这里仅做一个小小的演示吧.

日志信息分级

我们应该将每一条日志信息进行分类,可以分为四大类:
Debug: 调试中产生的信息
WARN: 调试中产生的警告信息
INFO: 项目运行时的状态信息
ERROR: 系统的错误信息

然后我们可以在日志文件中,每一条日志信息的前面,加上这条信息被写入的时间和其所属的分级,这样会大大增加日志的可读性.

代码还是在上面代码的基础上继续改动
Log.h:

#pragma once
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>
using namespace std;

class Log
{
private:
	
	Log() ;
	~Log();
	
public:
	//初始化文件路径,日志缓冲区大小,文件最大行数
	bool init(const char* file_name, int log_buf_size = 8192, int split_lines= 5000000);

	//新增了一个日志分级
	void write_log(int level,const char* str);

	static Log* getinstance();

private:
	FILE* file;
	char dir_name[128];//路径名
	char log_name[128];//日志名

	int m_split_lines; //日志文件最大行数
	long long m_count; //日志当前的行数
	int m_today;       //日志创建的日期,记录是那一天

	int m_log_buf_size; //日志缓冲区的大小,用来存放日志信息字符串
	char* m_buf;        //日志信息字符串;因为后续要把时间和日志分级也加进来,所以开一个新的char *
};

Log.cpp:

#define _CRT_SECURE_NO_WARNINGS
#include"Log.h"

Log::Log()
{
	m_count = 0;
}

Log::~Log()
{
	if (file != NULL)
	{
		fflush(file);
		fclose(file);
	}

	
	if (m_buf != NULL)
	{
		delete[] m_buf;
		m_buf = nullptr;
	}
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name, int log_buf_size , int split_lines)
{
	m_log_buf_size = log_buf_size;
	m_buf = new char[m_log_buf_size];
	memset(m_buf,'\0', m_log_buf_size);// 开辟缓冲区,准备存放格式化的日志字符串


	m_split_lines = split_lines;       //设置最大行数

	time_t t = time(NULL);
	struct tm* sys_tm = localtime(&t);
	struct tm  my_tm  = *sys_tm;       //获取当前的时间

	const char* p = strrchr(file_name, '\\');
	char log_full_name[256] = { 0 };

	if (p == NULL)
	{
		snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
		strcpy(log_name, file_name);
	}
	else
	{
		strcpy(log_name, p + 1);
		strncpy(dir_name, file_name, p - file_name + 1);
		                              //规范化命名
		snprintf(log_full_name,255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
	}

	m_today = my_tm.tm_mday;          //更新日志的创建时间

	file = fopen(log_full_name,"a");  //打开文件,打开方式:追加
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(int level,const char* str)
{
	if (file == NULL)
		return;
	

	time_t t = time(NULL); 
	struct tm* sys_tm = localtime(&t); 
	struct tm  my_tm = *sys_tm;       //获取当前的时间,用来后续跟日志的创建时间作对比

	char level_s[16] = { 0 };         //日志分级
	switch (level)
	{
	case 0:
		strcpy(level_s, "[debug]:");
		break;
	case 1:
		strcpy(level_s, "[info]:"); 
		break;
	case 2:
		strcpy(level_s, "[warn]:"); 
		break;
	case 3:
		strcpy(level_s, "[erro]:"); 
		break;
	default:
		strcpy(level_s, "[info]:"); 
		break;
	}


	m_count++;                        //日志行数+1

	if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //如果创建时间!=当前时间或者行数达到上限
	{
		char new_log[256] = { 0 };    //新日志的文件名
		fflush(file);
		fclose(file);

		char time_now[16] = { 0 };    //格式化当前的时间
		snprintf(time_now, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);

		if (m_today != my_tm.tm_mday) //如果是创建时间!=今天
		{
			snprintf(new_log, 255, "%s%s%s", dir_name, time_now, log_name);
			m_today = my_tm.tm_mday;  //更新创建时间
			m_count = 0;              //更新日志的行数
		}
		else                          //如果是行数达到文件上限
		{
			snprintf(new_log,255,"%s%s%lld_%s", dir_name, time_now, m_count / m_split_lines,log_name);//加上版本后缀
		}

		file = fopen(new_log, "a");
	}


	
	int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d %s",
		my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
		my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec,level_s); 

	int m = snprintf(m_buf + n, m_log_buf_size-n-1,"%s",str);

	m_buf[n+m] = '\n';
	m_buf[n+m+1] = '\0';

	
	fputs(m_buf, file);

}

main.cpp:

#include<iostream>
#include"Log.h"

int main()
{
	Log::getinstance()->init("Log\\log.txt");

	Log::getinstance()->write_log(0,"Hello World");
	Log::getinstance()->write_log(1,"Hello World");
	Log::getinstance()->write_log(2,"Hello World");
	Log::getinstance()->write_log(3,"Hello World");
}

运行结果:
在这里插入图片描述
如图:日志信息前面已经加上了时间和类别分级,增加了可读性

同步和异步两种写入方式

同步写入和异步写入的逻辑:

图片来自公众号:两猿社
在这里插入图片描述
我先说明一下同步和异步的特点:
同步可以理解为顺序执行,而异步可以理解为并行执行
比如说吃饭和烧水两件事,如果先吃饭后烧水,这种是同步执行
如果说一边吃饭一边烧水,这种就是异步执行

那么同步执行和异步执行有什么优点,又使用在什么场景之下呢?

同步:

  1. 当对写入顺序和实时性要求很高时,例如需要确保按照特定顺序写入或写入即时生效的情况下,同步写入通常更合适。
  2. 在数据完整性和一致性很重要的情况下,同步写入能够提供更好的保证,避免数据丢失或不完整。
  3. 对于一些不频繁的、关键的写入操作,同步写入方式可能更容易确保操作的可靠性。

异步:

  1. 当写入频率很高或写入操作消耗较多时间时,使用异步写入可以显著提升系统性能和响应速度。
  2. 对于写入操作对主线程影响较大,容易阻塞主线程的情况下,通过异步写入可以将写入操作移到独立的线程中处理,减少主线程负担。
  3. 在需要降低I/O操作的影响、提高系统吞吐量和并发能力的场景下,异步写入方式更为适宜。

然后说一下如何实现同步和异步

同步只需要正常写入就行了
而异步我们可以借助生产者-消费者模型,由子线程执行写入操作.

如果你想了解生产者-消费者模型,请点击链接
生产者-消费者模型

接下来给出带有同步和异步两种写入方式的日志实现,还是在之前代码的基础上改动

封装了生产者-消费者模型的阻塞队列类:

#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h> //包含时间和定时器的头文件!
#include "../lock/locker.h"
using namespace std;

template <class T>
class block_queue
{
public:
    block_queue(int max_size = 1000)
    {
        if (max_size <= 0)
        {
            exit(-1);
        }

        m_max_size = max_size;
        m_array = new T[max_size];
        m_size = 0;
        m_front = -1;
        m_back = -1;
    }

    void clear()
    {
        m_mutex.lock();
        m_size = 0;
        m_front = -1;
        m_back = -1;
        m_mutex.unlock();
    }

    ~block_queue()
    {
        m_mutex.lock();
        if (m_array != NULL)
            delete [] m_array;

        m_mutex.unlock();
    }
    //判断队列是否满了
    bool full() 
    {
        m_mutex.lock();
        if (m_size >= m_max_size)
        {

            m_mutex.unlock();
            return true;
        }
        m_mutex.unlock();
        return false;
    }
    //判断队列是否为空
    bool empty() 
    {
        m_mutex.lock();
        if (0 == m_size)
        {
            m_mutex.unlock();
            return true;
        }
        m_mutex.unlock();
        return false;
    }
    //返回队首元素
    bool front(T &value) 
    {
        m_mutex.lock();
        if (0 == m_size)
        {
            m_mutex.unlock();
            return false;
        }
        value = m_array[m_front];
        m_mutex.unlock();
        return true;
    }
    //返回队尾元素
    bool back(T &value) 
    {
        m_mutex.lock();
        if (0 == m_size)
        {
            m_mutex.unlock();
            return false;
        }
        value = m_array[m_back];
        m_mutex.unlock();
        return true;
    }

    int size() 
    {
        int tmp = 0;

        m_mutex.lock();
        tmp = m_size;

        m_mutex.unlock();
        return tmp;
    }

    int max_size()
    {
        int tmp = 0;

        m_mutex.lock();
        tmp = m_max_size;

        m_mutex.unlock();
        return tmp;
    }
    //往队列添加元素,需要将所有使用队列的线程先唤醒
    //当有元素push进队列,相当于生产者生产了一个元素
    //若当前没有线程等待条件变量,则唤醒无意义
    bool push(const T &item)
    {

        m_mutex.lock();
        if (m_size >= m_max_size)  //这里没考虑生产者必须在不空的情况下需要等待的问题
        {
            m_cond.broadcast();
            m_mutex.unlock();
            return false;
        }

        m_back = (m_back + 1) % m_max_size;
        m_array[m_back] = item;

        m_size++;

        m_cond.broadcast();
        m_mutex.unlock();
        return true;
    }
    //pop时,如果当前队列没有元素,将会等待条件变量
    bool pop(T &item)
    {

        m_mutex.lock();
        while (m_size <= 0)
        {
            
            if (!m_cond.wait(m_mutex.get()))
            {
                m_mutex.unlock();
                return false;
            }
        }

        m_front = (m_front + 1) % m_max_size;
        item = m_array[m_front];
        m_size--;
        m_mutex.unlock();
        return true;
    }
private:
    locker m_mutex;
    cond m_cond;

    T *m_array;
    int m_size;
    int m_max_size;
    int m_front;
    int m_back;
};
#endif

注意: 这个生产者消费者模型并没有考虑生产者必须要在不满的情况下才能生产这一情况,不过这样也能凑活用,先凑活看吧

Log.h:

#pragma once
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>
using namespace std;

class Log
{
private:
	
	Log() ;
	~Log();

	//异步写入方法:
	void* async_write_log()
	{
		string single_log;//要写入的日志
		while (m_log_queue->pop(single_log))
        {
            m_mutex.lock();//互斥锁上锁
            fputs(single_log.c_str(), file);
            m_mutex.unlock();//互斥锁解锁
        }
	}
	
public:
	//初始化文件路径,日志缓冲区大小,文件最大行数,阻塞队列长度(如果阻塞队列长度为正整数,表示使用异步写入,否则为同步写入)
	bool init(const char* file_name, int log_buf_size = 8192, int split_lines= 5000000,int max_queue_size=0);

	//新增了一个日志分级
	void write_log(int level,const char* str);

	//公有的异步写入函数,作为消费者线程的入口函数
	static void* flush_log_thread(void *args)
	{
		Log::getinstance()->async_write_log();
	}

	static Log* getinstance();

private:
	FILE* file;
	char dir_name[128];//路径名
	char log_name[128];//日志名

	int m_split_lines; //日志文件最大行数
	long long m_count; //日志当前的行数
	int m_today;       //日志创建的日期,记录是那一天

	int m_log_buf_size; //日志缓冲区的大小,用来存放日志信息字符串
	char* m_buf;        //日志信息字符串;因为后续要把时间和日志分级也加进来,所以开一个新的char *

	block_queue<string>* m_log_queue; //阻塞队列,封装生产者消费者模型
	bool  m_is_async;                 //异步标记,如果为true,表示使用异步写入方式,否则是同步写入方式
	locker m_mutex;                   //互斥锁类,内部封装了互斥锁,用来解决多线程竞争资源问题
};

Log.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Log.h"
// pthread,mutex等需要在Linux下使用相关的头文件才能使用,因为我是windows环境就暂时不加了.

Log::Log()
{
	m_count = 0;
	m_is_async = false;
}

Log::~Log()
{
	if (file != NULL)
	{
		fflush(file);
		fclose(file);
	}

	
	if (m_buf != NULL)
	{
		delete[] m_buf;
		m_buf = nullptr;
	}
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name, int log_buf_size , int split_lines,int max_queue_size)
{
	if (max_queue_size >= 1)
	{
		//设置写入方式flag
		m_is_async = true;  //设置为异步写入方式
		
		//创建并设置阻塞队列长度
		m_log_queue = new block_queue<string>(max_queue_size);
		pthread_t tid;
		
		//flush_log_thread为回调函数,这里表示创建线程异步写日志
		pthread_create(&tid, NULL, flush_log_thread, NULL);
	}

	m_log_buf_size = log_buf_size;
	m_buf = new char[m_log_buf_size];
	memset(m_buf,'\0', m_log_buf_size);// 开辟缓冲区,准备存放格式化的日志字符串


	m_split_lines = split_lines;       //设置最大行数

	time_t t = time(NULL);
	struct tm* sys_tm = localtime(&t);
	struct tm  my_tm  = *sys_tm;       //获取当前的时间

	const char* p = strrchr(file_name, '\\');
	char log_full_name[256] = { 0 };

	if (p == NULL)
	{
		snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
		strcpy(log_name, file_name);
	}
	else
	{
		strcpy(log_name, p + 1);
		strncpy(dir_name, file_name, p - file_name + 1);
		                              //规范化命名
		snprintf(log_full_name,255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
	}

	m_today = my_tm.tm_mday;          //更新日志的创建时间

	file = fopen(log_full_name,"a");  //打开文件,打开方式:追加
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(int level,const char* str)
{
	if (file == NULL)
		return;
	

	time_t t = time(NULL); 
	struct tm* sys_tm = localtime(&t); 
	struct tm  my_tm = *sys_tm;       //获取当前的时间,用来后续跟日志的创建时间作对比

	char level_s[16] = { 0 };         //日志分级
	switch (level)
	{
	case 0:
		strcpy(level_s, "[debug]:");
		break;
	case 1:
		strcpy(level_s, "[info]:"); 
		break;
	case 2:
		strcpy(level_s, "[warn]:"); 
		break;
	case 3:
		strcpy(level_s, "[erro]:"); 
		break;
	default:
		strcpy(level_s, "[info]:"); 
		break;
	}

	m_mutex.lock();                   //互斥锁上锁
	m_count++;                        //日志行数+1

	if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //如果创建时间!=当前时间或者行数达到上限
	{
		char new_log[256] = { 0 };    //新日志的文件名
		fflush(file);
		fclose(file);

		char time_now[16] = { 0 };    //格式化当前的时间
		snprintf(time_now, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);

		if (m_today != my_tm.tm_mday) //如果是创建时间!=今天
		{
			snprintf(new_log, 255, "%s%s%s", dir_name, time_now, log_name);
			m_today = my_tm.tm_mday;  //更新创建时间
			m_count = 0;              //更新日志的行数
		}
		else                          //如果是行数达到文件上限
		{
			snprintf(new_log,255,"%s%s%lld_%s", dir_name, time_now, m_count / m_split_lines,log_name);//加上版本后缀
		}

		file = fopen(new_log, "a");
	}

	m_mutex.unlock();                 //互斥锁解锁

	string log_str; 
	m_mutex.lock();

   //格式化
	int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d %s",
		my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
		my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec,level_s); 
  
	int m = snprintf(m_buf + n, m_log_buf_size-n-1,"%s",str);

	m_buf[n+m] = '\n';
	m_buf[n+m+1] = '\0';

	
	log_str = m_buf;
	m_mutex.unlock();

	//如果是异步的写入方式
	if (m_is_async && !m_log_queue->full()) 
	 {
		m_log_queue->push(log_str); 
	}
	else//如果是同步的写入方式
	{
	    m_mutex.lock(); 
		fputs(log_str.c_str(), file);
		m_mutex.unlock(); 
	}
}

日志系统先介绍到这里,我介绍的日志系统还是属于功能比较稀缺,实际使用上可能远远比这复杂,如果想使用日志系统,可以以文章介绍的为雏形继续添加新功能.

本文中代码非常可能有错误,如果发现有错误,烦请评论区指正,我会及时修改.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1445173.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

02.数据结构

一、链表 作用&#xff1a;用于写邻接表&#xff1b; 邻接表作用&#xff1a;用于存储图或树&#xff1b; 1、用数组模拟单链表 #include<iostream> using namespace std;const int N 100010;// head 表示头结点的下标 // e[i] 表示结点i的值 // ne[i] 表示结点i的ne…

python适配器模式开发实践

1. 什么是适配器设计模式&#xff1f; 适配器&#xff08;Adapter&#xff09;设计模式是一种结构型设计模式&#xff0c;它允许接口不兼容的类之间进行合作。适配器模式充当两个不兼容接口之间的桥梁&#xff0c;使得它们可以一起工作&#xff0c;而无需修改它们的源代码。 …

数字孪生:构建未来智慧社区的关键技术

随着科技的快速发展&#xff0c;数字孪生技术作为构建未来智慧社区的关键技术&#xff0c;正逐渐受到广泛关注。数字孪生技术能够实现物理世界与数字世界的交互映射&#xff0c;为智慧社区的建设提供强有力的支持。本文将探讨数字孪生技术在构建未来智慧社区中的作用和意义&…

蓝牙BLE学习

1. 简介 1.1 蓝牙发展历程 蓝牙&#xff0c;直接来自于一位国王的名字--King Harald ‘Bluetooth Gromsson。这位国王因两件事留名于史&#xff0c;其一是在公园958年统一了丹麦和挪威&#xff0c;其二是在其死后&#xff0c;其牙齿呈现出暗蓝色的颜色&#xff0c;因而得名蓝牙…

【Tauri】(1):使用Tauri1.5版本,进行桌面应用开发,在windows,linux进行桌面GUI应用程序开发,可以打包成功,使用 vite 最方便

1&#xff0c;视频地址&#xff1a; https://www.bilibili.com/video/BV1Pz421d7s4/ 【Tauri】&#xff08;1&#xff09;&#xff1a;使用Tauri1.5版本&#xff0c;进行桌面应用开发&#xff0c;在windows&#xff0c;linux进行桌面GUI应用程序开发&#xff0c;可以打包成功&…

Educational Codeforces Round 135 (Rated for Div. 2)C. Digital Logarithm(思维)

文章目录 题目链接题意题解代码 题目链接 C. Digital Logarithm 题意 给两个长度位 n n n的数组 a a a、 b b b&#xff0c;一个操作 f f f 定义操作 f f f为&#xff0c; a [ i ] f ( a [ i ] ) a [ i ] a[i]f(a[i])a[i] a[i]f(a[i])a[i]的位数 求最少多少次操作可以使 …

电路设计(16)——纪念馆游客进出自动计数显示器proteus仿真

1.设计要求 设计、制作一个纪念馆游客进出自动计数显示器。 某县&#xff0c;有一个免费参观的“陶渊明故里纪念馆”&#xff0c;游客进出分道而行&#xff0c;如同地铁有确保单向通行的措施。在入口与出口处分别设有红外检测、声响、累加计数器装置&#xff0c;当游人进&#…

fast.ai 机器学习笔记(一)

机器学习 1&#xff1a;第 1 课 原文&#xff1a;medium.com/hiromi_suenaga/machine-learning-1-lesson-1-84a1dc2b5236 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它&#xff0c;这些笔记将继续更…

【蓝桥杯省赛真题23】python水仙花数 青少年组蓝桥杯比赛python编程省赛真题解析

目录 python水仙花数 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python水仙花数 第十二届蓝桥杯青少年组python比赛省赛真题 一、题目要求…

【ES6】模块化

nodejs遵循了CommonJs的模块化规范 导入 require() 导出 module.exports 模块化的好处&#xff1a; 模块化可以避免命名冲突的问题大家都遵循同样的模块化写代码&#xff0c;降低了沟通的成本&#xff0c;极大方便了各个模块之间的相互调用需要啥模块&#xff0c;调用就行 …

年底总结:野生码农与辉煌的2023

目录 始于兴趣终与坚持成于热爱2024 flag 始于兴趣 那是在遥远的2018年&#xff0c;我从机械行业跨入IT领域&#xff0c;当时觉得写代码非常酷的事&#xff0c;而且开发出来的功能可以提高机械制造效率&#xff0c;那种成就感油然而生。展望着未来&#xff0c;我觉得自己或许正…

8.【CPP】Vector(扩容问题||迭代器失效问题简述迭代器的种类)

vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以动态改变的&#xff0c;而且它的大小会被容器自…

大数据Doris(六十五):基于Apache Doris的数据中台2.0

文章目录 基于Apache Doris的数据中台2.0 一、​​​​​​​架构升级

117.乐理基础-五线谱-音值组合法(二)

内容参考于&#xff1a;三分钟音乐社 上一个内容&#xff1a;116.乐理基础-五线谱-音值组合法&#xff08;一&#xff09;-CSDN博客 分母大于等于八的所有拍号的音值组合法&#xff0c;对于这些大于等于八的&#xff0c;可以用一句话来形容&#xff0c;那就是叫做&#xff0c…

骑砍战团MOD开发(44)-可编程渲染管线shader编程

一.可编程渲染管线 在GPU进行3D模型投射到2D平面过程中,渲染管线算法对开发者开放,目前支持的编程语言有OpenGL的ARB语言(pp文件),Direct3D的HLSL高级shader编程语言(fx文件). Direct3D提供一下API实现程序加载shader着色器文件: D3DXCreateEffectFromFile(gDevice,"fxfn…

使用 MinIO 超级充电 TileDB 引擎

MinIO 是一个强大的主要 TileDB 后端&#xff0c;因为两者都是为性能和规模而构建的。MinIO 是一个单一的 Go 二进制文件&#xff0c;可以在许多不同类型的云和本地环境中启动。它非常轻量级&#xff0c;但也具有复制和加密等功能&#xff0c;并且提供与各种应用程序的集成。Mi…

分享76个时间日期JS特效,总有一款适合您

分享76个时间日期JS特效&#xff0c;总有一款适合您 76个时间日期JS特效下载链接&#xff1a;https://pan.baidu.com/s/1s7tPGT_ItK7dNK5_qbZkug?pwd8888 提取码&#xff1a;8888 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;…

java基础(2) 面向对象编程-java核心类

面向对象 面向对象对应的就是面向过程&#xff0c; 面向过程就是一步一步去操作&#xff0c;你需要知道每一步的步骤。 面向对象的编程以对象为核心&#xff0c;通过定义类描述实体及其行为&#xff0c;并且支持继承、封装和多态等特性 面向对象基础 面向对象编程&#xff0…

C++进阶(十四)智能指针

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、为什么需要智能指针&#xff1f;二、内存泄漏1、 什么是内存泄漏&#xff0c;内存泄漏的危…

探索现代Web前端开发框架:选择最适合你的工具

在当今快速发展的Web开发领域&#xff0c;前端开发框架的选择显得尤为关键。这些框架可以帮助我们更高效地构建出交互性强、性能卓越的用户界面。本文将带你了解几个当前最受欢迎的Web前端开发框架&#xff0c;并帮助你根据自己的需求选择最合适的工具。 1. React React由Fac…