Qt 学习之路 2(36):二进制文件读写
Qt 学习之路 2(36):二进制文件读写
在上一章中,我们介绍了有关QFile和QFileInfo两个类的使用。我们提到,QIODevice提供了read()、readLine()等基本的操作。同时,Qt 还提供了更高一级的操作:用于二进制的流QDataStream和用于文本流的QTextStream。本节,我们将讲解有关QDataStream的使用以及一些技巧。下一章则是QTextStream的相关内容。
QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,可以不经过任何处理,直接拿到运行了 Solaris 的 SPARC 机器上读取。由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等。
QDataStream既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。
结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起:
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out << QString("the answer is");
out << (qint32)42;
| 
 1 
2 
3 
4 
5 
 | 
 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 输出到数据流(如果你不明白这句话的意思,这可是宇宙终极问题的答案 ;-P 请自行搜索《银河系漫游指南》)。由于我们的 out 对象建立在file之上,因此相当于将宇宙终极问题的答案写入file。
需要指出一点:最好使用 Qt 整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。
我们通过一个例子来看看 Qt 是如何存储数据的。例如char *字符串,在存储时,会首先存储该字符串包括 \0 结束符的长度(32位整型),然后是字符串的内容以及结束符 \0。在读取时,先以 32 位整型读出整个的长度,然后按照这个长度取出整个字符串的内容。
但是,如果你直接运行这段代码,你会得到一个空白的 file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:
| 
 1 
 | 
 file.close(); // 如果不想关闭文件,可以使用 file.flush(); 
 | 
重新运行一下程序,你就得到宇宙终极问题的答案了。
我们已经获得宇宙终极问题的答案了,下面,我们要将这个答案读取出来:
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;
| 
 1 
2 
3 
4 
5 
6 
 | 
 QFile file("file.dat"); 
file.open(QIODevice::ReadOnly); 
QDataStream in(&file); 
QString str; 
qint32 a; 
in >> str >> a; 
 | 
这段代码没什么好说的。唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。也就是说,程序数据写入的顺序必须预先定义好。在这个例子中,我们首先写入字符串,然后写入数字,那么就首先读出来的就是字符串,然后才是数字。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。
由于二进制流是纯粹的字节数据,带来的问题是,如果程序不同版本之间按照不同的方式读取(前面说过,Qt 保证读写内容的一致,但是并不能保证不同 Qt 版本之间的一致),数据就会出现错误。因此,我们必须提供一种机制来确保不同版本之间的一致性。通常,我们会使用如下的代码写入:
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
// 写入魔术数字和版本
out << (quint32)0xA0B0C0D0;
out << (qint32)123;
out.setVersion(QDataStream::Qt_4_0);
// 写入数据
out << lots_of_interesting_data;
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
 | 
 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; 
 | 
这里,我们增加了两行代码:
| 
 1 
 | 
 out << (quint32)0xA0B0C0D0; 
 | 
用于写入魔术数字。所谓魔术数字,是二进制输出中经常使用的一种技术。二进制格式是人不可读的,并且通常具有相同的后缀名(比如 dat 之类),因此我们没有办法区分两个二进制文件哪个是合法的。所以,我们定义的二进制格式通常具有一个魔术数字,用于标识文件的合法性。在本例中,我们在文件最开始写入 0xA0B0C0D0,在读取的时候首先检查这个数字是不是 0xA0B0C0D0。如果不是的话,说明这个文件不是可识别格式,因此根本不需要去继续读取。一般二进制文件都会有这么一个魔术数字,例如 Java 的 class 文件的魔术数字就是 0xCAFEBABE,使用二进制查看器就可以查看。魔术数字是一个 32 位的无符号整型,因此我们使用quint32来得到一个平台无关的 32 位无符号整型。
接下来一行,
| 
 1 
 | 
 out << (qint32)123; 
 | 
是标识文件的版本。我们用魔术数字标识文件的类型,从而判断文件是不是合法的。但是,文件的不同版本之间也可能存在差异:我们可能在第一版保存整型,第二版可能保存字符串。为了标识不同的版本,我们只能将版本写入文件。比如,现在我们的版本是 123。
下面一行还是有关版本的:
| 
 1 
 | 
 out.setVersion(QDataStream::Qt_4_0); 
 | 
上面一句是文件的版本号,但是,Qt 不同版本之间的读取方式可能也不一样。这样,我们就得指定 Qt 按照哪个版本去读。这里,我们指定以 Qt 4.0 格式去读取内容。
当我们这样写入文件之后,我们在读取的时候就需要增加一系列的判断:
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;
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
 | 
 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呢?QDataStream同QIODevice有什么区别?区别在于,QDataStream提供流的形式,性能上一般比直接调用原始 API 更好一些。我们通过下面一段代码看看什么是流的形式:
file.open(QIODevice::ReadWrite);
QDataStream stream(&file);
QString str = "the answer is 42";
QString strout;
stream << str;
file.flush();
stream >> strout;
| 
 1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
 | 
 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.device()->seek(0);
stream >> strout;
| 
 1 
2 
3 
 | 
 stream << str; 
stream.device()->seek(0); 
stream >> strout; 
 | 
Qt 学习之路 2(36):二进制文件读写的更多相关文章
- Qt 学习之路 2(37):文本文件读写
		
Qt 学习之路 2(37):文本文件读写 豆子 2013年1月7日 Qt 学习之路 2 23条评论 上一章我们介绍了有关二进制文件的读写.二进制文件比较小巧,却不是人可读的格式.而文本文件是一种人可读 ...
 - 《Qt 学习之路 2》目录
		
<Qt 学习之路 2>目录 <Qt 学习之路 2>目录 豆子 2012年8月23日 Qt 学习之路 2 177条评论 <Qt 学习之路 2>目录 序 Qt ...
 - Qt 学习之路 2(72):线程和事件循环
		
Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻> -- 有需求的话还需要进行专题学习 豆子 2013年11月24日 Qt 学习之路 2 34条评论 前面一章我 ...
 - Qt 学习之路 2(71):线程简介
		
Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...
 - Qt 学习之路 2(69):进程
		
Qt 学习之路 2(69):进程 豆子 2013年11月9日 Qt 学习之路 2 15条评论 进程是操作系统的基础之一.一个进程可以认为是一个正在执行的程序.我们可以把进程当做计算机运行时的一个基础单 ...
 - Qt 学习之路 2(67):访问网络(3)
		
Qt 学习之路 2(67):访问网络(3) 豆子 2013年11月5日 Qt 学习之路 2 16条评论 上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作.本章我们将继续完 ...
 - Qt 学习之路 2(66):访问网络(2)
		
Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2) 豆子 2013年10月31日 Qt 学习之路 2 27条评论 上一 ...
 - Qt 学习之路 2(64):使用 QJsonDocument 处理 JSON
		
Home / Qt 学习之路 2 / Qt 学习之路 2(64):使用 QJsonDocument 处理 JSON Qt 学习之路 2(64):使用 QJsonDocument 处理 JSON 豆子 ...
 - Qt 学习之路 2(62):保存 XML
		
Home / Qt 学习之路 2 / Qt 学习之路 2(62):保存 XML Qt 学习之路 2(62):保存 XML 豆子 2013年8月26日 Qt 学习之路 2 9条评论 前面几章我们 ...
 
随机推荐
- ORA-01145: 除非启用了介质恢复 否则不允许立即脱机
			
Microsoft Windows [版本 6.1.7601]版权所有 (c) 2009 Microsoft Corporation.保留所有权利. C:\Users\Administrator> ...
 - [bzoj3223]文艺平衡树(splay区间反转模板)
			
解题关键:splay模板题. #include<cstdio> #include<cstring> #include<algorithm> #include< ...
 - 【HDU3394】Railway
			
[题目描述] 有一个公园有n个景点,公园的管理员准备修建m条道路,并且安排一些形成回路的参观路线.如果一条道路被多条道路公用,那么这条路是冲突的:如果一条道路没在任何一个回路内,那么这条路是不冲突的 ...
 - 构造方法概念,自定义构造(init)方法的用途, 类工厂方法(就是直接用类名 类调用)
			
一. 构造方法 构造方法:在OC中init开头的方法, 我们称之为构造方法 构造方法的用途: 用于初始化一个对象, 让某个对象一创建出来就拥有某些属性和值 // 比如我们定义一个Person的类,然后 ...
 - SpringBoot25 gradle安装、利用gradle创建SrpingBoot项目
			
1 gradle安装 技巧01:gradle依赖JDK或者JRE,而且版本至少时1.7 1.1 下载安装包 到gradle官网下载安装包[PS: 也可以利用命令的方式安装,本案例是利用安装包的方式] ...
 - mybatis 框架 的应用之三(操作两张没有关联的表,存在主键和外键关系)
			
#注意:要配置开启多条语句操作,否则会报错( org.apache.ibatis.exceptions.PersistenceException) lf-driver=com.mysql.jdbc.D ...
 - IntelliJ IDEA——利用maven插件构建web工程
 - 高级软件测试技术(测试管理工具实践day3)
			
昨天在晚上由于安装bugzilla中有一些小问题,并且需要手工安装很多perl模块 ppm install XXX(模块名称).一直到过了十二点就没有继续更博了.所以由今天更. 继昨天的安装问题之后 ...
 - zend studio中安装Emmet插件后迅速编写html的方法
			
table>tr*3>th*1+td*3h1{hello} <h1>hello</h1>a[href="xx.xxx.xxx(网址) ...
 - HTML__图片轮播ion-slide-box
			
先大概描述一下要做的界面: 从网络请求json数据,获取网络图征数据,然后轮播图片.我遇到的问题是:图片不显示,代码如下 <ion-slide-box does-continue="t ...