1、实验课第7讲,文件的逻辑结构 字符和中文字符的概念 文件压缩和解压的步骤 编码和解码的过程 计算文件中各字符出现的频数 位操作 获取某一位的值 改变某一位的值 将一个整数存入文件的不同方法比较 压缩文件的结构 对文件I/O流操作 对二进制文件的操作 文件的存取和指针定位,文件的逻辑结构与字符集的概念,计算机操作系统中将文件看作是字节流结构的,也就是说文件的逻辑结构是字节流式的,即文件是由一个个字节组成的。 注意下述概念的关系 ANSI字符、ASCII字符和中文字符 最初只有ANSI的ASCII字符集,它使用7 bits来表示一个字符(字节的最高位为0),总共表示128个字符。 扩展ASCII字
2、符集:IBM扩展了ASCII字符集,用8bit来表示一个字符,总共可以表示256个字符,充分利用了一个字节。 ANSI字符集:ASCII字符集,以及由此派生并兼容的字符集,如:GB2312。 ANSI字符集也称为MBCS(Multi-Byte Chactacter System,多字节字符系统)。 中文字符:在ANSI中,用两个字节表示,每个字节最高位为1。 在进行文件压缩时只需将文件看作是一些字节的序列来处理即可。,文件压缩的步骤,假定要压缩的文件内容为: AABBABABACDCEACBDEC 那么压缩步骤如下: 统计文件中各个字符出现的次数: A:6 B:5 C:4 D:2 E:1 建立
3、Huffman树; 由此获得Huffman编码: A:10 B: 11 C:01 D:000 E:001 用编码替换文件的字符,得文件的编码: 101011111011101110010000100110011100000101 注意:不能将编码后的字串直接存入磁盘,而是需要将其0和1看作是二进制位来存盘,这样上述编码就只用6个字节存放即可。,用编码替换文件内容的方法,0)将各个字符的编码存在数组中(编码是0和1组成的串) 1)当从文件中读到一个字符,则在数组中查到该字符的编码串; 2)将编码串与前面字符得到的编码合并,得到一个更长的串,若串长度不小于8,则将其前8个0和1的字符转为一个字节存
4、盘,直到串长度小于8为止; 3)判断文件是否还有未读字符,若有,转1),将0和1构成的字串转为字节的函数,我们需要写一个函数,它将0和1构成的字串的前8个字符转为一个字节并作为函数返回,同时,将原字串中仅剩下未转换的字串,假如这个转换函数为: char getByteFromBinString(char s); 如果s“01000001101110”;那么执行 cout getByteFromBinString(s)endl; 屏幕显示A, 并且s的内容改变为: s“101110”,只剩下未转换的部分。 这个函数的实现需要能改变字节的每一位的函数,位操作改变某一位的值,unsigned cha
5、r setBit2One(unsigned char c, int n) /将c的第n位的值改为1 switch (n)case 1:return (c | 0x01);case 2:return (c | 0x02);case 3:return (c | 0x04);case 4:return (c | 0x08);,case 5:return (c | 0x10);case 6:return (c | 0x20);case 7:return (c | 0x40);case 8:return (c | 0x80);exit(1); ;,unsigned char setBit2Zero(un
6、signed char c, int n)/将c的第n位的值改为0 switch (n)case 1:return (c ,case 5:return (c ,/将ch的第i位设为value: value只能取1和0,1=i=8 unsigned char SetBit(unsigned char ch, int i,int value)unsigned char c1,c0;c1=(0x01(i-1); /将0x00的第i位设为1,左移i-1位c0=c10xFF; /位的异或运算:将0xFF的第i位设为0/或改为c0=c1;if (value=0) ch=ch ,仅用一个函数来改变某一位的值
7、,用Huffman编码对二进制文件压缩的问题,二进制文件的字符最多256种,其ASCII码:0255。那么,计算文件中各个字符的频数就可以用一个有256个元素的整数数组来实现:字符的频数存放在下标为其ASCII码的数组元素中,计算频数的程序如下:int freq256; /字符频数int i;char c; for(i=0;i256;i+) freqi=0; /初始化字符频数数组 ifstream fi(“实验X.ppt“, ios:binary); while(fi.get(c)i=int(unsigned char)c);freqi+; fi.close();,压缩文件的解压步骤,首先从压
8、缩文件中读出能构造Huffman树的信息; 构造Huffman树(见下图); 读入压缩数据,在读的过程中解码: 读入一个字节,顺序测试它的每一位,若为0,则向左走,为1,向右走,例如下图为Huffman树,读入字符各位为10101101,则解码为AABC,测试一个字节每一位的函数在这里是关键,位操作获取某一位的值,int getBit(unsigned char c, int n) /获取c的第n位的值 switch(n)case 1:return 0!=(c ,压缩文件的解压步骤中的几个关键问题,在压缩文件中保存什么样的信息能方便地保存和构造Huffman树? 这些信息保存在压缩文件的什么位
9、置?或者说压缩文件有什么样的结构? 为了信息访问方便,应以什么形式保存?,压缩文件的解压步骤中的几个关键问题(续1),在压缩文件中保存什么样的信息能方便地保存和构造Huffman树? 我们应当保存Huffman编码或Huffman树或字符频数中的哪一个? 建议保存各个字符的频数。这样所占磁盘空间较小并基本固定,也同样可以方便的构造Huffman树。如果保存编码,由于编码不等长,不易读出也不易构造树,所占空间可能较大。直接保存H树,占用较大的磁盘空间。,压缩文件的解压步骤中的几个关键问题(续2),这些信息保存在压缩文件的什么位置?或者说压缩文件有什么样的结构?,各字符频数,最后一个字节的有效位数
10、,压缩的文件内容,由于文件经Huffman 编码后,其位数不一定是8的倍数,但磁盘只能存一个字节,最后几个bit必须用0添至8位,因此需要记住其有效位数!,压缩文件的解压步骤中的几个关键问题(续3),为了信息访问方便,应以什么形式保存各字符及其频数? 最佳方案:字符及其频数等长保存。 如果这样,字符不必专门保存,只需在磁盘数组中保存频数即可。实际上,数组下标即是字符的ASCII。 如何等长保存一个整数?,如何等长保存一个整数? 将一个整数存入文件的不同方法比较,#include #include void main()int i; cout“一个整数所占字节数“sizeof(i)endl;co
11、utendl;,下面用两种方法将一个整数i存放到二进制文件中,并比较不同效果,i=100000;/第1种方法ofstream first(“first.1“, ios:binary);firstn;cout“第1种方法存放的数再取出,其值“nendlendl;getfirst.close();,/第2种方法 int *p=new int;char * c;*p=i;c=(char *)p;ofstream second(“second.2“, ios:binary);second.write(c, sizeof(int);second.close();/检查第2种方法存放的数据*p=0;ifs
12、tream getSecond(“second.2“, ios:binary);getSecond.read(c, sizeof(int);p=(int*)c;cout“第2种方法存放的数再取出,其值” *pendl;getSecond.close();,比较,文件first.1的文件大小:6字节,文件second.2的文件大小:4字节 100000的二进制表示11000011010100000 十六进制表示0186A0,将一百万个整数存入磁盘应采用哪种方法?,采用第一种方法,数的大小不同位数就不同,在磁盘所占的字节数也不同(所占字节数位数):一位数9占一个字节,999占三个字节,每个数所占字
13、节数不固定。因此,数和数之间要用分隔符隔开(如,或空格),访问第k个数时需要从第一个数开始顺序访问。空间和时间效率低。 采用第二种方法,每个数所占字节数固定为4,容易计算每个数的存放地址后直接访问(不必用分隔符隔开)。如,第k个数的地址为4k。,结论,如果我们希望非常方便地计算一个整数在文件中的存储位置并进行直接存取操作,那么采用第二种方法(二进制表示)来存放整数比较好。因为第二种方法的每个整数固定占4个字节,利于位置计算也节省空间。,压缩文件的结构,一个整数占4个字节,因此文件头存放各个字符的频数时,需要4*256=1024字节的空间。 ASCII为100的字符频数在4100的位置存放(用4
14、个字节)。,各字符频数,共占1KB空间,最后一个字节的有效位数,压缩的文件内容,由于文件经Huffman 编码后,其位数不一定是8的倍数,但磁盘只能存一个字节,最后几个bit必须用0添至8位,因此需要记住其有效位数!,占1B,也可以存在第一个字节处,计算文件中各字符出现的频数,#include #include void main()int freq256; /字符频数int i;int file_size=0; /文件大小char c;,计算文件中各字符出现的频数(续2),for(i=0;i256;i+) freqi=0; /初始化字符频数数组ifstream fi(“实验X.ppt“, i
15、os:binary);while(fi.get(c)i=int(unsigned char)c);freqi+; file_size+;fi.close();,保存字符及其频数,int *iptr = new int;ofstream fo(“实验X.HFM“, ios:binary);/频数(4个字节),所有字符的频数所占的字节数256*4/存放字符的频数for(i=0;i256;i+) *iptr=freqi;fo.write(char *)iptr,sizeof(int);/保留最后字节的有效位数(也可以存放在文件的第一个字节)fo.put(0x00); ,文件I/O流,在程序中使用文件
16、时,首先要包含#include 命令。由它提供的输入文件流类ifstream、输出文件流类ofstream和输入输出文件流类fstream定义用户所需要的文件流对象。 利用文件流对象调用相应类中的open成员函数,按照一定的打开方式打开一个文件。 文件被打开后,就可以通过流对象访问它。 访问结束后再通过流对象关闭它。,文件指针,对于每个打开的文件,都存在着一个文件指针。 初始指向一个隐含的位置,该位置由具体打开方式决定。 每次对文件写入或读出信息都是从当前文件指针所指的位置开始的,当写入或读出若干个字节后,文件指针就后移相应多个字节。 当文件指针移动到最后,读出的是文件结束符时,则将使流对象调
17、用eof()成员函数返回非0值(通常为1),当然读出的是文件内容时将返回0。 文件结束符占有一个字节,其值为-1,在ios类中把EOF常量定义为-1。 若利用字符变量依次读取字符文件中的每个字符,当读取到的字符等于文件结束符EOF时则表示文件访问结束。,文件指针定位,seekg()是移动读指针,seekp()是移动写指针。 #include #include int main ( ) using namespace std; ifstream file; char c, c1; file.open( “basic_istream_seekg.txt“ );file.seekg(2); / ch
18、ars to skip file c; cout c; cout c1; cout c1 endl; ,文件指针定位(续),seekg()是移动读指针,seekp()是移动写指针。 #include #include int main() using namespace std;ofstream x(“basic_ostream_seekp.txt“); streamoff i = x.tellp(); cout i endl; x “testing“; i = x.tellp(); cout i endl; x.seekp(2); / Put char in third char posit
19、ion in file x “ “; x.seekp(2, ios:end); / Put char two after end of file x “z“; ,文件流对象和打开文件的例子,(2) ifstream fin;fin.open(“a:wr.dat”, ios:in | ios:nocreate); 定义了一个输入文件流对象fin,并使其在内存中得到一个文件缓冲区; 然后打开a盘上的wr.dat文件,并规定以输入方式进行访问; 若该文件不存在则不建立新文件,使打开该文件的操作失败,此时由fin带回0值,由(!fin)是否为真判断打开是否失败。 相当于: ifstream fin(“
20、a:wr.dat”, ios:in | ios:nocreate);,文件流对象和打开文件的例子,(3) ofstream ofs;ofs.open(“a:xxk.dat”, ios:app); 定义一个输出文件流对象ofs,在内存中得到一个文件缓冲区; 打开a盘上已存在的xxk.dat文件,并规定以追加数据的方式访问,即不破坏原有文件中的内容,只允许向尾部写入新的数据。 相当于:ofstream ofs(“a:xxk.dat”, ios:app); (4) fstream fio;fio.open(“a:abc.ran”, ios:in | ios:out | ios:binary); 定义一个输入输出文件流对象fio,在内存中得到一个文件缓冲区; 按输入和输出方式打开a盘上的abc.ran二进制文件, 既可以按字节向该文件写入信息,又可以从该文件读出信息。 相当于: fstream ofs(a:abc.ran”, ios:in | ios:out | ios:binary);,关闭流对象所对应的文件,流对象调用close()成员函数即可。 如要关闭fout流所对应的a:xxk.dat文件,则关闭语句为:fout.close();,