4. QIODevice与数据流:QDataStream与QTextStream的序列化与反序列化

说到 Qt 的文件读写,很多人第一反应就是 QFile::readAll() 或者 write()。但说实话,真正做项目时,你很少直接跟 QFile 的字节流打交道。为什么?因为你要处理的是「数据」,不是「字节」。

我个人习惯把 QIODevice 看作一根水管,而 QDataStream 和 QTextStream 就是接在水管上的「智能水龙头」。你不需要关心水管里流的是什么,水龙头会帮你把水转换成你想要的形态。

核心认知: QIODevice 是底层 I/O 抽象,QDataStream/QTextStream 是上层序列化工具。前者管「怎么传」,后者管「传什么」。

4.1 三者的关系:一根水管,两个水龙头

先看一张图,把这三者的关系理清楚。

QIODevice、QDataStream、QTextStream 关系图 QIODevice(水管) QFile QBuffer QTcpSocket QDataStream(二进制水龙头) QTextStream(文本水龙头) QDataStream 处理二进制(紧凑、跨平台),QTextStream 处理文本(可读、国际化) 两者都通过 setDevice() 绑定到 QIODevice 子类

你看,QIODevice 在底层,QFile、QBuffer、QTcpSocket 都是它的子类。而 QDataStream 和 QTextStream 不继承 QIODevice,它们是「包装器」——通过 setDevice() 绑定到具体的设备上。

我的习惯: 只要涉及跨平台文件交换,我优先用 QDataStream。文本文件只在需要人工阅读或与第三方系统对接时才用 QTextStream。

4.2 QDataStream:二进制序列化的正确姿势

QDataStream 是 Qt 序列化的核心。它把 C++ 的基本类型、Qt 容器、甚至自定义类型,都转换成平台无关的二进制格式。说白了,你在 Windows 上写的数据,在 Linux 上读回来,完全没问题。

4.2.1 基本类型读写

// 写数据
QFile file("data.bin");
if (!file.open(QIODevice::WriteOnly)) {
    qWarning() << "打开文件失败";
    return;
}

QDataStream out(&file);
out << 42;           // int
out << 3.14159;      // double
out << QString("Qt"); // QString
out << QByteArray("Hello"); // QByteArray

file.close();

// 读数据
if (!file.open(QIODevice::ReadOnly)) {
    qWarning() << "打开文件失败";
    return;
}

QDataStream in(&file);
int i;
double d;
QString s;
QByteArray ba;

in >> i >> d >> s >> ba;

qDebug() << i << d << s << ba;
file.close();

这里有个细节:QDataStream 默认使用大端字节序(Big Endian)。如果你跟其他系统对接,可能需要调整字节序:

out.setByteOrder(QDataStream::LittleEndian);
我曾经踩过的坑: 有一次跟一个嵌入式设备对接,对方用的小端序,我忘了设置,结果读出来的 int 全是乱码。排查了整整半天才发现是字节序问题。所以,跨系统对接时,第一件事就是确认字节序。

4.2.2 版本控制:别让旧文件读不了

项目迭代过程中,数据结构会变。今天加个字段,明天删个字段。如果不做版本控制,旧文件就废了。

QDataStream 提供了版本号机制:

// 写数据时设置版本
QDataStream out(&file);
out.setVersion(QDataStream::Qt_5_15);

// 读数据时也要设置
QDataStream in(&file);
in.setVersion(QDataStream::Qt_5_15);

我个人习惯在文件头写一个自定义版本号:

// 写
quint32 magic = 0xDEADBEEF;  // 魔数,用于校验文件格式
quint16 version = 2;          // 数据版本

out << magic << version;
out << data1 << data2;

// 读
quint32 magic;
quint16 version;
in >> magic >> version;

if (magic != 0xDEADBEEF) {
    // 不是本程序的文件
    return;
}

switch (version) {
case 1:
    // 旧版本读取逻辑
    break;
case 2:
    // 新版本读取逻辑
    break;
}
经验之谈: 版本号从 1 开始,不要从 0 开始。魔数用 4 字节的十六进制,一眼就能看出是不是自己的文件。

4.2.3 自定义类型的序列化

项目中你肯定有自己的结构体。让 QDataStream 认识它们,需要重载 << 和 >> 运算符:

struct UserInfo {
    int id;
    QString name;
    double score;
};

// 写运算符重载
QDataStream &operator<<(QDataStream &out, const UserInfo &user) {
    out << user.id << user.name << user.score;
    return out;
}

// 读运算符重载
QDataStream &operator>>(QDataStream &in, UserInfo &user) {
    in >> user.id >> user.name >> user.score;
    return in;
}

// 使用
UserInfo user{1, "张三", 95.5};
out << user;

UserInfo user2;
in >> user2;

你想想看,这样写完之后,UserInfo 就跟 int、QString 一样,可以直接用 << 和 >> 了。是不是很清爽?

4.3 QTextStream:文本读写的正确打开方式

QTextStream 处理的是文本,不是二进制。它最大的优势是编码处理——自动帮你搞定 UTF-8、GBK、Latin1 这些编码转换。

4.3.1 编码设置

QFile file("data.txt");
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
    return;
}

QTextStream out(&file);
out.setCodec("UTF-8");  // 设置编码
out << "你好,世界!" << Qt::endl;

file.close();

// 读取
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    return;
}

QTextStream in(&file);
in.setCodec("UTF-8");
QString line = in.readLine();  // 读一行
QString all = in.readAll();    // 读全部

file.close();
注意: 打开文件时加上 QIODevice::Text 标志,Qt 会自动处理换行符的跨平台转换(Windows 的 \r\n 和 Linux 的 \n)。

4.3.2 格式化输出

QTextStream 支持类似 printf 的格式化,但更安全:

QTextStream out(&file);

// 设置字段宽度和对齐
out << left << qSetFieldWidth(10) << "Name" << qSetFieldWidth(0) << Qt::endl;
out << left << qSetFieldWidth(10) << "Alice" << qSetFieldWidth(0) << Qt::endl;

// 浮点数精度
double pi = 3.1415926535;
out << qSetRealNumberPrecision(3) << pi;  // 输出 3.142

// 整数进制
int value = 255;
out << hex << value;   // 输出 ff
out << dec << value;   // 输出 255

4.3.3 大文件逐行处理

我曾经处理过一个 2GB 的日志文件。如果用 readAll(),内存直接爆掉。正确的做法是逐行读:

QFile file("huge_log.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    return;
}

QTextStream in(&file);
int lineCount = 0;

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

    // 每 10000 行打印一次进度
    if (lineCount % 10000 == 0) {
        qDebug() << "已处理" << lineCount << "行";
    }
}

file.close();
避坑指南: 不要用 in.readLine() 去读二进制文件。文本流遇到 0x00 会当作字符串结束符,导致数据截断。二进制文件请用 QDataStream 或 QFile::read()。

4.4 性能对比:该用哪个?

我整理了一个对比表,方便你根据场景选择:

特性 QDataStream QTextStream
数据格式 二进制 文本
可读性 不可读 可读
文件大小 紧凑(小) 较大
读写速度 快 较慢(编码转换开销)
跨平台性 内置支持(字节序可控) 依赖编码设置
适用场景 程序内部数据交换、配置文件、网络传输 日志文件、用户可见的文本、CSV
版本控制 内置版本号机制 需自行实现

4.5 实战:一个完整的序列化示例

最后,我把前面讲的知识点串起来,写一个完整的示例。这个例子模拟了一个学生成绩管理系统,把数据序列化到文件,再反序列化回来。

#include <QCoreApplication>
#include <QFile>
#include <QDataStream>
#include <QDebug>

struct Student {
    int id;
    QString name;
    double score;
};

QDataStream &operator<<(QDataStream &out, const Student &s) {
    out << s.id << s.name << s.score;
    return out;
}

QDataStream &operator>>(QDataStream &in, Student &s) {
    in >> s.id >> s.name >> s.score;
    return in;
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    // 准备数据
    QList<Student> students = {
        {1, "张三", 95.5},
        {2, "李四", 88.0},
        {3, "王五", 92.3}
    };

    // 写文件
    QFile file("students.dat");
    if (!file.open(QIODevice::WriteOnly)) {
        qWarning() << "无法写入文件";
        return 1;
    }

    QDataStream out(&file);
    out.setVersion(QDataStream::Qt_5_15);

    // 写魔数和版本
    out << (quint32)0x12345678;
    out << (quint16)1;

    // 写学生数量
    out << (quint32)students.size();

    // 写每个学生
    for (const auto &s : students) {
        out << s;
    }

    file.close();
    qDebug() << "写入完成,共" << students.size() << "条记录";

    // 读文件
    if (!file.open(QIODevice::ReadOnly)) {
        qWarning() << "无法读取文件";
        return 1;
    }

    QDataStream in(&file);
    in.setVersion(QDataStream::Qt_5_15);

    quint32 magic;
    quint16 version;
    in >> magic >> version;

    if (magic != 0x12345678) {
        qWarning() << "文件格式错误";
        return 1;
    }

    qDebug() << "文件版本:" << version;

    quint32 count;
    in >> count;

    QList<Student> loaded;
    for (quint32 i = 0; i < count; ++i) {
        Student s;
        in >> s;
        loaded.append(s);
    }

    file.close();

    // 输出结果
    qDebug() << "读取完成,共" << loaded.size() << "条记录:";
    for (const auto &s : loaded) {
        qDebug() << s.id << s.name << s.score;
    }

    return 0;
}

这个例子包含了魔数校验、版本控制、自定义类型序列化、列表读写。你直接拿去用,改改结构体就能适配自己的项目。

总结一下: QDataStream 适合程序内部的数据持久化,QTextStream 适合人类可读的文本文件。选对工具,你的代码会少很多坑。