基于单例模式的日志输出(C++)

原文:https://blog.csdn.net/giser_mxd/article/details/75557410

  在软件运行中,系统一般会采用一个持久化的日志系统来记录运行情况。
  本题你需要实现一个简易的 logger 日志类,这个类主要负责辅助调试,将日志信息输出到一个日志文件中。
  • 1
  • 2

  比赛预期的输出为同一目录下的 shiyanloulogger.log日志文件,文件的内容如下:

shiyanlou logger test start[2017.07.20 16:13:02 Thursday]
info info info[2017.07.20 16:13:02 Thursday]
shiyanlou logger test end[2017.07.20 16:13:02 Thursday]
  • 1
  • 2
  • 3

  程序的主要目标为: 
  1. 实现 Logger 简单的单例实现(自动垃圾回收) 
  2. 日志文件需要使用追加的方式添加日志

  C++单例模式也称为单件模式、单子模式。使用单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很多地方需要这样的功能模块,如系统的日志输出等。参考1

  在具体实现的时候需要考虑两个问题: 
  1. 单例模式的实现问题 
  这样的例子有很多,参考1,单例模式的好处就是保证这个类的实例是唯一的,从而不会产生冲突。 
  2. 日志文件流的生命周期 
  在进行日志输出的时候,当日志对象创建的时候,日志文件流就应该已经创建。当日志对象结束的时候,文件流也应当关闭。这样不用每次写一行日志的时候就要重新生成打开关闭文件,效率较高。 
  3. 数据释放问题 
  可以在日志类里面创建一个私有类,在程序运行结束时,自动调用私有类的析构函数,在析构函数中进行释放当前日志对象和关闭文件等操作。这样,不需要在外部函数中进行释放,不用关心日志对象的释放问题。 
  最终日志对象的头文件easyLogger.h如下:

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <memory>
#include <ctime>
#include <iostream>
#include <fstream>

using namespace std;

class easyLogger
{
public:
    static easyLogger *myInst()
    {
        if (NULL == _instance)
        {
            _instance = new  easyLogger();
            // write to easyLogger.log
            _instance->ofs.open("logger.log",ios::app);
        }
        return _instance;
    }

    void Log(const string& logInfo);

private:
    easyLogger(void) {}
    virtual ~easyLogger(void) {}
    friend class auto_ptr<easyLogger>;
    static easyLogger *_instance;
    char tmp[100];
    ofstream ofs;// 输出文件流
    class CGarbo // 它的唯一工作就是在析构函数中删除easyLogger的实例
    {
    public:
        ~CGarbo()
        {
            if(easyLogger::_instance)
            {
                easyLogger::_instance->ofs.close();//关闭文件流
                delete easyLogger::_instance;
                easyLogger::_instance = NULL;
            }
        }
    };
    static CGarbo Garbo;
};
easyLogger *easyLogger::_instance = NULL;
void easyLogger::Log(const string& logInfo)
{
    time_t t = time(0);
    strftime(tmp, sizeof(tmp), "[%Y.%m.%d %X %A]", localtime(&t));
    ofs << logInfo.c_str()<<tmp<<endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

  接下来我们进行日志写入的测试,代码如下:

#include "stdafx.h"
#include <iostream>
#include <ctime>
#include <cstdlib>
#include "easyLogger.h"

#define NUM 100000  // 单次写入的日志条数

using namespace std;

char logdata[NUM][100];

double random(double start,double end)
{
    return start+(end-start)*rand()/(RAND_MAX+1.0);
}

// 测试日志写入的效率
int main()
{
    // 以当前的时间作为种子,这样每次生成的随机数都是不一样的,否则每次随机数都是一样的
    // srand(unsigned(time(0)));
    clock_t t1,t2;
    int sum_time =0;
    // 测试10次
    for (int k=0;k<10;k++)
    {
        // 生成随机的数据
        t1 = clock();
        for(int i = 0;i<NUM;++i)
        {
            for(int j =0;j<100-1;++j)
                logdata[i][j] = rand()%74+48;
            logdata[i][99] = '\0';
        }
        t2 = clock();
        cout<<"生成日志数据"<<NUM<<"条,耗费时间:"<<(t2-t1)<<"ms"<<endl;

        // 进行日志写入
        t1 = clock();
        for(int i = 0;i<NUM;++i)
            easyLogger::myInst()->Log(logdata[i]);
        t2 = clock();
        cout<<"写入日志数据"<<NUM<<"条,耗费时间:"<<(t2-t1)<<"ms"<<endl;
        sum_time += t2-t1;
    }
    cout<<"平均每次花费时间为:"<<sum_time/10<<"ms"<<endl;
    system("pause");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

  最终在本平台(windows 10 64位+VS2010 +win32 debug模式下)运行结果如下: 
log_test_resulut
  程序其实还有需要改进的地方,比如说多线程环境下是否还能保证类的实例是唯一的、如何指定输出路径、以及如何能够进一步提升大量数据情况下的写入性能等等。



localtime()函数:获取当前时间和日期并转换为本地时间

1. time 函数
    返回1970-1-1, 00:00:00以来经过的秒数
    原型: time_t time(time_t *calptr) 
         结果可以通过返回值,也可以通过参数得到,见实例
    头文件 <time.h>
    返回值: 
        成功:秒数,从1970-1-1,00:00:00 可以当成整型输出或用于其它函数
        失败:-1
    例:
      time_t now;
      time(&now);// 等同于now = time(NULL)
      printf("now time is %d\n", now);
 
2. localtime函数
         将时间数值变换成本地时间,考虑到本地时区和夏令时标志;
    原型: struct tm *localtime(const time_t * calptr); 
    头文件 <time.h>
 返回值:
        成功: struct tm *结构体, 原型如下:
            struct tm {
                   int tm_sec;       /* 秒 – 取值区间为[0,59] */ 
                   int tm_min;       /* 分 - 取值区间为[0,59] */ 
                   int tm_hour;      /* 时 - 取值区间为[0,23] */ 
                   int tm_mday;     /* 一个月中的日期 - 取值区间为[1,31] */ 
                   int tm_mon;     /* 月份(从一月开始,0代表一月) - 取值区间为[0,11] */ 
                   int tm_year;     /* 年份,其值等于实际年份减去1900 */ 
                   int tm_wday;    /* 星期 – 取值区间为[0,6],其中0代表星期天,1代表星期一 */ 
                   int tm_yday;    /* 从每年1月1日开始的天数– 取值区间[0,365],其中0代表1月1日 */ 
                   int tm_isdst;    /* 夏令时标识符,夏令时tm_isdst为正;不实行夏令时tm_isdst为0 */    
            };
                此结构体空间由内核自动分配, 而且不要去释放它.
        失败: NULL
 
    例:
        time_t now ;
        struct tm *tm_now ;
        time(&now) ;
        tm_now = localtime(&now) ;
        printf("now datetime: %d-%d-%d %d:%d:%d\n", tm_now->tm_year+1900, tm_now->tm_mon+1, tm_now->tm_mday, tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec) ;
void open (const char * filename, openmode mode);

这里filename 是一个字符串,代表要打开的文件名,mode 是以下标志符的一个组合:

ios::in 为输入(读)而打开文件
ios::out 为输出(写)而打开文件
ios::ate 初始位置:文件尾
ios::app 所有输出附加在文件末尾
ios::trunc 如果文件已存在则先删除该文件
ios::binary 二进制方式

这些标识符可以被组合使用,中间以”或”操作符(|)间隔。例如,如果我们想要以二进制方式打开文件"example.bin" 来写入一些数据,我们可以通过以下方式调用成员函数open()来实现:

ofstream file;
file.open ("example.bin", ios::out | ios::app | ios::binary);

ofstream, ifstream 和 fstream所有这些类的成员函数open 都包含了一个默认打开文件的方式,这三个类的默认方式各不相同:

参数的默认方式
ofstream ios::out | ios::trunc
ifstream ios::in
fstream ios::in | ios::out

只有当函数被调用时没有声明方式参数的情况下,默认值才会被采用。如果函数被调用时声明了任何参数,默认值将被完全改写,而不会与调用参数组合。


猜你喜欢

转载自blog.csdn.net/qq_33890670/article/details/80218151