4. QIODevice与数据流:QDataStream与QTextStream的序列化与反序列化
说到 Qt 的文件读写,很多人第一反应就是 QFile::readAll() 或者 write()。但说实话,真正做项目时,你很少直接跟 QFile 的字节流打交道。为什么?因为你要处理的是「数据」,不是「字节」。
我个人习惯把 QIODevice 看作一根水管,而 QDataStream 和 QTextStream 就是接在水管上的「智能水龙头」。你不需要关心水管里流的是什么,水龙头会帮你把水转换成你想要的形态。
4.1 三者的关系:一根水管,两个水龙头
先看一张图,把这三者的关系理清楚。
你看,QIODevice 在底层,QFile、QBuffer、QTcpSocket 都是它的子类。而 QDataStream 和 QTextStream 不继承 QIODevice,它们是「包装器」——通过 setDevice() 绑定到具体的设备上。
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);
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;
}
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;
}
这个例子包含了魔数校验、版本控制、自定义类型序列化、列表读写。你直接拿去用,改改结构体就能适配自己的项目。