参考学习:从零开始实现 C++ TinyWebServer 异步日志系统 Log类详解_tiny webserver-CSDN博客
本文用于强化学习记忆,原文讲解很清晰。
log.cpp:
构造函数:
Log::Log():is_async_(false), today_(0),line_count_(0),fp_(nullptr), deque_(nullptr), write_thread_(nullptr){}
析构函数:
Log::~Log(){ while(!deque_->empty())deque_->flush();//唤醒消费者处理剩下数据 deque_->close(); write_thread_->join(); //等待线程退出 if(fp_){ std::lock_guard<std::mutex> locker(mtx_); Flush(); fclose(fp_); } }
初始化函数Init():
1 //初始化 2 void Log::Init(int level, const char* path, const char* suffix, int max_capacity){ 3 is_open_ = true;// 标记日志模块已开启(后续可通过IsOpen()判断) 4 level_ = level;// 设置日志等级(0=DEBUG,1=INFO,...,4=FATAL) 5 path_ = path; // 记录日志文件存放路径(如"./log") 6 suffix_ = suffix;// 记录日志文件后缀(如".log") 7 8 if(max_capacity){ // 若max_capacity>0,启用异步模式(需创建队列和线程) 9 is_async_= true; // 标记为异步模式 10 if(!deque_){ // 如果阻塞队列还未创建(首次初始化) 11 // 1. 创建阻塞队列(用于缓存待写日志,生产者-消费者模型) 12 std::unique_ptr<BlockQueue<string>> new_deque(new BlockQueue<string>); 13 deque_ = std::move(new_deque); //所有权转移 // 用移动语义转移队列所有权(避免拷贝) 14 15 // 2. 创建异步写日志线程(线程函数为FlushLogThread) 16 std::unique_ptr<thread> new_thread(new thread(FlushLogThread)); 17 write_thread_ = std::move(new_thread); // 转移线程所有权 18 } 19 }else { 20 // 若max_capacity=0,启用同步模式(直接写文件,无需队列和线程) 21 is_async_ = false; 22 } 23 line_count_ = 0; // 重置当前日志文件的行数计数器(从0开始) 24 path_ = path; // 冗余:重复设置path_(和前面第3行重复,可删除) 25 suffix_ = suffix; // 冗余:重复设置suffix_(和前面第4行重复,可删除) 26 time_t timer = time(nullptr); // 获取当前时间(秒级,从1970年开始计算) 27 struct tm* sys_time = localtime(&timer); // 转换为本地时间(年/月/日/时/分/秒) 28 struct tm t = *sys_time; // 拷贝一份本地时间(避免后续操作修改原结构体) 29 char filename[Log_NAME_LENGTH] = {0}; // 定义文件名数组(长度为Log_NAME_LENGTH=256),初始化为0 30 // 格式化文件名:路径+年_月_日+后缀(例如:"./log2024_07_26.log") 31 snprintf(filename, Log_NAME_LENGTH - 1, "%s%04d_%02d_%02d%s", path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_); 32 today_ = t.tm_mday; // 记录当前日期的“日”(如26号),用于后续判断是否跨天(需切分文件) 33 34 { // 作用域:限制lock_guard的生命周期(自动加锁/解锁) 35 std::lock_guard<std::mutex> locker(mtx_); // 加锁,确保文件操作线程安全(多线程下不冲突) 36 if(fp_){ // 如果之前已经打开了日志文件 37 Flush(); // 先刷新缓存(将内存中的数据写入磁盘) 38 fclose(fp_); // 关闭旧文件 39 } 40 fp_ = fopen(filename, "a"); // 以“追加模式”打开新日志文件(新内容加在文件末尾) 41 if(fp_ == nullptr){ // 如果文件打开失败(可能是路径不存在) 42 mkdir(path_, 0777); //777最大权限 // 创建日志目录(0777表示最大权限) 43 fp_ = fopen(filename, "a"); // 再次尝试打开文件 44 } 45 assert(fp_ != nullptr); // 断言:如果文件仍未打开,则程序终止(避免后续操作出错) 46 } 47 }
这里的std::unique_ptr表示智能指针类型,是C++11引入的独占所有权智能指针,它的核心作用是:自动管理动态资源的生命周期,再执政超出作用域时自动调用delete释放内存,避免手动new/delete导致的内存泄露(比如忘记delete,或在异常场景下delete未执行)。
这里说涉及到程序中堆和栈的问题,在栈上创建对象只要出了作用域就会自动销毁,在堆上创建对象需要手动进行销毁。
这两的区分方式主要是看作用域,创建方式,生命周期;一般栈上的创建时直接声名,不需要使用new、malloc等动态分配关键字。堆则是使用new这类的关键字。栈上创建的生命周期严格绑定作用域,出了作用域就自动销毁了。
根据日志等级,向日志缓冲区添加对应的等级前缀AppendLogLevel
1 void Log::AppendLogLevel(int level){ 2 //定义一个存储 “日志等级前缀字符串” 的数组,每个元素对应一个等级的标识。 3 const char* level_title[] = { "[DEBUG]:","[INFO]:", "[WARN]:", "[ERROR]:", "[FATAL]:"}; 4 //校验并修正日志等级 5 int valid_level = (level >= 0 && level <=4 ) ? level : 1; 6 //将等级前缀添加到缓冲区 7 buff_.Append(level_title[valid_level], 9); 8 }
日志核心写入函数Write:
1 void Log::Write(int level, const char* format, ...){ 2 struct timeval now = {0, 0}; // 1. 初始化时间结构体(包含秒和微秒) 3 gettimeofday(&now, nullptr); // 2. 获取当前时间(精确到微秒),存到now中 4 time_t time_second = now.tv_sec; // 3. 提取秒级时间(从1970年开始的秒数) 5 struct tm* sys_time = localtime(&time_second); // 4. 将秒级时间转换为本地时间(年/月/日/时/分/秒) 6 struct tm t = *sys_time; // 5. 拷贝本地时间到结构体t(避免sys_time被其他线程修改,确保线程安全) 7 8 //日期不对或行数满了 9 // 6. 判断是否需要切换文件:① 日期变化(跨天);② 行数达到上限(当前文件满了) 10 if(today_ != t.tm_mday || (line_count_ && (line_count_ % MAX_LINES == 0))){ 11 // 7. 加锁后立即解锁:暂时占用锁防止其他线程同时修改文件,随后释放减少阻塞 12 std::unique_lock<std::mutex> locker(mtx_); 13 locker.unlock(); 14 15 // 8. 定义新文件名和日期后缀的缓冲区 16 char new_file[Log_NAME_LENGTH]; // 新日志文件名(长度由Log_NAME_LENGTH宏定义,如256) 17 char tail[36]; // 日期后缀(如"2024_07_26") 18 19 // 9. 格式化日期后缀为"年_月_日"(如2024_07_26) 20 snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday); 21 22 // 10. 分支1:日期变化(跨天,如从25日到26日) 23 if(today_ != t.tm_mday) 24 { 25 // 11. 新文件名格式:路径+日期+后缀(如"./log/2024_07_26.log") 26 snprintf(new_file, Log_NAME_LENGTH - 72, "%s%s%s", path_, tail, suffix_); 27 today_ = t.tm_mday; // 更新当前日期为新的"日"(如26) 28 }else{ 29 // 12. 分支2:行数满了(当前文件达到MAX_LINES行) 30 int num = line_count_ / MAX_LINES; // 计算序号(第几个文件,如1、2、3...) 31 // 13. 新文件名格式:路径+日期-序号+后缀(如"./log/2024_07_26-1.log") 32 snprintf(new_file, Log_NAME_LENGTH, "%s%s-%d%s", path_, tail, num, suffix_); 33 } 34 35 // 14. 加锁处理文件切换(确保线程安全) 36 { 37 std::lock_guard<std::mutex> locker(mtx_); // 加锁,防止多线程同时操作文件指针 38 Flush(); // 15. 刷新旧文件的缓存(将内存数据写入磁盘) 39 fclose(fp_); // 16. 关闭旧文件 40 fp_ = fopen(new_file, "a"); // 17. 以追加模式打开新文件 41 assert(fp_ != nullptr); // 18. 断言:确保文件打开成功(失败则程序终止) 42 } 43 } 44 // 19. 加锁:确保日志内容格式化和写入的原子性(线程安全) 45 { 46 std::lock_guard<std::mutex> locker(mtx_); 47 line_count_++; // 20. 当前文件行数+1(用于判断是否达到切换阈值) 48 49 // 21. 格式化时间戳到缓冲区:"年-月-日 时:分:秒.微秒"(如2024-07-26 15:30:20.123456) 50 int n = snprintf(buff_.WriteBegin(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld", 51 t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, // 年(+1900)、月(+1)、日 52 t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec); // 时、分、秒、微秒 53 buff_.HasWritten(n); // 22. 更新缓冲区已写入的长度(n字节) 54 AppendLogLevel(level); // 23. 向缓冲区添加日志等级前缀(如[DEBUG]:,调用之前分析的函数) 55 56 // 24. 处理用户传入的可变参数(如LOG_DEBUG("fd=%d", fd)中的"fd=%d"和fd) 57 va_list vaList; // 定义可变参数列表 58 va_start(vaList, format); // 初始化参数列表(绑定到format后面的参数) 59 // 25. 将格式化的日志内容写入缓冲区(如"fd=5") 60 int m = vsnprintf(buff_.WriteBegin(), buff_.WritableBytes(), format, vaList); 61 va_end(vaList); // 26. 结束可变参数处理(释放资源) 62 buff_.HasWritten(m); // 27. 更新缓冲区已写入的长度(m字节) 63 buff_.Append("\n\0", 2); // 28. 向缓冲区添加换行符和字符串终止符(每个日志占一行) 64 65 // 29. 根据模式写入:异步模式放入队列,同步模式直接写入文件 66 if( is_async_ && deque_) 67 deque_->push_back(buff_.RetrieveAllAsString()); // 异步:缓冲区内容转字符串入队 68 else 69 fputs(buff_.ReadBegin(), fp_); // 同步:直接将缓冲区内容写入当前文件 70 buff_.RetrieveAll(); // 30. 清空缓冲区,准备下次写入 71 } 72 73 }
GetInstance单例模式
1 //单例模式之饿汉模式 2 // 定义一个静态成员函数GetInstance,返回Log类的指针 3 Log* Log::GetInstance(){ 4 //静态局部变量的初始化是线程安全的 5 // 定义一个静态局部变量log:属于Log类的唯一实例 6 static Log log; 7 // 返回这个唯一实例的地址 8 return &log; 9 }
异步日志的写线程函数FlushLogThread
1 //异步日志的写线程函数 2 void Log::FlushLogThread(){ 3 Log::GetInstance()->AsyncWrite(); // 核心逻辑:调用单例的AsyncWrite方法 4 }
//写线程真正的执行函数 void Log::AsyncWrite(){ string str = ""; // 用于临时存储从队列中取出的单条日志内容 while (deque_->pop(str)){ // 循环条件:从队列中成功取出日志(队列未关闭且有数据) //异步模式-消费者 std::lock_guard<std::mutex> locker(mtx_); // 加锁,确保文件操作线程安全 fputs(str.c_str(), fp_); // 将日志内容写入当前打开的文件 } }
剩下一些函数很容易就看懂
1 //唤醒消费者,开始写日志 2 void Log::Flush(){ 3 if(is_async_){ // 判断当前是否为异步日志模式 4 deque_->flush(); // 唤醒阻塞队列中的消费者,强制处理剩余日志 5 } 6 fflush(fp_); // 刷新文件指针fp_对应的流缓冲区,将内存数据写入磁盘 7 } 8 9 int Log::GetLevel(){ 10 std::lock_guard<std::mutex> lock(mtx_); 11 return level_; 12 } 13 14 void Log::SetLevel(int level){ 15 std::lock_guard<std::mutex> lock(mtx_); 16 level_ = level; 17 } 18 19 bool Log::IsOpen(){ 20 return is_open_; 21 }
log.cpp
1 #include "Log/log.h" 2 3 Log::Log():is_async_(false), today_(0),line_count_(0),fp_(nullptr), 4 deque_(nullptr), write_thread_(nullptr){} 5 6 Log::~Log(){ 7 while(!deque_->empty())deque_->flush();//唤醒消费者处理剩下数据 8 deque_->close(); 9 write_thread_->join(); //等待线程退出 10 if(fp_){ 11 std::lock_guard<std::mutex> locker(mtx_); 12 Flush(); 13 fclose(fp_); 14 } 15 } 16 17 //初始化 18 void Log::Init(int level, const char* path, const char* suffix, int max_capacity){ 19 is_open_ = true;// 标记日志模块已开启(后续可通过IsOpen()判断) 20 level_ = level;// 设置日志等级(0=DEBUG,1=INFO,...,4=FATAL) 21 path_ = path; // 记录日志文件存放路径(如"./log") 22 suffix_ = suffix;// 记录日志文件后缀(如".log") 23 24 if(max_capacity){ // 若max_capacity>0,启用异步模式(需创建队列和线程) 25 is_async_= true; // 标记为异步模式 26 if(!deque_){ // 如果阻塞队列还未创建(首次初始化) 27 // 1. 创建阻塞队列(用于缓存待写日志,生产者-消费者模型) 28 std::unique_ptr<BlockQueue<string>> new_deque(new BlockQueue<string>); 29 deque_ = std::move(new_deque); //所有权转移 // 用移动语义转移队列所有权(避免拷贝) 30 31 // 2. 创建异步写日志线程(线程函数为FlushLogThread) 32 std::unique_ptr<thread> new_thread(new thread(FlushLogThread)); 33 write_thread_ = std::move(new_thread); // 转移线程所有权 34 } 35 }else { 36 // 若max_capacity=0,启用同步模式(直接写文件,无需队列和线程) 37 is_async_ = false; 38 } 39 line_count_ = 0; // 重置当前日志文件的行数计数器(从0开始) 40 path_ = path; // 冗余:重复设置path_(和前面第3行重复,可删除) 41 suffix_ = suffix; // 冗余:重复设置suffix_(和前面第4行重复,可删除) 42 time_t timer = time(nullptr); // 获取当前时间(秒级,从1970年开始计算) 43 struct tm* sys_time = localtime(&timer); // 转换为本地时间(年/月/日/时/分/秒) 44 struct tm t = *sys_time; // 拷贝一份本地时间(避免后续操作修改原结构体) 45 char filename[Log_NAME_LENGTH] = {0}; // 定义文件名数组(长度为Log_NAME_LENGTH=256),初始化为0 46 // 格式化文件名:路径+年_月_日+后缀(例如:"./log2024_07_26.log") 47 snprintf(filename, Log_NAME_LENGTH - 1, "%s%04d_%02d_%02d%s", path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_); 48 today_ = t.tm_mday; // 记录当前日期的“日”(如26号),用于后续判断是否跨天(需切分文件) 49 50 { // 作用域:限制lock_guard的生命周期(自动加锁/解锁) 51 std::lock_guard<std::mutex> locker(mtx_); // 加锁,确保文件操作线程安全(多线程下不冲突) 52 if(fp_){ // 如果之前已经打开了日志文件 53 Flush(); // 先刷新缓存(将内存中的数据写入磁盘) 54 fclose(fp_); // 关闭旧文件 55 } 56 fp_ = fopen(filename, "a"); // 以“追加模式”打开新日志文件(新内容加在文件末尾) 57 if(fp_ == nullptr){ // 如果文件打开失败(可能是路径不存在) 58 mkdir(path_, 0777); //777最大权限 // 创建日志目录(0777表示最大权限) 59 fp_ = fopen(filename, "a"); // 再次尝试打开文件 60 } 61 assert(fp_ != nullptr); // 断言:如果文件仍未打开,则程序终止(避免后续操作出错) 62 } 63 } 64 65 66 void Log::AppendLogLevel(int level){ 67 //定义一个存储 “日志等级前缀字符串” 的数组,每个元素对应一个等级的标识。 68 const char* level_title[] = { "[DEBUG]:","[INFO]:", "[WARN]:", "[ERROR]:", "[FATAL]:"}; 69 //校验并修正日志等级 70 int valid_level = (level >= 0 && level <=4 ) ? level : 1; 71 //将等级前缀添加到缓冲区 72 buff_.Append(level_title[valid_level], 9); 73 } 74 75 void Log::Write(int level, const char* format, ...){ 76 struct timeval now = {0, 0}; // 1. 初始化时间结构体(包含秒和微秒) 77 gettimeofday(&now, nullptr); // 2. 获取当前时间(精确到微秒),存到now中 78 time_t time_second = now.tv_sec; // 3. 提取秒级时间(从1970年开始的秒数) 79 struct tm* sys_time = localtime(&time_second); // 4. 将秒级时间转换为本地时间(年/月/日/时/分/秒) 80 struct tm t = *sys_time; // 5. 拷贝本地时间到结构体t(避免sys_time被其他线程修改,确保线程安全) 81 82 //日期不对或行数满了 83 // 6. 判断是否需要切换文件:① 日期变化(跨天);② 行数达到上限(当前文件满了) 84 if(today_ != t.tm_mday || (line_count_ && (line_count_ % MAX_LINES == 0))){ 85 // 7. 加锁后立即解锁:暂时占用锁防止其他线程同时修改文件,随后释放减少阻塞 86 std::unique_lock<std::mutex> locker(mtx_); 87 locker.unlock(); 88 89 // 8. 定义新文件名和日期后缀的缓冲区 90 char new_file[Log_NAME_LENGTH]; // 新日志文件名(长度由Log_NAME_LENGTH宏定义,如256) 91 char tail[36]; // 日期后缀(如"2024_07_26") 92 93 // 9. 格式化日期后缀为"年_月_日"(如2024_07_26) 94 snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday); 95 96 // 10. 分支1:日期变化(跨天,如从25日到26日) 97 if(today_ != t.tm_mday) 98 { 99 // 11. 新文件名格式:路径+日期+后缀(如"./log/2024_07_26.log") 100 snprintf(new_file, Log_NAME_LENGTH - 72, "%s%s%s", path_, tail, suffix_); 101 today_ = t.tm_mday; // 更新当前日期为新的"日"(如26) 102 }else{ 103 // 12. 分支2:行数满了(当前文件达到MAX_LINES行) 104 int num = line_count_ / MAX_LINES; // 计算序号(第几个文件,如1、2、3...) 105 // 13. 新文件名格式:路径+日期-序号+后缀(如"./log/2024_07_26-1.log") 106 snprintf(new_file, Log_NAME_LENGTH, "%s%s-%d%s", path_, tail, num, suffix_); 107 } 108 109 // 14. 加锁处理文件切换(确保线程安全) 110 { 111 std::lock_guard<std::mutex> locker(mtx_); // 加锁,防止多线程同时操作文件指针 112 Flush(); // 15. 刷新旧文件的缓存(将内存数据写入磁盘) 113 fclose(fp_); // 16. 关闭旧文件 114 fp_ = fopen(new_file, "a"); // 17. 以追加模式打开新文件 115 assert(fp_ != nullptr); // 18. 断言:确保文件打开成功(失败则程序终止) 116 } 117 } 118 // 19. 加锁:确保日志内容格式化和写入的原子性(线程安全) 119 { 120 std::lock_guard<std::mutex> locker(mtx_); 121 line_count_++; // 20. 当前文件行数+1(用于判断是否达到切换阈值) 122 123 // 21. 格式化时间戳到缓冲区:"年-月-日 时:分:秒.微秒"(如2024-07-26 15:30:20.123456) 124 int n = snprintf(buff_.WriteBegin(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld", 125 t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, // 年(+1900)、月(+1)、日 126 t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec); // 时、分、秒、微秒 127 buff_.HasWritten(n); // 22. 更新缓冲区已写入的长度(n字节) 128 AppendLogLevel(level); // 23. 向缓冲区添加日志等级前缀(如[DEBUG]:,调用之前分析的函数) 129 130 // 24. 处理用户传入的可变参数(如LOG_DEBUG("fd=%d", fd)中的"fd=%d"和fd) 131 va_list vaList; // 定义可变参数列表 132 va_start(vaList, format); // 初始化参数列表(绑定到format后面的参数) 133 // 25. 将格式化的日志内容写入缓冲区(如"fd=5") 134 int m = vsnprintf(buff_.WriteBegin(), buff_.WritableBytes(), format, vaList); 135 va_end(vaList); // 26. 结束可变参数处理(释放资源) 136 buff_.HasWritten(m); // 27. 更新缓冲区已写入的长度(m字节) 137 buff_.Append("\n\0", 2); // 28. 向缓冲区添加换行符和字符串终止符(每个日志占一行) 138 139 // 29. 根据模式写入:异步模式放入队列,同步模式直接写入文件 140 if( is_async_ && deque_) 141 deque_->push_back(buff_.RetrieveAllAsString()); // 异步:缓冲区内容转字符串入队 142 else 143 fputs(buff_.ReadBegin(), fp_); // 同步:直接将缓冲区内容写入当前文件 144 buff_.RetrieveAll(); // 30. 清空缓冲区,准备下次写入 145 } 146 147 } 148 149 150 //单例模式之饿汉模式 151 // 定义一个静态成员函数GetInstance,返回Log类的指针 152 Log* Log::GetInstance(){ 153 //静态局部变量的初始化是线程安全的 154 // 定义一个静态局部变量log:属于Log类的唯一实例 155 static Log log; 156 // 返回这个唯一实例的地址 157 return &log; 158 } 159 160 161 162 //异步日志的写线程函数 163 void Log::FlushLogThread(){ 164 Log::GetInstance()->AsyncWrite(); // 核心逻辑:调用单例的AsyncWrite方法 165 } 166 167 //写线程真正的执行函数 168 void Log::AsyncWrite(){ 169 string str = ""; // 用于临时存储从队列中取出的单条日志内容 170 while (deque_->pop(str)){ // 循环条件:从队列中成功取出日志(队列未关闭且有数据) 171 //异步模式-消费者 172 std::lock_guard<std::mutex> locker(mtx_); // 加锁,确保文件操作线程安全 173 fputs(str.c_str(), fp_); // 将日志内容写入当前打开的文件 174 } 175 } 176 177 //唤醒消费者,开始写日志 178 void Log::Flush(){ 179 if(is_async_){ // 判断当前是否为异步日志模式 180 deque_->flush(); // 唤醒阻塞队列中的消费者,强制处理剩余日志 181 } 182 fflush(fp_); // 刷新文件指针fp_对应的流缓冲区,将内存数据写入磁盘 183 } 184 185 int Log::GetLevel(){ 186 std::lock_guard<std::mutex> lock(mtx_); 187 return level_; 188 } 189 190 void Log::SetLevel(int level){ 191 std::lock_guard<std::mutex> lock(mtx_); 192 level_ = level; 193 } 194 195 bool Log::IsOpen(){ 196 return is_open_; 197 }
还有对于log.cpp的测试源文件,这里不详细剖析了:
1 #include "Log/log.h" 2 #include <iostream> 3 4 int main(){ 5 //获取Log类的单例实例 6 Log* logger = Log::GetInstance(); 7 8 //初始化日志系统 9 logger->Init(0, "./logs/", ".log", 1024); 10 11 //输出不同级别的日志信息 12 LOG_DEBUG("This is a debug message."); 13 LOG_INFO("This is an info message."); 14 LOG_WARN("This is a warning message."); 15 LOG_ERROR("This is an error message."); 16 LOG_FATAL("This is an fatal message."); 17 18 //输出日志级别 19 std::cout << "Current log level: " << logger->GetLevel() << std::endl; 20 21 //修改日志级别 22 logger->SetLevel(2); 23 std::cout << "New log level: " << logger->GetLevel() << std::endl; 24 25 //再次输出不同级别的日志信息 26 LOG_DEBUG("This debug message should not be logged."); 27 LOG_INFO("This info message should not be logged."); 28 LOG_WARN("This is a new warning message."); 29 LOG_ERROR("This is a new error message."); 30 LOG_FATAL("This is a new fatal message."); 31 32 //可变参数 33 logger->SetLevel(0); 34 LOG_DEBUG("%s %d %d", "log info", 123, 666); 35 36 //大量日志 37 for(int i = 0; i < 100000; ++i) 38 LOG_DEBUG("This is the %d-th errpr message.", i); 39 40 return 0; 41 }