如何读写二进制文件

DinS          Written on 2017/12/13

本文介绍如何读写二进制文件,对于文本文件可见《如何写文本文件》和《如何读文本文件

一、写二进制文件

下面让我们进入二进制的世界。既然是二进制,肯定没有文本文件那么直观了,一定要理解类型、字节和文件之间的关系,如果没有理解,再读一遍《如何写文本文件》一文中的文件概念一节。

先来看看最简单的一个例子,在这里例子里我们写一个int和一串字符串的二进制。

构造fout时指定以二进制方式,这个应该没什么问题,整个代码的核心就是那个fout.write。看起来貌似很复杂,但是其实还好。
先不管具体参数是怎么写的,write就接收两个参数。第一个是一个char*指针。为什么是char?因为一个char是一个字节,而字节是我们能够操作的最小单位,因此char*就相当于按字节写入了。第二个是指定写入的字节数量,也就是指明要写多少字节进去。
理解了这个再来看具体的参数写法。第一个参数是(char*)&nNumber,nNumber是int型,所以我们把它强制转化为char*,又因为nNumber是变量,所以加上&取地址,才是合法的指针。你可能会有疑问:明明是int,转成char不会出问题吗?
因为这里仅仅是按照字节写,我们并不操作这个变量干其他事,所以不会有问题。后面那个sizeof(int)求得int的字节数。因为我们知道int是4字节,所以你直接填4在这里不会出问题。但是对于自定义类型我们不知道字节数,所以还是统一用sizeof好。
整个这句代码表示的就是把这个nNumber的四个字节写入文件。
另一个小疑问是计算机怎么知道从nNumber的第一个字节开始后面的字节在哪里?不会写串行吗?这个程序员就不用管了,是底层的事。

再来看看下一行代码,这其实是针对string的一种简化写法。看第一个参数。因为string本身装的就是char,并且c_str()直接返回const char*,也就不用强制转换了。直接写str.c_str()即可。第二个参数实际上省略了sizeof(char),因为char就是1字节。但是只有1字节还不够,我们还要告诉write一共要写多少字节,string的长度实际上就是其字节大小,因此第二个参数直接写str.size()即可。(注:如果使用wstring那么一个字符就不是1字节了,可见字符集专题
另外没有endl之类的控制格式的东西,这是因为二进制本身就不是给人读的,不需要。如果真加入了控制符读二进制文件反而增加了困难。

运行看结果

注意这个文件是26字节。一个int是4字节,那个字符串有22个字符,共22个字节。因此是成功了。打开看看

文本部分显示正常,但是int那部分是乱码。这就对了。这正是二进制文件与文本文件的区别。用文本编辑器打开文件,默认里面的都是可以用char解释的字符,所以显示出来是人类能够识别的字符串,但是用二进制写,就没有这个约定了。
为了增加我们的理解,让我们用二进制编辑器打开看看

这是十六进制的表示。果然人类看不懂,但是别着急,我们第一个写入的是123456,所以说头四个字节应该对应这个数的二进制,打开计算器输入123456

然后选择左边的十六进制

跟上面编辑器对比一下,发现了什么?
其实是一样的,只不过顺序不一样,这是高低位的区别。计算器显示的内容的完整表述是00 01 E2 40这么四个字节,而编辑器显示的是40 E2 01 00。
这说明我们确实把123456这个int按照二进制写入文件了。

应该能够发现,写二进制文件跟类型的外延其实没有什么关系,都是按字节写入。只要知道类型占多少字节即可,因此可以使用c++的模板来制作一个通用的函数。
比如这样

这就是对刚才代码的泛化,把类型换成了T而已,然后可以这样使用

结果

补充一句,因为是二进制所以起什么后缀名都无所谓,一般就用bin或者dat

二、读二进制文件

如果理解了写二进制文件,那么读二进制文件就没什么难度了,只不过是反过程而已。当然要读二进制文件,必须先有二进制文件,我们使用之前生成的Binary.bin做试验。
新建工程,把文件拷贝到工程目录,然后写如下代码

核心是read(),这个跟write正好相反。
读int就不需要多解释了,道理是一样的。
读字符串则需要解释解释,如果这里直接使用string会出现莫名其妙的问题,究其根本string并不是char*。为了读字符串,我们必须使用原始的字符数组char[]。
然后注意到23和22的区别。因为我们知道二进制里面的字符串一个有22个字节,所以可以写22.换句话说如果我们不知道二进制文件内容很难解读出有意义的信息。那么23是怎么回事?原始的字符数组末尾有一个\0,这本身占一个字符,所以要多申请一个字节。
运行看结果

可见不管是int还是字符串都成功读入了。字符数组的话可以在读取成功后转成string,这样之后的操作都方便。

同理read与类型的关系并不大,我们可以仿照write的方式制作一个模版

使用之前的4个int二进制文件测试

运行结果

但是要注意,读文件比写文件麻烦。
读文件没有app模式,所以如果一个二进制文件里有不同的类型,那么这个模版中最好是传入ifstream的引用,这样才能保证读完一段后再次调用可以接着读。
另外这里还有一个参数nTimes指明读的次数,这个也是不可省略的,除非你想从头读到尾。

读文件的时候我们没有用string,但是这并不意味着string没有作用。实际上string在某个场合有妙用:我们不关心二进制内容,仅仅为了转发(比如网络通信)。string本质上可以理解为长度可变的字节数组,因为其内部是char。于是我们可以这样写

我们并不管读出来的内容是什么,仅仅是逐字节提取信息,存入字节数组string中,等到二进制文件读完就返回内容。String可以非常大,所以通常而言不必担心out_of_range,直接返回编译器会进行return-value-optimization,也不用担心效率问题。

(注:实际上也可以使用getline一次读取所有二进制内容到string里,写法跟读文本文件一样)

这样使用

运行

编译器会说字符无效,但是不必管它,因为我们只是转发信息。然后可以拿着这个字符串socket出去之类的。
使用string还有一个原因:兼容性。第三方库都必须兼容string,所以用string来充当信息容器是再好不过的了