在C++项目里,把运行状态、异常信息以及业务关键节点记录下来,是保障系统可维护性的基础手段。文件I/O提供了一种简单且通用的持久化方式,开发者无需引入第三方服务即可在本地留存线索。不同于控制台输出,文件日志能够跨会话保存,适合长期运行的守护进程或客户端软件。

基于ofstream的基础文件日志
C++标准库中的<fstream>头文件提供了ofstream类,专门用于处理文件输出。以追加模式(std::ios::app)打开文件,可以保证每次启动程序都不会清空已有日志,新内容始终写在末尾。相比截断模式,这种写法更符合日志场景的需求。
下面的示例展示了最基础的写法:创建日志文件对象,在程序关键位置写入一行带时间戳的文字。注意这里用std::endl会同时刷新缓冲区,若日志量巨大可考虑用"n"减少频繁刷盘带来的性能损耗。
#include <fstream>
#include <iostream>
#include <chrono>
#include <iomanip>
void write_log(const std::string& msg) {
std::ofstream fout("app.log", std::ios::app);
if (!fout) {
std::cerr << "无法打开日志文件" << std::endl;
return;
}
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
fout << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S")
<< " " << msg << std::endl;
}
int main() {
write_log("程序启动");
write_log("处理用户请求");
return 0;
}
这种方式的优点是实现成本极低,不依赖任何外部库。缺点是每次调用都重新打开和关闭文件,频繁I/O时效率偏低;而且多个线程同时写同一个文件会出现内容穿插,甚至丢行。
线程安全的日志封装
当程序采用多线程架构,例如网络服务器中每个连接由一个线程处理,日志函数就必须保证同一时刻只有一个线程能写入,否则不同线程的半截信息会混在同一行。最常见的做法是借助std::mutex进行互斥保护,并将文件流提升为类成员,避免反复开关。
下面给出一个简单的线程安全日志类。构造函数打开文件,析构时自动关闭;log方法加锁后写内容。这样在多线程环境里,每条日志都是完整的一行。
#include <fstream>
#include <mutex>
#include <string>
class FileLogger {
public:
FileLogger(const std::string& path) : fout(path, std::ios::app) {}
~FileLogger() { if (fout) fout.close(); }
void log(const std::string& level, const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
fout << "[" << level << "] " << msg << "n";
fout.flush();
}
private:
std::ofstream fout;
std::mutex mtx;
};
// 使用示例
// FileLogger logger("server.log");
// logger.log("ERROR", "数据库连接失败");
引入互斥锁后,日志写入变成了串行操作,虽然牺牲了一点并发度,但换来了数据完整性。如果担心flush太频繁,可以改为按大小或时间滚动,在性能与可靠性之间取舍。
日志分级与滚动策略
实际系统中,并不是所有信息都同等重要。通常我们会划分DEBUG、INFO、WARN、ERROR等级别,通过配置屏蔽低级别噪音。文件I/O层面还可以做滚动:当单个文件超过指定尺寸,就重命名旧文件并新建,防止磁盘被撑满。
下面示例演示了带级别过滤和简单尺寸检查的封装思路。当文件大于1MB时,关闭当前流,将app.log改名为带序号的备份,再重新打开。
#include <fstream>
#include <mutex>
#include <string>
class RollingLogger {
public:
RollingLogger(const std::string& path, size_t max_bytes)
: path(path), max_bytes(max_bytes) {
fout.open(path, std::ios::app);
}
void log(const std::string& level, const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
if (fout.tellp() > max_bytes) {
fout.close();
std::rename(path.c_str(), (path + ".1").c_str());
fout.open(path, std::ios::trunc);
}
fout << level << " " << msg << "n";
}
private:
std::ofstream fout;
std::mutex mtx;
std::string path;
size_t max_bytes;
};
分级让排查更高效,滚动避免单文件无限膨胀。二者结合后,C++文件I/O日志就能支撑中小规模服务的运维需要。对于超大规模场景,可进一步接入异步队列,将落盘操作抛到后台线程,降低主逻辑延迟。
常见误区与注意事项
一个容易被忽视的问题是缓冲区丢失。若一直使用"n"且不主动flush,程序若遇到段错误或被强杀,缓冲区里积攒的日志就不会写到磁盘。因此对ERROR或FATAL级别,务必在写完后调用flush,或设置unitbuf。
另一个误区是在信号处理器里调用常规日志函数。信号上下文不允许执行非异步信号安全的标准库调用,此时写ofstream可能导致死锁或崩溃。正确做法是仅设置一个标志位,由主循环负责写盘,或者使用极低层的write系统调用。
#include <iostream>
// 错误示范:在信号处理中直接使用ofstream
// void handler(int) {
// std::ofstream f("sig.log", std::ios::app);
// f << "received signaln"; // 不安全
// }
volatile sig_atomic_t got_signal = 0;
void safe_handler(int) {
got_signal = 1; // 仅置位,主循环处理
}
只要避开上述陷阱,并依据项目规模选择合适的封装粒度,C++通过文件I/O实现日志记录既轻量又可靠,是多数原生应用的首选方案。