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函数需要参数,当前元素的下一个一定会有值吗?如果是空的怎么办?
- 首先,需要明确一点,这个阻塞队列是用来坐什么的,这个阻塞队列是用来暂时存放异步日志的数据的,也就是后期需要从这里读取数据,写进日志系统中
- 其次,pop函数的功能是,从当前阻塞队列中获取一个元素(比如字符串类型的数据),把这个元素的值取出来,复制到日志文件中
- 最后,正是pop函数不止普通的"弹出一个元素"的功能,他还需要取出(复制)这个元素的值,所以需要一个参数,接受这个值。
- 如果没有数据,就会返回false,证明,这时候的阻塞队列中并没有数据,也就是,异步日志的数据已经更新完毕了(全都写进了日志文件中)
为什么获取队列大小的函数中需要加锁,什么情况下会造成它的线程不安全?直接返回不行吗?
- 不可以直接返回,会造成线程不安全
- 考虑这种情况:
- 如果不加锁,线程A需要返回变量m_size, 而线程B正在修改m_size, 那么,线程A获得的值就无法保证其唯一性了,可能是B修改前的,也可能是B修改后的。
- 如果加了锁,线程A需要返回变量m_size, 而线程B正在修改m_size,那么,由于A正在访问(在临界区中),B不可以进入修改,从而保证了,A获得的数据是B修改前的。
增加一个超时处理的pop函数有什么作用?里面的实现逻辑是怎样的?
- 超时处理和不超时处理的主要区别在于:当遇到没有数据可读是,什么时候返回?
- 首先确定的是,如果没有数据可读,会尝试阻塞等待,这是 条件变量的一个特点
- 其次,如果阻塞失败,会立即返回false;
- 最后,如果阻塞成功,超时处理会阻塞一定的时间,然后如果还没有数据,就会返回false;而不超时处理的话,就会一直等待;
类的公有成员和私有成员有什么区别
- 公有成员:类内和类外都可访问。
- 私有成员:类内可访问,类外不可访问。友元可访问。什么是友元,下面会介绍。
学习一些没有遇到过的库函数
-
函数的宏定义:
#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()方法。这样可以避免不必要的函数调用,从而提高性能。 通常,在需要频繁记录日志信息的地方使用宏定义来简化代码和提高性能。
-
可变形参的宏
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,则结果为未定义。 */
-
时间有关的函数
头文件:#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;
-
复制字符串有关的函数
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字符串中。