昨晚本来准备写一个读位图(bmp)文件的程序,结果在读信息头的最后一个字段biClrImportant时出现了错误,没有得到预期的结果。用Winhex以二进制方式打开原位图文件,对比其中的数据,发现程序意外地跳过了下图所示阴影部分字段中的两个值为0x0B的字节:
查阅ASCII码表后,发现这是“垂直制表符”,传说中的空白字符,问题就出在读取文件使用的提取运算符‘>>’自动跳过了空白字符上。所以下面就讨论一下空白字符和C++中如何读入空白字符的问题,最后延伸一下在C++中进行文件读写的问题。
一、什么是ASCII码表中的空白字符?
ASCII码表中的空白字符主要有:空格(0x20,' '),回车符(0x0D,‘ ’),换行符(0x0A,' '),水平制表符(0x09,' '),垂直制表符(0x0B,'v'),换页符(f)。
以下内容来源于网络:
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
“回车”和“换行”是不是一回事?
“回车”的效果实际上是输出回到本行行首,结果可能会用回车符后的字符将这一行之前的输出覆盖掉。分在控制台中显示还是在文本编辑器中显示两种情况:
1.在控制台中的效果,不会换行且会用后面的覆盖前面的;
2.在文本编辑器中,用windows自带的记事本做测试,不会覆盖也不会显示回车符,与没有加回车符一样,在其他文本编辑器中则不一定是此结果(文本编辑器是ASCII显 示,即显示文件数据对应的ASCII字符,是否显示如何显示与编辑器有关,与文件本身的二进制数据无关)。
“换行”就是换到下一行首。 终端输出要达到换行效果用“ ”就可以,但要在文本文件输出中达到换行效果在各个系统中有所区别。在Unix系统中,每行的结尾是" ",windows中则是" ",mac则是" "。(本段未做测试)
“垂直制表符”:垂直制表符不常用,它的作用是让‘v’后面的字符从下一行开始输出,且开始的列数是“v”的前一个字符所在列的后面一列。
总结一下就是:‘ ’,‘ ’,‘v’,‘f’是控制字符,它们会控制字符的输出方式。当它们在终端输出时(打印在电脑屏幕上)会有上面的表现,但如果写入文本文件,一般文本编辑器(vi或记事本)对‘ ’‘v’‘f’的显示是没有控制效果的。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
二、C++中如何读入空白字符?
- 首先了解一下所谓“文件”的相关知识,这里有一篇不错的博文:http://www.cnblogs.com/flying-roc/articles/1798817.html 关于二进制文件与文本文件的介绍!
无论是文本文件还是二进制文件,从存储的角度而言,一切文件在物理上都是以二进制形式存储的,都是二进制文件,只不过我们解读的方式不同,或者说用不同的查看器打开时解析的方式不一样,因此有了我们看到的文本文件和二进制文件(不能以ASCII码映射为有意义的文本形式,通常所说的乱码文件)。对于编程,最重要的是要知道,如果目标文件本身是一个文本文件,那么最终该文件在文本编辑器中显示的时候,是以其二进制存储格式(以Winhex查看的格式)中的每个字节值作为一个ASCII码,逐字节翻译成对应的字符进行显示的。套用引文中的话说,二进制文件是无格式有数据类型的,文本文件是有格式无数据类型的。
- 然后讨论文件读取的方式,主要有三种:
1.纯C风格的File*;
2.调用Windows API;
3.C++风格的文件流fstream。
大一学的就是fstream,对此比较熟悉,因此讨论以fstream为主。fstream有两个派生类,即ifstream和ofstream,分别对应输入文件流、输出文件流。使用文件流读文件的流程:
1.创建输入文件流对象:ifstream fin;
2.将该对象与一个具体的文件关联起来:fin.open("XXX.YYY"); 实际上,open方法还包含一个参数mode,用以指定其打开方式。
ios::in 以读取方式打开文件
ios::out 以写入方式打开文件
ios::ate 存取指针在文件末尾
ios::app 写入时采用追加方式
ios::trunc 写入时抹去旧数据
ios::binary 以二进制方式存取。
未指定任何打开方式时,则采用默认参数:输入文件流即ios::in,输出文件流即ios::out。一般在需要组合特殊的mode才显式指定,比如: ios::in | ios::binary(以二进制方式读取文件)。除此之外,还可以在构造时指定相应的文件路径和名称,让创建过程一步到位。上述代码可改写为:
ifstream fin("C:filename.txt");
与open方法相反的是close方法,它的作用与open正好相反。open是将文件流对象与外设中的文件关联起来,close则是解除二者的关联。close还起到清空缓存的作用。最好让open方法与close方法成对出现。
创建并打开一个文件流后,就能像操作标准I/O那样使用流插入操作符(<<)与流提取操作符(>>)。对于输入文件流来说,使用提取操作符时会自动跳过上述六种空白字符,如果二进制文件中刚好有某个字节的值与这六种空白字符的ASCII码相同,则会造成问题,如我开篇所讲那样。尤其当文件较大时,遇到空白字符(只是值与空白字符的ASCII码相同,并不是起控制作用的)的概率将大大增加。
- 最后关键地,如何在读取文件时能读取到空白字符(显然已不能继续使用流提取操作符">>"):
经过实验,发现调用ifstream的成员函数 read(char* _Str,std::streamsize _Count) 可以读出任意的字节值,它是一种无格式有数据类型的读取,不会infer每个字节所包含的意义(如,该字节是不是空白字符,在ASCII码表中对应哪个字符),全部当作8位二进制的整数(具体是有符号还是无符号整数,取决于保存该整数的变量是unsigned char还是char)。值得注意的一点是read函数的第一个参数_Str是一个字符指针,实际上是一个字符串,用于保存读出的值,而实际输入的字节数由后面的_Count确定(不能超过数组的大小)。我的写法是:
//已经定义ifstream对象 pic
unsigned char uc_var;
read( (char*)&uc_var , 1 );
由于我每次只读入一个字节,将它以无符号整型的格式保存在uc_var中,所以采用上述强制类型转换。
最后提一下流提取操作符和流插入操作符,他们的道理是一致的,以流提取操作符>>为例说明。所有的原始输入(键盘输入或文本文件中保存的字符数据)对于>>而言都是字符串,即char* [](若一次只输入一个字符,则是单字符的字符串)。>>会根据后面所跟变量的数据类型,适当的进行转换。如后面是int型,就要将字符串(如"1234",是四个独立的ASCII码)转换成整型值(如1234,一个整型数值)。当越界或类型无法匹配时,如对于unsigned short型变量,输入"65536"或“abc”,则会出错(VS2012中陷入死循环)。对于输出而言,如果对一个值为1234的unsigned short类型的变量用<<进行输出,那么实际上<<会首先对该变量进行转换,转换成由四个字符组成的字符串"1234"(四个ASCII码)。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
最后放一篇刚看到的文章,排版吐槽一下,看得我好累...文章中有部分结论我觉得还有待讨论,作者说的有点绝对,而我并不认同,这些剩下的有时间再整理吧。
http://pnig0s1992.blog.51cto.com/393390/563152 这篇文章讲二进制文件的读写,部分引用如下,
只有使用fwrite和write()函数才能以二进制形式输出到文件中,调用puts、fprintf、<<等函数输出的都是ASCII文本,使用<<来输出一个整数时,输出到二进制文件中的仍然是文本格式!<< operator在输出之前会自动给你进行转换,把一个整数值转换成一位一位的数字字符!而且我后来试过了,即使我以文本模式打开一个文件,假如我用fwrite 函数输出的话,文件中仍然是二进制格式,呵呵,说明在输出数据到文件时,的确与打开文件的模式没有关系,只与调用的输出函数有关!!