【WebServer项目:https://github.com/Hansimov/linux-server】Day02 - 日志系统

day02: log – 日志系统

代码

log.h文件

#ifndef LOG_H
#define LOG_H

#include"block_queue.h"
#include<pthread.h>
#include<iostream>
using namespace std;
#include<stdio.h>
#include<string>
#include<stdarg.h>

//因为WebServer是多线程服务器,所以,必须使用某种方法,保证对日志的操纵是线程安全的,这里采用的是 静态局部变量的单例模式懒汉方法保证线程安全
class Log{

private:
    char dir_name[128]; // 路径名
    char log_name[128]; //日志文件名
    int m_split_lines; //日志文件的最大行数
    int m_log_buf_size; //日志文件的缓冲区大小
    long long m_count; //日志文件的行数记录
    int m_today; //因为按天分类,所以需要记录当前是哪一天
    FILE* m_fp; //打开log文件的文件指针
    char *m_buf; //缓冲区指针
    block_queue<string>*m_log_queue; // 阻塞队列,用来存放异步日志模式的数据,相当于日志数据的缓存,还需要调用async_write_log函数将其写入日志文件中
    bool m_is_async; //是否同步标志位
    locker m_mutex; //互斥锁,用来保证对日志位写是线程安全的
    int m_close_log; //关闭日志


//单例模式,使用局部变量懒汉不用加锁
private: //构造函数、析构函数、异步函数私有,只能在类内调用
    Log(); 
    virtual ~Log();
    void *async_write_log(){ //异步函数:检查阻塞队列中是否有数据需要写入日志文件的
        string  single_log;
        while(m_log_queue->pop(single_log)){ //从阻塞队列中取出一个数据(如果有的话)
            m_mutex.lock();
            fputs(single_log.c_str(), m_fp); //将数据,写入日志文件
            m_mutex.unlock();
        }
    }

//单例模式的对外接口
public:
    static Log* get_instance() {
        static Log instance; //这个变量只会有一份,只会被初始化一次,静态成员函数和静态成员变量的特性
        return &instance;
    }
    static void* flush_log_thread(void* args){ //线程的主函数,功能是从阻塞队列中读取数据到日志文件
        Log::get_instance()->async_write_log(); 
        /**
         * 1. 类的静态函数和静态变量不属于任何一个对象,如果需要使用,只能通过类域名
         * 2. async_write_log()是类的私有函数,只能在类内调用
         * 3. Log::get_instance():获取这个类的唯一实例对象指针
         * 4. 调用这实例的私有方法
        */
    } 
    bool init(const char*file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000,int max_queue_size = 0);
    void write_log(int level, const char *format,...);
    void flush(void);
};  

//日志信息的级别:调试、普通、警告、错误
#define LOG_DEBUG(format,..) if(m_close_log == 0){Log::get_instance()->write_log(0,format,##_VA_ARGS_);Log::get_instance()->flush();}
#define LOG_INFO(format,..) if(m_close_log == 0){Log::get_instance()->write_log(1,format,##_VA_ARGS_);Log::get_instance()->flush();}
#define LOG_WANRN(format,..) if(m_close_log == 0){Log::get_instance()->write_log(2,format,##_VA_ARGS_);Log::get_instance()->flush();}
#define LOG_ERROR(format,..) if(m_close_log == 0){Log::get_instance()->write_log(3,format,##_VA_ARGS_);Log::get_instance()->flush();}
/*
	
这是一个宏定义,用于记录调试级别的日志信息。它接受一个格式字符串和可变数量的参数,并将它们传递给Log::get_instance()->write_log()方法来记录日志信息。

这个宏定义首先检查m_close_log变量的值是否为0。如果为0,则表示日志系统没有被关闭,可以记录日志信息。否则,不记录日志信息。

使用宏定义来记录日志信息有几个好处。首先,它可以简化代码,使得记录日志信息更加方便。例如,使用上面的宏定义,我们可以这样记录一条调试信息:

LOG_DEBUG("This is a debug message: %d", 123);
其次,宏定义可以提高性能。在上面的宏定义中,我们首先检查了m_close_log变量的值,如果日志系统已经被关闭,则不会调用write_log()方法。这样可以避免不必要的函数调用,从而提高性能。

通常,在需要频繁记录日志信息的地方使用宏定义来简化代码和提高性能。

另外: __VA_ARGS__ 是一个可变参数的宏

*/



#endif

log.cpp文件

```#include"log.h"
#include<pthread.h>
#include<string.h> //memset函数的头文件
#include<time.h> //localtime()函数的头文件
#include<stdarg.h> //获取可变形参参数的个数

Log::Log(){
    m_count = 0; //只会调用一次
    m_is_async = false;
}

/**
    instance是一个静态局部变量,它是在栈上分配的,而不是在堆上分配的。因此,不需要使用delete来释放它。

    静态局部变量的生命周期与程序的生命周期相同。它在程序运行期间一直存在,直到程序结束。当程序结束时,静态局部变量会被自动销毁。
*/
Log::~Log(){ //关闭文件流
    if(m_fp != NULL){
        fclose(m_fp);
    }
}

/*
    异步需要设置阻塞队列的长度,而同步不需要,初始化的步骤:
    1. 根据传进来的参数确定是异步写还是同步写,如果是异步写,需要初始化阻塞队列,并且创建一个线程,负责把阻塞队列中的数据同步到日志文件中
    2. 根据形参,初始化一些成员变量,比如,日志文件开闭状态、缓冲区大小、
    3. 调用时间有关的函数,获取到当前的时间,因为日志文件是以日期为标准进行分类的,更新日期变量
    4. 处理日志文件名称:
        1. 如果当前文件名称是一个不包含'/'的,比如说是'day01_log.txt',
            1.  就是把时间格式拼接到文件名之前,变成:'2023_4_17_day01_log.txt'
        2. 如果当前文件名称是一个包含'/'的,比如说是'log/day01_log.txt';
            1.  这个p指针会定位到'/',然后在p指针之后字符串复制给 log_name,因为后面就是日志名称
            2.  复制'/'之前的那段字符串,作为日志文件的路径,也即是'log/'
            3.  就是把时间格式拼接到文件名之前,变成:'log/2023_4_17_day01_log.txt'
    5.  根据这个处理之后的文件名,打开(如果不存在,就创建)文件,使用文件流的形式,“a”模式,表示追加
    6.  初始化完成,等待数据的写入;
*/
bool Log::init(const char*file_name, int close_log, int log_buf_size , int split_lines,int max_queue_size){
    //如果传进来的参数中有设置了阻塞队列的长度,则证明,使用这个实例的线程想要异步日志
    if(max_queue_size > 1) {
        m_is_async = true; //当前为异步日志
        m_log_queue = new block_queue<string>(max_queue_size); //申请阻塞队列内存
        pthread_t tid; //创建一个线程,负责把阻塞队列中的数据同步到日志文件中
        int ret =  pthread_create(&tid,NULL,flush_log_thread,NULL); 
        // flush_log_thread为回调函数,这里表示创建线程异步写日志
        //回调函数:把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。
        //如果代码立即被执行就称为同步回调,如果在之后晚点的某个时间再执行,则称之为异步回调。        
    }

    m_close_log = close_log;
    m_log_buf_size = log_buf_size;
    m_buf = new char[m_log_buf_size];
    memset(m_buf,'\0',m_log_buf_size);

    time_t t = time(NULL); 
    //t 是个从1970-01-01 00:00:00 UTC开始算起的时间变量,它是以秒为单位的,time函数以 time_t 对象返回当前日历时间。
    struct tm*sys_tm = localtime(&t); 
    //     使用 t的值,来填充 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 到 11                */
    //     int tm_year;        /* 自 1900 起的年数                */
    //     int tm_wday;        /* 一周中的第几天,范围从 0 到 6                */
    //     int tm_yday;        /* 一年中的第几天,范围从 0 到 365                    */
    //     int tm_isdst;       /* 夏令时                        */    
    //     };
    //      该函数返回指向 tm 结构的指针,该结构带有被填充的时间信息。
    
    struct tm my_tm = *sys_tm;

    const char *p = strrchr(file_name,'/');
    /*
        1.  在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
        2.  str -- C 字符串。
            c -- 要搜索的字符。以 int 形式传递,但是最终会转换回 char 形式。
        3.  该函数返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针。
    */
   char log_full_name[256] = {0};

   if(p == NULL){
    snprintf(log_full_name,255,"%d_%02d_%02d_%02d_%s",my_tm.tm_year+1990,my_tm.tm_mon+1,my_tm.tm_mday,file_name);
    /*
        1. 描述:C 库函数 int snprintf(char *str, size_t size, const char *format, ...) 
                设将可变参数(...)按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,
                超过 size 会被截断,最多写入 size-1 个字符。
                与 sprintf() 函数不同的是,snprintf() 函数提供了一个参数 size,可以防止缓冲区溢出。
                如果格式化后的字符串长度超过了 size-1,则 snprintf() 只会写入 size-1 个字符,
                并在字符串的末尾添加一个空字符(\0)以表示字符串的结束。
        2. 参数:
                str -- 目标字符串,用于存储格式化后的字符串的字符数组的指针。
                size -- 字符数组的大小。
                format -- 格式化字符串。
                ... -- 可变参数,可变数量的参数根据 format 中的格式化指令进行格式化。
        3. 返回值:
                snprintf() 函数的返回值是输出到 str 缓冲区中的字符数,不包括字符串结尾的空字符 \0。
                如果 snprintf() 输出的字符数超过了 size 参数指定的缓冲区大小,则输出的结果会被截断,
                只有 size - 1 个字符被写入缓冲区,最后一个字符为字符串结尾的空字符 \0。
                需要注意的是,snprintf() 函数返回的字符数并不包括字符串结尾的空字符 \0,
                因此如果需要将输出结果作为一个字符串使用,则需要在缓冲区的末尾添加一个空字符 \0。
    */

   }else{
    strcpy(log_name,p+1);
    strncpy(dir_name,file_name,p-file_name+1);
    /*
        1. 描述:C 库函数 char *strncpy(char *dest, const char *src, size_t n) 
                把 src 所指向的字符串复制到 dest,最多复制 n 个字符。
                当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。
        2. 参数:
                dest -- 指向用于存储复制内容的目标数组。
                src -- 要复制的字符串。
                n -- 要从源中复制的字符数。
        3. 返回值
                该函数返回最终复制的字符串。
        
        p - file_name + 1表示从file_name字符串的开头到最后一个出现的'/'字符之间的字符数量(包括'/'字符)。
        因此,这段代码会将file_name字符串中从开头到最后一个出现的'/'字符之间的部分复制到dir_name字符串中。
    */
    snprintf(log_full_name,255,"%s%d_%02d_%02d_%02d_%s",dir_name,my_tm.tm_year+1990,my_tm.tm_mon+1,my_tm.tm_mday,file_name);
   }

   m_today = my_tm.tm_mday;

   m_fp = fopen(log_full_name,"a");
   /* 
        模式	描述
        "r"	    打开一个用于读取的文件。该文件必须存在。
        "w"	    创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。
        "a"	    追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。
        "r+"	打开一个用于更新的文件,可读取也可写入。该文件必须存在。
        "w+"	创建一个用于读写的空文件。
        "a+"	打开一个用于读取和追加的文件。
   */

   if(m_fp == NULL){
    return false;
   }
   return true;  
}

/*
    往日志文件中写入数据:一条完整的日志信息,必须注明:日志级别、时间、内容
    1.  获取当前系统的时间
    2.  根据形参level获取该条日志信息的日志级别
    3.  判断该条日志消息是什么时候产生的,或者是否需要新开一页?如果需要,那么就把文件流更新,m_fp = new_log。
        1.  这里需要注意,因为是单例模式,所以只有一个实例,这个实例里面记录的时间可能是昨天的,
            如果这条日志消息是今天产生的,就需要新打开一个文件,把数据写进去,因为log是按照日期day来分类的。
        2.  如果某一天产生的日志消息很多,也需要新打开一个文件,把数据续写下去。
    4.  根据形参,把 日志级别-日期-内容 拼接成一条日志信息,存放在 一个字符串数组中
    5.  到这里,已经获得了一条完整的日志内容,接下来就是根据 异步 还是 同步,把数据 写入 阻塞队列 或者 直接写入文件。
    6.  往日志里面写数据操作完成。
*/
void Log::write_log(int level,const char*format,...){
    struct timeval now = {0,0};

    gettimeofday(&now,NULL);

    time_t t = now.tv_sec;

    struct tm* sys_tm = localtime(&t);
    struct tm my_tm = *sys_tm;
    char s[16] = {0};
    //获取日志级别
    switch (level)
    {
    case 0 :
        strcpy(s,"[debug]:");
        break;
    case 1 :
        strcpy(s,"[info]:");
        break;
    case 2:
        strcpy(s,"[warn]:");
        break;
    case 3 :
        strcpy(s,"[error]:");
        break;
        
    default:
        break;
    }
    //写入一个log,对m_count++, m_split_lines最大行数
    m_mutex.lock();

    m_count++; //日志条数++

    if(m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyDay log?
    {
        char new_log[256] = {0}; //新开一个文件
        fflush(m_fp);
        fclose(m_fp);
        char tail[16]={0};

        snprintf(tail,16,"%d_%02d_%02d_",my_tm.tm_year+1990,my_tm.tm_mon+1,my_tm.tm_mday);

        if(m_today != my_tm.tm_mday){ //如果当前的日志信息不是今天的,那就另一个新的log,因为log是按照日期day来分类的
            snprintf(new_log,255,"%s%s%s",dir_name,tail,log_name);
            m_today = my_tm.tm_mday;
            m_count = 0; //新文件,重新计算日志条数
        }else{ //如果这天产生的日志数量太多了,也需要新开一个日志文件
            snprintf(new_log,255,"%s%s%s.%lld",dir_name,tail,log_name,m_count/m_split_lines);
        }

        m_fp = fopen(new_log,"a"); //保证打开是一个新的日志文件
    }
    m_mutex.unlock();
   
    va_list valst; //申请参数列表变量 -- 形参就是日志消息的内容
    va_start(valst,format); // format之后的参数都会放在valst列表中
    string log_str;
    /*
        1. 简介
                stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。

                可变参数的函数通在参数列表的末尾是使用省略号(,...)定义的。

        2.库变量
                下面是头文件 stdarg.h 中定义的变量类型:

                序号	变量& 描述
                1	    va_list 这是一个适用于 va_start()、va_arg() 和 va_end() 这三个宏存储信息的类型。
        3. 库宏
                下面是头文件 stdarg.h 中定义的宏:
                序号	宏 & 描述
                1	void va_start(va_list ap, last_arg)
                    这个宏初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
                2	type va_arg(va_list ap, type)
                    这个宏检索函数参数列表中类型为 type 的下一个参数。
                3	void va_end(va_list ap)
                    这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。
    */

   m_mutex.lock();

    //写入的具体时间内容格式
    int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %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, now.tv_usec, s);
    
    int m = vsnprintf(m_buf+n,m_log_buf_size-1,format,valst);
    /*
        int vsnprintf (char * s, size_t n, const char * format, va_list arg );

        描述:

        将格式化数据从可变参数列表写入大小缓冲区
        如果在printf上使用格式,则使用相同的文本组成字符串,但使用由arg标识的变量参数列表中的元素而不是附加的函数参数,

        并将结果内容作为C字符串存储在s指向的缓冲区中 (以n为最大缓冲区容量来填充)。

        如果结果字符串的长度超过了n-1个字符,则剩余的字符将被丢弃并且不被存储,而是被计算为函数返回的值。
        在内部,函数从arg标识的列表中检索参数,就好像va_arg被使用了一样,因此arg的状态很可能被调用所改变。
        在任何情况下,arg都应该在调用之前的某个时刻由va_start初始化,并且在调用之后的某个时刻,预计会由va_end释放。

        参数:

        s

        指向存储结果C字符串的缓冲区的指针。
        缓冲区应至少有n个字符的大小。

        n

        在缓冲区中使用的最大字节数。
        生成的字符串的长度至多为n-1,为额外的终止空字符留下空间。
        size_t是一个无符号整数类型。

        format

        包含格式字符串的C字符串,其格式字符串与printf中的格式相同

        arg

        标识使用va_start初始化的变量参数列表的值。
        va_list是在<cstdarg>中定义的特殊类型。

        返回值:

        如果n足够大,则会写入的字符数,不包括终止空字符。
        如果发生编码错误,则返回负数。
        注意,只有当这个返回值是非负值且小于n时,字符串才被完全写入。

    */

    m_buf[n+m]='\n'; //换行
    m_buf[n+m]='\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(), m_fp);
        m_mutex.unlock();
    }

    va_end(valst); //清空参数列表, 并置参数指针arg_ptr无效.
}

//强制刷新,写入流缓冲区
void Log::flush(void){
    m_mutex.lock();
    fflush(m_fp);
    m_mutex.unlock();
}

================= 一些可能有点重复的内容 =====================

总结:

同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备.

  • 自定义阻塞队列
  • 单例模式创建日志
  • 同步日志
  • 异步日志
  • 实现按天、超行分类

问题:

为什么pop函数需要参数,当前元素的下一个一定会有值吗?如果是空的怎么办?

  1. 首先,需要明确一点,这个阻塞队列是用来坐什么的,这个阻塞队列是用来暂时存放异步日志的数据的,也就是后期需要从这里读取数据,写进日志系统中
  2. 其次,pop函数的功能是,从当前阻塞队列中获取一个元素(比如字符串类型的数据),把这个元素的值取出来,复制到日志文件中
  3. 最后,正是pop函数不止普通的"弹出一个元素"的功能,他还需要取出(复制)这个元素的值,所以需要一个参数,接受这个值。
  4. 如果没有数据,就会返回false,证明,这时候的阻塞队列中并没有数据,也就是,异步日志的数据已经更新完毕了(全都写进了日志文件中)

为什么获取队列大小的函数中需要加锁,什么情况下会造成它的线程不安全?直接返回不行吗?

  1. 不可以直接返回,会造成线程不安全
  2. 考虑这种情况:
    1. 如果不加锁,线程A需要返回变量m_size, 而线程B正在修改m_size, 那么,线程A获得的值就无法保证其唯一性了,可能是B修改前的,也可能是B修改后的。
    2. 如果加了锁,线程A需要返回变量m_size, 而线程B正在修改m_size,那么,由于A正在访问(在临界区中),B不可以进入修改,从而保证了,A获得的数据是B修改前的。

增加一个超时处理的pop函数有什么作用?里面的实现逻辑是怎样的?

  1. 超时处理和不超时处理的主要区别在于:当遇到没有数据可读是,什么时候返回?
  2. 首先确定的是,如果没有数据可读,会尝试阻塞等待,这是 条件变量的一个特点
  3. 其次,如果阻塞失败,会立即返回false;
  4. 最后,如果阻塞成功,超时处理会阻塞一定的时间,然后如果还没有数据,就会返回false;而不超时处理的话,就会一直等待;

类的公有成员和私有成员有什么区别

  1. 公有成员:类内和类外都可访问。
  2. 私有成员:类内可访问,类外不可访问。友元可访问。什么是友元,下面会介绍。

学习一些没有遇到过的库函数

  1. 函数的宏定义:

    #define LOG_DEBUG(format,..) if(m_close_log == 0){Log::get_instance()->write_log(0,format,##_VA_ARGS_);Log::get_instance()->flush();} 
    ```
    这是一个宏定义,用于记录调试级别的日志信息。它接受一个格式字符串和可变数量的参数,并将它们传递给Log::get_instance()->write_log()方法来记录日志信息。
    
    这个宏定义首先检查m_close_log变量的值是否为0。如果为0,则表示日志系统没有被关闭,可以记录日志信息。否则,不记录日志信息。
    
    使用宏定义来记录日志信息有几个好处。首先,它可以简化代码,使得记录日志信息更加方便。例如,使用上面的宏定义,我们可以这样记录一条调试信息:
    
    LOG_DEBUG("This is a debug message: %d", 123);
    其次,宏定义可以提高性能。在上面的宏定义中,我们首先检查了m_close_log变量的值,如果日志系统已经被关闭,则不会调用write_log()方法。这样可以避免不必要的函数调用,从而提高性能。
    
    通常,在需要频繁记录日志信息的地方使用宏定义来简化代码和提高性能。
    
  2. 可变形参的宏

    va_list valst; //申请参数列表变量 -- 形参就是日志消息的内容    
     va_start(valst,format); // format之后的参数都会放在valst列表中  
     va_end(valst); //清空参数列表, 并置参数指针arg_ptr无效.
     /*
     string log_str;  
         1. 简介
                 stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。
    
                 可变参数的函数通在参数列表的末尾是使用省略号(,...)定义的。
    
         2.库变量
                 下面是头文件 stdarg.h 中定义的变量类型:
    
                 序号	变量& 描述
                 1	    va_list 这是一个适用于 va_start()、va_arg() 和 va_end() 这三个宏存储信息的类型。
         3. 库宏
                 下面是头文件 stdarg.h 中定义的宏:
                 序号	宏 & 描述
                 1	void va_start(va_list ap, last_arg)
                     这个宏初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
                 2	type va_arg(va_list ap, type)
                     这个宏检索函数参数列表中类型为 type 的下一个参数。
                 3	void va_end(va_list ap)
                     这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。
     */
    
    
  3. 时间有关的函数

    头文件:#include<time.h> #include<sys/time.h>
    
    time_t t = time(NULL); 
    //t 是个从1970-01-01 00:00:00 UTC开始算起的时间变量,它是以秒为单位的,time函数以 time_t 对象返回当前日历时间。
    struct tm*sys_tm = localtime(&t); 
    //     使用 t的值,来填充 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 到 11                */
    //     int tm_year;        /* 自 1900 起的年数                */
    //     int tm_wday;        /* 一周中的第几天,范围从 0 到 6                */
    //     int tm_yday;        /* 一年中的第几天,范围从 0 到 365                    */
    //     int tm_isdst;       /* 夏令时                        */    
    //     };
    //      该函数返回指向 tm 结构的指针,该结构带有被填充的时间信息。
    
    struct tm my_tm = *sys_tm;
    
    timeval 和 timespec有啥区别?gettimeofday函数又是啥?
        1. 精确级别不同:timeval 精确度为微秒, timespec 精确度为纳秒 
        2. 1s = 1000ms(毫秒) = 1000*1000us(微秒) = 1000*1000*1000ns(纳秒)
        3. 一般由函数int gettimeofday(struct timeval *tv, struct timezone *tz)获取系统的时间 
        1. gettimeofday()会把目前的时间用tv结构体返回,当地时区的信息则放到tz所指的结构中
        2. 参数:
            //结构体
            struct timeval{
    
            long tv_sec; //秒
    
            long tv_usec;//微秒
    
            };
    
            struct timezone{
    
            int tz_minuteswest;//和greenwich时间查了多少分钟
    
            int tz_dsttime; //DST 时间的修正方式
    
            }
            //在gettimeofday()函数中的俩个参数都可以为NULL,为NULL时对应的结构体将不返回值。返回值 0 成功;-1失败,原因存于errno; 
    
  4. 复制字符串有关的函数

        int vsnprintf (char * s, size_t n, const char * format, va_list arg );
    
        描述:
    
        将格式化数据从可变参数列表写入大小缓冲区
        如果在printf上使用格式,则使用相同的文本组成字符串,但使用由arg标识的变量参数列表中的元素而不是附加的函数参数,
    
        并将结果内容作为C字符串存储在s指向的缓冲区中 (以n为最大缓冲区容量来填充)。
    
        如果结果字符串的长度超过了n-1个字符,则剩余的字符将被丢弃并且不被存储,而是被计算为函数返回的值。
        在内部,函数从arg标识的列表中检索参数,就好像va_arg被使用了一样,因此arg的状态很可能被调用所改变。
        在任何情况下,arg都应该在调用之前的某个时刻由va_start初始化,并且在调用之后的某个时刻,预计会由va_end释放。
    
        参数:
    
        s
    
        指向存储结果C字符串的缓冲区的指针。
        缓冲区应至少有n个字符的大小。
    
        n
    
        在缓冲区中使用的最大字节数。
        生成的字符串的长度至多为n-1,为额外的终止空字符留下空间。
        size_t是一个无符号整数类型。
    
        format
    
        包含格式字符串的C字符串,其格式字符串与printf中的格式相同
    
        arg
    
        标识使用va_start初始化的变量参数列表的值。
        va_list是在<cstdarg>中定义的特殊类型。
    
        返回值:
    
        如果n足够大,则会写入的字符数,不包括终止空字符。
        如果发生编码错误,则返回负数。
        注意,只有当这个返回值是非负值且小于n时,字符串才被完全写入。
    
     1. 描述:C 库函数 int snprintf(char *str, size_t size, const char *format, ...) 
                设将可变参数(...)按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,
                超过 size 会被截断,最多写入 size-1 个字符。
                与 sprintf() 函数不同的是,snprintf() 函数提供了一个参数 size,可以防止缓冲区溢出。
                如果格式化后的字符串长度超过了 size-1,则 snprintf() 只会写入 size-1 个字符,
                并在字符串的末尾添加一个空字符(\0)以表示字符串的结束。
        2. 参数:
                str -- 目标字符串,用于存储格式化后的字符串的字符数组的指针。
                size -- 字符数组的大小。
                format -- 格式化字符串。
                ... -- 可变参数,可变数量的参数根据 format 中的格式化指令进行格式化。
        3. 返回值:
                snprintf() 函数的返回值是输出到 str 缓冲区中的字符数,不包括字符串结尾的空字符 \0。
                如果 snprintf() 输出的字符数超过了 size 参数指定的缓冲区大小,则输出的结果会被截断,
                只有 size - 1 个字符被写入缓冲区,最后一个字符为字符串结尾的空字符 \0。
                需要注意的是,snprintf() 函数返回的字符数并不包括字符串结尾的空字符 \0,
                因此如果需要将输出结果作为一个字符串使用,则需要在缓冲区的末尾添加一个空字符 \0。
    
     1. 描述:C 库函数 char *strncpy(char *dest, const char *src, size_t n) 
             把 src 所指向的字符串复制到 dest,最多复制 n 个字符。
             当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。
     2. 参数:
             dest -- 指向用于存储复制内容的目标数组。
             src -- 要复制的字符串。
             n -- 要从源中复制的字符数。
     3. 返回值
             该函数返回最终复制的字符串。
     
     p - file_name + 1表示从file_name字符串的开头到最后一个出现的'/'字符之间的字符数量(包括'/'字符)。
     因此,这段代码会将file_name字符串中从开头到最后一个出现的'/'字符之间的部分复制到dir_name字符串中。
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Geminfit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值