2、QFile高级操作:读写文本文件、二进制文件、临时文件与文件锁

文件操作是Qt开发里躲不开的活。说实话,很多人用了好几年QFile,还只停留在open、read、write三板斧。遇到大文件、二进制结构、多进程冲突,就开始头疼了。

这一章,我把QFile的四个核心场景拆开讲:文本读写、二进制操作、临时文件管理、文件锁机制。每个点我都会带上实际项目里的坑和习惯用法。

2.1 文本文件读写:别再用readAll()了

先说说最常见的文本文件。很多人图省事,上来就readAll()。小文件没问题,但一个几百兆的日志文件,你试试?内存直接爆炸。

避坑指南:我曾经接手过一个监控项目,日志文件每天增长到500MB。前任工程师用readAll()逐行解析,结果程序跑三天就OOM崩溃。后来改成流式读取,内存占用从500MB降到不到2MB。

正确的做法是逐行读取,或者按块读取。Qt提供了QTextStream,配合readLine(),这才是正道。

// 逐行读取大文本文件
QFile file("server.log");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    qWarning() << "打开文件失败:" << file.errorString();
    return;
}

QTextStream in(&file);
in.setCodec("UTF-8");  // 我习惯显式指定编码,避免跨平台乱码

while (!in.atEnd()) {
    QString line = in.readLine();
    // 处理每一行
    processLine(line);
}

file.close();

写入文本时,同样用QTextStream。注意一点:频繁调用flush()会影响性能,但关键数据写入后最好手动刷一次。

QFile file("output.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
    QTextStream out(&file);
    out << "时间: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n";
    out << "状态: " << statusCode << "\n";
    out.flush();  // 关键数据写入后刷盘
    file.close();
}
个人经验:文本文件的编码问题是个大坑。Windows下默认是本地编码(GBK),Linux下是UTF-8。我建议统一用setCodec("UTF-8"),省得跨平台时出幺蛾子。

2.2 二进制文件读写:结构体直接读写

二进制文件比文本文件效率高得多,尤其适合存储结构化数据。比如传感器数据、图像像素、网络包缓存等。

Qt里读写二进制文件,核心是QDataStream。它能把C++的基本类型和Qt的容器类型直接序列化到文件里。

// 写入二进制数据
QFile file("sensor_data.bin");
if (file.open(QIODevice::WriteOnly)) {
    QDataStream out(&file);
    out.setByteOrder(QDataStream::LittleEndian);  // 我习惯用小端序,兼容x86

    // 写入结构体
    quint32 timestamp = 1628000000;
    double temperature = 36.5;
    quint16 humidity = 75;

    out << timestamp << temperature << humidity;

    file.close();
}

// 读取二进制数据
if (file.open(QIODevice::ReadOnly)) {
    QDataStream in(&file);
    in.setByteOrder(QDataStream::LittleEndian);

    quint32 timestamp;
    double temperature;
    quint16 humidity;

    in >> timestamp >> temperature >> humidity;

    file.close();
}

你想想看,如果用文本方式存这些数据,还得做字符串转换,效率差一个数量级。二进制读写直接内存拷贝,快得很。

关键点:二进制文件读写时,QDataStream的版本号很重要。不同Qt版本默认版本号不同,跨版本读写会出问题。我建议显式设置版本号:out.setVersion(QDataStream::Qt_5_15);

2.3 临时文件:安全创建,自动清理

临时文件在什么场景下用?我举几个例子:程序升级时下载临时包、编辑器自动保存的备份、大数据排序时的中间缓存。

Qt提供了QTemporaryFile,比手动创建临时文件安全得多。它自动生成唯一文件名,而且对象销毁时自动删除文件。

// 创建临时文件
QTemporaryFile tempFile;
tempFile.setFileTemplate(QDir::tempPath() + "/myapp_XXXXXX.txt");
if (tempFile.open()) {
    qDebug() << "临时文件路径:" << tempFile.fileName();

    // 写入临时数据
    QTextStream out(&tempFile);
    out << "这是临时数据";

    // 文件路径可以传给其他模块使用
    QString filePath = tempFile.fileName();

    // 临时文件会在对象析构时自动删除
    // 如果不想自动删除,调用 tempFile.setAutoRemove(false);
}
我曾经踩过的坑:临时文件路径里不要包含中文或特殊字符。有一次我在Windows上用了中文模板,结果open()一直返回false。后来发现是文件系统编码问题。老老实实用英文路径,稳得很。

临时文件的典型应用场景:

场景 说明 注意事项
软件升级 下载更新包到临时目录 下载完成后移动到安装目录
编辑器备份 定时保存编辑中的文档 崩溃后恢复时保留临时文件
大数据排序 外部排序的中间缓存 注意磁盘空间,用完即删
网络缓存 下载大文件的分片缓存 断点续传时不能自动删除

2.4 文件锁:多进程安全的关键

多进程同时读写同一个文件,数据错乱是必然的。我见过最惨的一次,两个进程同时写配置文件,结果文件内容变成两段数据拼在一起,直接解析失败。

Qt提供了QLockFile,专门解决这个问题。它通过文件系统级别的锁文件来实现互斥。

// 使用文件锁保护共享资源
QLockFile lockFile(QDir::tempPath() + "/config.lock");
lockFile.setStaleLockTime(30000);  // 30秒后锁自动失效

if (lockFile.tryLock(1000)) {  // 等待1秒
    // 成功获取锁,可以安全读写共享文件
    QFile configFile("shared_config.ini");
    if (configFile.open(QIODevice::ReadWrite)) {
        // 读写操作...
        configFile.close();
    }

    lockFile.unlock();  // 释放锁
} else {
    qWarning() << "无法获取文件锁,其他进程正在使用";
}
个人习惯:我一般把锁文件放在临时目录,而不是和数据文件放在一起。这样即使锁文件残留,也不会污染数据目录。setStaleLockTime这个参数很关键,防止进程崩溃后锁永远不释放。

文件锁的几个要点:

  • 锁文件是独立的:锁文件本身不包含数据,只是一个标记
  • 自动清理:进程崩溃后,锁文件可能残留,staleLockTime可以兜底
  • 跨平台:Windows和Linux下都能用,底层实现不同但接口一致
  • 不要用于线程同步:QLockFile是进程间锁,线程间用QMutex

2.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速索引,遇到具体问题时直接定位到对应模块。

QFile高级操作知识体系 QFile高级操作知识体系 QFile 高级操作 文本文件读写 二进制文件读写 临时文件管理 文件锁机制 QTextStream 逐行读取 编码设置 QDataStream 字节序控制 版本兼容 QTemporaryFile 自动清理 唯一文件名 QLockFile 互斥访问 超时机制 核心原则 1. 大文件用流式读写,避免内存暴涨 2. 二进制数据用QDataStream,效率优先 3. 多进程共享文件必须加锁

嗯,到这里,QFile的四个高级操作就讲完了。文本读写别再用readAll(),二进制数据用QDataStream,临时文件交给QTemporaryFile,多进程安全靠QLockFile。这些套路我在项目里反复用,基本没出过问题。