【Qt笔记】二进制文件读写

在上一章中,咱们介绍了有关QFileQFileInfo两个类的使用。咱们提到,QIODevice提供了read()readLine()等基本的操做。同时,Qt 还提供了更高一级的操做:用于二进制的流QDataStream和用于文本流的QTextStream。本节,咱们将讲解有关QDataStream的使用以及一些技巧。下一章则是QTextStream的相关内容。网络

QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流彻底不依赖于底层操做系统、CPU 或者字节顺序(大端或小端)。例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,能够不通过任何处理,直接拿到运行了 Solaris 的 SPARC 机器上读取。因为数据流就是二进制流,所以咱们也能够直接读写没有编码的二进制数据,例如图像、视频、音频等。函数

QDataStream既可以存取 C++ 基本类型,如 int、char、short 等,也能够存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为不少基本单元实现的。性能

结合QIODeviceQDataStream能够很方便地对文件、网络套接字等进行读写操做。咱们从代码开始看起:ui

QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out << QString("the answer is");
out << (qint32)42;

在这段代码中,咱们首先打开一个名为 file.dat 的文件(注意,咱们为简单起见,并无检查文件打开是否成功,这在正式程序中是不容许的)。而后,咱们将刚刚建立的file对象的指针传递给一个QDataStream实例out。相似于std::cout标准输出流,QDataStream也重载了输出重定向<<运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。因为咱们的 out 对象创建在file之上,所以至关于将宇宙终极问题的答案写入file编码

须要指出一点:最好使用 Qt 整型来进行读写,好比程序中的qint32。这保证了在任意平台和任意编译器都可以有相同的行为。操作系统

咱们经过一个例子来看看 Qt 是如何存储数据的。例如char *字符串,在存储时,会首先存储该字符串包括 \0 结束符的长度(32位整型),而后是字符串的内容以及结束符 \0。在读取时,先以 32 位整型读出整个的长度,而后按照这个长度取出整个字符串的内容。指针

可是,若是你直接运行这段代码,你会获得一个空白的 file.dat,并无写入任何数据。这是由于咱们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。所以,咱们必须在最后添加一行代码:rest

file.close(); // 若是不想关闭文件,可使用 file.flush();

下面,咱们要将这个答案读取出来:code

QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;

这段代码没什么好说的。惟一须要注意的是,你必须按照写入的顺序,将数据读取出来。也就是说,程序数据写入的顺序必须预先定义好。在这个例子中,咱们首先写入字符串,而后写入数字,那么就首先读出来的就是字符串,而后才是数字。顺序颠倒的话,程序行为是不肯定的,严重时会直接形成程序崩溃。视频

因为二进制流是纯粹的字节数据,带来的问题是,若是程序不一样版本之间按照不一样的方式读取(前面说过,Qt 保证读写内容的一致,可是并不能保证不一样 Qt 版本之间的一致),数据就会出现错误。所以,咱们必须提供一种机制来确保不一样版本之间的一致性。一般,咱们会使用以下的代码写入:

QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);

// 写入魔术数字和版本
out << (quint32)0xA0B0C0D0;
out << (qint32)123;

out.setVersion(QDataStream::Qt_4_0);

// 写入数据
out << lots_of_interesting_data;

这里,咱们增长了两行代码:

out << (quint32)0xA0B0C0D0;

用于写入魔术数字。所谓魔术数字,是二进制输出中常用的一种技术。二进制格式是人不可读的,而且一般具备相同的后缀名(好比 dat 之类),所以咱们没有办法区分两个二进制文件哪一个是合法的。因此,咱们定义的二进制格式一般具备一个魔术数字,用于标识文件的合法性。在本例中,咱们在文件最开始写入 0xA0B0C0D0,在读取的时候首先检查这个数字是否是 0xA0B0C0D0。若是不是的话,说明这个文件不是可识别格式,所以根本不须要去继续读取。通常二进制文件都会有这么一个魔术数字,例如 Java 的 class 文件的魔术数字就是 0xCAFEBABE,使用二进制查看器就能够查看。魔术数字是一个 32 位的无符号整型,所以咱们使用quint32来获得一个平台无关的 32 位无符号整型。

接下来一行,

out << (qint32)123;

是标识文件的版本。咱们用魔术数字标识文件的类型,从而判断文件是否是合法的。可是,文件的不一样版本之间也可能存在差别:咱们可能在初版保存整型,第二版可能保存字符串。为了标识不一样的版本,咱们只能将版本写入文件。好比,如今咱们的版本是 123。

下面一行仍是有关版本的:

out.setVersion(QDataStream::Qt_4_0);

上面一句是文件的版本号,可是,Qt 不一样版本之间的读取方式可能也不同。这样,咱们就得指定 Qt 按照哪一个版本去读。这里,咱们指定以 Qt 4.0 格式去读取内容。

当咱们这样写入文件以后,咱们在读取的时候就须要增长一系列的判断:

QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);

// 检查魔术数字
quint32 magic;
in >> magic;
if (magic != 0xA0B0C0D0) {
    return BAD_FILE_FORMAT;
}

// 检查版本
qint32 version;
in >> version;
if (version < 100) {
    return BAD_FILE_TOO_OLD;
}
if (version > 123) {
    return BAD_FILE_TOO_NEW;
}

if (version <= 110) {
    in.setVersion(QDataStream::Qt_3_2);
} else {
    in.setVersion(QDataStream::Qt_4_0);
}
// 读取数据
in >> lots_of_interesting_data;
if (version >= 120) {
    in >> data_new_in_version_1_2;
}
in >> other_interesting_data;

这段代码就是按照前面的解释进行的。首先读取魔术数字,检查文件是否合法。若是合法,读取文件版本:小于 100 或者大于 123 都是不支持的。若是在支持的版本范围内(100 <= version <= 123),则当是小于等于 110 的时候,按照Qt_3_2的格式读取,不然按照Qt_4_0的格式读取。当设置完这些参数以后,开始读取数据。

至此,咱们介绍了有关QDataStream的相关内容。那么,既然QIODevice提供了read()readLine()之类的函数,为何还要有QDataStream呢?QDataStreamQIODevice有什么区别?区别在于,QDataStream提供流的形式,性能上通常比直接调用原始 API 更好一些。咱们经过下面一段代码看看什么是流的形式:

QFile file("file.dat");
file.open(QIODevice::ReadWrite);

QDataStream stream(&file);
QString str = "the answer is 42";
QString strout;

stream << str;
file.flush();
stream >> strout;

在这段代码中,咱们首先向文件中写入数据,紧接着把数据读出来。有什么问题吗?运行以后你会发现,strout实际是空的。为何没有读取出来?咱们不是已经添加了file.flush();语句吗?缘由并不在于文件有没有写入,而是在于咱们使用的是“流”。所谓流,就像水流同样,它的游标会随着输出向后移动。当使用<<操做符输出以后,流的游标已经到了最后,此时你再去读,固然什么也读不到了。因此你须要在输出以后从新把游标设置为 0 的位置才可以继续读取。具体代码片断以下:

stream << str;
stream.device()->seek(0);
stream >> strout;
相关文章
相关标签/搜索