C++程序在开发调试和线上运行时,把日志写入文件是最常见的需求之一。标准库提供的ofstream(输出文件流)可以方便地完成这项工作,但很多人只知道最基础的打开和写入,忽略了文件打开模式、错误检测、缓冲区刷新、多线程安全等关键细节。本文将从基础用法讲起,逐步深入到一个可实际使用的日志类实现。

一、ofstream基础用法与打开模式
ofstream定义在<fstream>头文件中,它是ostream的派生类,专门用于向文件写入数据。最简单的用法是构造一个ofstream对象并传入文件名:
#include <fstream>
#include <iostream>
int main() {
std::ofstream out("app.log"); // 默认模式:截断写入
if (!out.is_open()) {
std::cerr << "文件打开失败" << std::endl;
return 1;
}
out << "程序启动" << std::endl;
out.close(); // 析构时也会自动关闭,显式close便于提前释放
return 0;
}打开模式通过第二个参数指定,常用模式可以组合使用。日志场景最关键的是std::ios::app(追加模式),如果不加这个标志,每次运行程序都会把上次的日志清空,这在排查历史问题时会造成严重后果。
| 打开模式 | 含义 | 典型用途 |
|---|---|---|
| std::ios::app | 每次写入都追加到文件末尾 | 日志文件最常用 |
| std::ios::trunc | 打开时清空文件内容 | 覆盖式输出 |
| std::ios::out | 允许写入(ofstream默认包含) | 一般无需显式指定 |
| std::ios::binary | 二进制模式,不做换行符转换 | 写二进制数据 |
| std::ios::ate | 打开后定位到文件末尾 | 先读末尾再追加 |
追加模式的写法如下,多个模式之间用按位或运算符连接:
std::ofstream log("app.log", std::ios::app | std::ios::out);
log << "[INFO] 服务已启动" << std::endl;二、错误检测与缓冲区刷新
文件写入并不总是成功的:磁盘可能满了、文件可能被占用、路径可能不存在。如果只管写入不检查状态,日志会静默丢失。可以通过is_open()、good()、fail()等成员函数检测流状态,写入后检查out.good()是一个稳妥的习惯。
另一个容易被忽视的问题是缓冲区。ofstream内部有缓冲区,写入的数据并不一定立即落盘。遇到以下情况缓冲区才会刷新:缓冲区写满、调用flush()、插入std::endl(它会插入换行并刷新)、调用close()、对象析构时。如果程序异常崩溃,缓冲区里未刷新的日志就会丢失,因此关键日志建议立即刷新。
out << "[ERROR] 数据库连接失败"; out.flush(); // 主动刷新,确保立即写入磁盘 // endl 与 "\n" 的区别 out << "带刷新的换行" << std::endl; // 换行并flush out << "普通换行\n"; // 只换行,不flush
在高频写日志的场景中,每行都用std::endl会带来大量系统调用,性能明显下降。更好的做法是统一用\n换行,只在错误级别日志或者定时器触发时调用一次flush(),在可靠性和性能之间取得平衡。
三、实现一个带时间戳的日志类
实际项目中,日志需要时间戳、日志级别、统一的调用入口。下面给出一个基于ofstream的单例日志类,支持按级别过滤,自动格式化时间:
#include <fstream>
#include <string>
#include <ctime>
enum class LogLevel { DEBUG, INFO, WARN, ERROR };
class Logger {
public:
static Logger& instance() {
static Logger logger; // C++11起线程安全的局部静态变量
return logger;
}
bool init(const std::string& filename, LogLevel minLevel) {
file_.open(filename, std::ios::app);
minLevel_ = minLevel;
return file_.is_open();
}
void log(LogLevel level, const std::string& msg) {
if (level < minLevel_ || !file_.is_open()) return;
file_ << currentTime() << " [" << levelName(level)
<< "] " << msg << "\n";
if (level == LogLevel::ERROR) file_.flush();
}
private:
Logger() = default;
std::ofstream file_;
LogLevel minLevel_ = LogLevel::INFO;
static std::string currentTime() {
char buf[64];
std::time_t t = std::time(nullptr);
std::tm tmv;
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmv);
return buf;
}
static const char* levelName(LogLevel l) {
switch (l) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
}
return "UNKNOWN";
}
};
// 使用示例
int main() {
Logger::instance().init("server.log", LogLevel::DEBUG);
Logger::instance().log(LogLevel::INFO, "服务启动成功");
Logger::instance().log(LogLevel::ERROR, "端口绑定失败");
return 0;
}这个类有几个设计要点:单例模式保证全局只有一个文件流;错误级别日志立即刷新保证关键信息不丢失;时间格式化统一放在currentTime()中,跨平台处理了Windows和Linux的线程安全时间函数差异。
四、多线程写入与日志滚动
多线程环境下,多个线程同时向同一个ofstream写入会导致日志交错混乱。标准本身不保证流操作的原子性,解决办法是用互斥锁把写入操作保护起来:
#include <mutex>
class ThreadSafeLogger {
public:
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx_);
file_ << msg << "\n";
}
private:
std::ofstream file_;
std::mutex mtx_;
};如果追求更高性能,可以采用前端后端架构:各线程把日志写入自己的内存缓冲队列,由单独的后台线程负责统一写文件,这样加锁范围只限于队列操作,吞吐量可以提升数倍。成熟的日志库如spdlog本质上就是这个思路。
最后还要考虑日志文件的滚动问题。单文件无限增长会占满磁盘,常见策略有两种:按大小滚动,文件超过指定大小(比如10MB)就关闭当前文件,改名归档后新建;按日期滚动,每天生成一个新的日志文件。按日期滚动实现最简单,只需在写日志前比较当前日期与打开文件时的日期,不一致就重新打开新文件。掌握这些要点后,用ofstream构建一个稳定高效的日志模块就不再是难事。
C++ ofstream日志写入文件流修改时间:2026-08-31 10:10:56