fopen+计数器无法持久化,因缺乏原子性更新与崩溃容错:先读再写易因中断丢失数据,无锁并发会覆盖,纯文本无校验;推荐原子重命名+json方案。

为什么 fopen + 计数器无法持久化?
直接在程序里用全局变量或静态变量累加访问次数,进程退出就清零。哪怕用 std::ofstream 每次打开写一次,也容易因异常、崩溃或并发导致计数丢失或错乱。持久化核心是「原子性更新」和「落地可靠」,不是“写进去就行”。
- 单次写入不加锁 → 多进程同时访问同一文件会覆盖彼此的计数
- 先读再写再保存 → 中间崩溃(如断电)会导致旧值丢失、新值未写入
- 用普通文本格式存数字 → 没有校验,文件被手动改写后程序无法识别
推荐方案:用原子重命名 + JSON 格式存储
Linux/macOS 下利用 rename() 的原子性,Windows 下用 MoveFileEx 配合 MOVEFILE_REPLACE_EXISTING。JSON 便于人工检查、支持扩展字段(如最后访问时间、IP等),且 nlohmann/json 库解析轻量、无依赖。
- 每次访问前,读取当前
stats.json(若不存在则初始化为{"count":0,"updated":"..."}) - 内存中递增
count,更新updated字段为当前时间戳 - 将新内容写入临时文件
stats.json.tmp - 调用
rename("stats.json.tmp", "stats.json")—— 这步是原子的,失败则保留原文件
示例关键逻辑(C++20,需链接 -lnlohmann_json):
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
#include <nlohmann>
#include <fstream>
#include <filesystem>
int update_access_count(const std::string& path) {
nlohmann::json j;
std::ifstream f(path);
if (f.good()) j = nlohmann::json::parse(f, nullptr, false);
if (j.is_discarded()) j = {{"count", 0}};
j["count"] = j.value("count", 0) + 1;
j["updated"] = std::time(nullptr);
std::string tmp_path = path + ".tmp";
std::ofstream out(tmp_path);
out ();
}
</filesystem></fstream></nlohmann>
Windows 下 CreateFile 的共享模式陷阱
如果用 CreateFile 手动控制文件句柄,必须显式设置 dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE,否则其他进程(包括自己下次启动)会因“文件正被另一进程使用”而打不开 —— 这会导致统计中断,而非并发冲突。
- 只设
FILE_SHARE_READ→ 写入新统计时被拒绝 - 完全不设共享模式(默认 0)→ 第二次运行直接
INVALID_HANDLE_VALUE - 即使用了原子重命名,也要确保读取原
stats.json时允许被其他进程同时读
并发安全的底线:不要依赖 flock 或 LockFile
flock 在 NFS 或某些容器环境不可靠;LockFile 是字节范围锁,对整个文件加锁又太重,且 Windows 上跨进程 lock 可能不释放干净。真正的容错做法是:接受偶尔的计数偏差(比如两次并发访问只记成 +1),但保证「不丢数据」「不损坏格式」「不阻塞主流程」。
- 把统计更新设为 fire-and-forget:异步线程做,失败就丢弃日志,不影响业务
- 定期(如每天)用脚本校验
stats.json是否为合法 JSON,防止被篡改后程序 crash - 如果要求强一致性(如计费场景),就得换数据库,文件系统本身不提供事务语义
最常被忽略的是:没给 stats.json 设置合适的目录权限(尤其多用户环境),导致非属主进程无法写入 —— 这比并发问题更早出现,且错误静默。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










