第二十三讲:二进制文件的操作与字符串流

来源:互联网 发布:无需网络的游戏大全 编辑:程序博客网 时间:2024/04/28 23:05

第二十三讲:二进制文件的操作与字符串流

    * 掌握:二进制文件读写、显示操作;字符流在C++程序设计中的应用。
    * 理解:文件流与字符串流的区别。
重点、难点
    ◆二进制文件读写、显示操作;字符流在C++程序设计中的应用。

一、对二进制文件的操作

前面已经介绍,二进制文件不是以ASCII代码存放数据的,它将内存中数据存储形式不加转换地传送到磁盘文件,因此它又称为内存数据的映像文件。因为文件中的信息不是字符数据,而是字节中的二进制形式的信息,因此它又称为字节文件。
对二进制文件的操作也需要先打开文件,用完后要关闭文件。在打开时要用ios::binary指定为以二进制形式传送和存储。二进制文件除了可以作为输入文件或输出文件外,还可以是既能输入又能输出的文件。这是和ASCII文件不同的地方。

1.用成员函数read和write读写二进制文件
对二进制文件的读写主要用lstream类的成员函数read和write来实现。这两个成员函数的原型为
istream&read(char *buffer,inllea);
ostream&write(coastchar*buffer,intlen);
字符指针buffer指向内存中一段存储空间。len是读写的字节数。调用的方式为
a.write(pl,50);
b.read(p2,30);
上面第一行中的a是输出文件流对象,wrtte函数将字符指针Pl所给出的地址开始的50个字节的内容不加转换地写到磁盘文件中。在第二行中,b是输入文件流对象,read函数从b所关联的磁盘文件中,读人30个字节(或遇EOF结束),存放在字符指针p2所指的一段空间内。

例14 将一批数据以二进制形式存放在磁盘文件中。
可以写出下面的程序:
#include <fstream>
using namespace std;
struct student
{char name[20];
int num;
int age;
char sex;
};
int main()
{student stud[3]={"Li",1001,18,'f',"Fun",1002,19,'m',"Wang",1004,17,'f'};
ofstream outfile("stud.dat",ios::binary);
if(!outfile)
{cerr<<"open error!"<<endl;
abort(); //退出程序
}
for(int i=0;i<3;i++)
outfile.write((char *)&stud[i],sizeof(stud[i]));
outfile.close();
return 0;}
定义了结构体类型student,它包括4个成员。用student定义结构体数组stud,并对其初始化。建立输出文件流对象outfile,打开磁盘文件stud.dat(如果原来无此文件,则建立新文件,如果已有同名文件则将其原有内容删除,以便重新写人数据)。将stud.dat文件的工作方式定为二进制文件。
用成员函数write向stud.dat输出数据,从前面给出的write函数的原型可以看出:第1个形参是指向char型常变量的指针变量buffer,之所以用const声明,是因为不允许通过指针改变其指向数据的值。形参要求相应的实参是字符指针或字符串的首地址。现在要将结构体数组的一个元素(包含4个成员)一次输㈩到磁盘文件stud.dat。&stud[i]是结构体数组第i个元素的首地址,但这是指向结构体的指针,与形参类型不匹配。因此要用(char。)把它强制转换为字符指针。第2个参数是指定一次输出的字节数。sizeof (stud[i])的值是结构体数组的一个元素的字节数。调用一次write函数,就将从&stud[¨开始的结构体数组的一个元素输出到磁盘文件中,执行3次循环输出结构休数组的3个元素。
其实可以一次输出结构体数组的3个元素,将for循环的两行政为以下一行:
outfile.write((char*)&stud[O],sizeof(stud));
执行一次write函数即输出了结构体数组的全部数据。
abort函数的作用是退出程序,与exit函数的作用相同。
可以看到,用这种方法一次可以输出一批数据,效率较高。在输出的数据之间不必加入空格,在一次输出之后也不必加回车换行符。在以后从该文件读人数据时不是靠空格作为数据的间隔,而是用字节数来控制。

例15 将刚才以二进制形式存放在磁盘文件中的数据读人内存并在显示器上显示。
#include <fstream>
using namespace std;
struct student
{char name[20];
int num;
int age;
char sex; };
int main()
{student stud[3];
int i;
ifstream infile("stud.dat",ios::binary);
if(!infile)
{cerr<<"open error!"<<endl;
abort(); }
for(i=0;i<3;i++)
infile.read((char*)&stud[i],sizeof(stud[i]));
infile.close();
for(i=0;i<3;i++)
{cout<<"NO."<<i+1<<endl;
cout<<"name:"<<stud[i].name<<endl;
cout<<"num:"<<stud[i].num<<endl;;
cout<<"age:"<<stud[i].age<<endl;
cout<<"sex:"<<stud[i].sex<<endl<<endl; }
return 0; }

运行时在显示器上显示:
NO.1
name:Li
num:1001
age:18
sex:T
NO.2
name:Fun
num:1001
age:19
sex:m
NO.3
name:Wang
num:1004
age:17
Sex:T
有了例7.14的基础,读者看懂这个程序是不会有什么困难的。
请思考:能否—次读人文件中的全部数据,如
infile.read((char*)&stud[O],sizeof(stud));
答案是可以的,将指定数目的字节读入内存,依次存放在以地址&stud[0]开始的存储空间中。要注意读人的数据的格式要与存放它的空间的格式匹配。由于磁盘文件中的数据是从内存中结构体数组元素得来的,因此它仍然保留结构体元素的数据格式。现在再读人内存,存放在同样的结构体数组中,这必然是匹配的。如果把它放到一个整型数组中,就不匹配了,会出错。

2.与文件指针有关的流成员函数
在磁盘文件中有一个文件指针,用来指明当前应进行读写的位置。在输入时每读人一个字节,指针就向后移动一个字节。在输出时每向文件输出一个字节,指针就向后移动一个字节,随着输出文件中字节不断增加,指针不断后移。对于二进制文件,允许对指针进行控制,使它按用户的意图移动到所需的位置,以便在该位置上进行渎写。文件流提供一些有关文件指针的成员函数。为了查阅方便,将它们归纳为表7,并作必要的说明。

说明:
(1)读者很容易发现:这些函数名的第一个字母或最后一个字母不是g就是p。带g的是用于输入的函数(g是get的第一个字母,以g作为输入的标识,容易理解和记忆),带p的是用于输出的函数(p是put的第一个字母,以p作为输出的标识)。例如有两个tell函数,tellg用于输人文件,tellp用于输出文件。同样,seekg用于输入文件,seekp用于输出文件。以上函数见名知意,一看就明白,不必死记。
如果是既可输入又可输出的文件,则任意用seekg或seekp。
(2)函数参数中的“文件中的位置”和“位移量”已被指定为long型整数,以字节为单位。“参照位置”可以是下面三者之一:
●ios::beg 文件开头(beg是begin的缩写),这是默认值。
●ios ::cur 指针当前的位置(cur是current的缩写)。
●ios ::end 文件末尾。
它们是在lOS类中定义的枚举常量。
举例如下:
infile.seekg(100); //输入文件中的指针向前移到100字节位置
infile.seekg(-50,ios::cur);//输入文件中的指针从当前位置后移50字节
outfile.seekp(-75,ios::end);//输出文件中的指针从文件尾后移50字节

3.随机访问二进制数据文件
一般情况下读写是顺序进行的,即逐个字节进行渎写。但是对于二进制数据文件来说,可以利用上面的成员函数移动指针,随机地访问文件中任一位置上的数据,还可以修改文件中的内容。

例16 有5个学生的数据,要求:
(1)把它们存到磁盘文件中;
(2)将磁盘文件中的第l,3,5个学生数据读人程序,并显示出来;
(3)将第3个学生的数据修改后存回磁盘文件中的原有位置;
(4)从磁盘文件读入修改后的5个学生的数据并显示出来。
要实现以上要求,需要解决3个问题:
(1)由于同一磁盘文件在程序中需要频繁地进行输入和输出,因此可将文件的工作方式指定为输入输出文件,即ios::in|ios::out|ios::binary。
(2)正确计算好每次访问时指针的定位,即正确使用seekg或seekp函数。
(3)正确进行文件中数据的重写(更新)。

可写出以下程序:
#include <fstream>
using namespace std;
struct student
{int num;
char name[20];
float score;
};
int main()
{int i;
student stud[5]={1001,"Li",85,1002,"Fun",97.5,1004,"Wang",54,
1006,"Tan",76.5,1010,"ling",96};
fstream iofile("stud.dat",ios::in|ios::out|ios::binary);
//用fstream类定义输入输出二进制文件流对象iofile
if(!iofile)
{cerr<<"open error!"<<endl;
abort(); }
for(i=0;i<5;i++) //向磁盘文件输出5个学生的数据
iofile.write((char *)&stud[i],sizeof(stud[i]));
student stud1[5]; //用来存放从磁盘文件读入的数据
for(i=0;i<5;i=i+2)
{iofile.seekg(i*sizeof(stud[i]),ios::beg); //定位于第0,2,4学生数据开头
iofile.read((char *)&stud1[i/2],sizeof(stud1[i]));
//先后读人3个学生的数据,存放在studl[o],stud[1]和stud[2]中
cout<<stud1[i/2].num<<" "<<stud1[i/2].name<<" "<<stud1[i/2].score<<endl;
//输出studl[0],stud[1)和stud[2]各成员的值
}
cout<<endl;
stud[2].num=1012; //修改第3个学生(序号为2)的数据
strcpy(stud[2].name,"Wu");
stud[2].score=60;
iofile.seekp(2*sizeof(stud[0]),ios::beg); //定位于第3个学生数据的开头
iofile.write((char *)&stud[2],sizeof(stud[2])); //更新第3个学生数据 
iofile.seekg(0,ios::beg); //重新定位于文件开头
for(i=0;i<5;i++)
{iofile.read((char *)&stud[i],sizeof(stud[i]));//读入5个学生的数据
cout<<stud[i].num<<" "<<stud[i].name<<" "<<stud[i].score<<endl; }
iofile.close();
return 0; }
运行情况如下: .
1001 Li 85 (第1个学生数据)
1004 Wang 54 (第3个学生数据)
1010 ling 96 (第5个学牛数据)
1001 Li 85 (输出修改后5个学生数据)
1002 Fun 97.5
1012 Wu 60 (巳修改的第3个学生数据)
1006 Tan 76.5
1010 ling 96
本程序也可以将磁盘文件stud.dat先后定义为输出文件和输人文件,在结束第一次的输出之后关闭该文件,然后再按输入方式打开它,输入完后再关闭它,然后再按输出方式打开,再关闭,再按输入方式打开它,输入完后再关闭。显然这是很烦琐和不方便的。
在程序中把它指定为输入输出型的二进制文件。这样,不仅可以向文件添加新的数据或读人数据,还可以修改(更新)数据。利用这些功能,可以实现比较复杂的输入输出任务,请注意,不能用ifstream或ofstream类定义输入输出的二进制文件流对象,而应当用fstream类。

二、 字符串流
文件流是以外存文件为输人输出对象的数据流,字符串流不是以外存文件为输入输出的对象,而以内存中用户定义的字符数组(字符串)为输入输出的对象,即将数据输出到内存中的字符数组,或者从字符数组(字符申)将数据读入。字符串流也称为内存流。
字符串流也有相应的缓冲区,开始时流缓冲区是空的。如果向字符数组存人数据,随着向流插入数据,流缓冲区中的数据不断增加,待缓冲区满了(或遇换行符),一起存人字符数组。如果是从字符数组读数据,先将字符数组中的数据送到流缓冲区,然后从缓冲区中提取数据赋给有关变量。
在字符数组中可以存放字符,也可以存放整数、浮点数以及其他类型的数据。在向字符数组存人数据之前,要先将数据从二进制形式转换为ASCII代码,然后存放在缓冲区,再从缓冲区送到字符数组。从字符数组读数据时,先将字符数组中的数据送到缓冲区,在赋给变量前要先将ASCII代码转换为二进制形式。总之,流缓冲区中的数据格式与字符数组相同。这种情况与以标准设备(键盘和显示器)为对象的输入输出是类似的,键盘和显示器都是按字符形式输入输出的设备,内存中的数据在输出到显示器之前,亢要转换为ASCII码形式,刀:送到输出缓冲区中。从键盘输入的数据以ASCII码形式输入到输入缓冲区,在赋给变量前转换为相应变量类型的二进制形式,然后赋给变量。对于字符串流的输入输出的情况,如不请楚,可以从对标准设备的输入输出中得到启发。
文件流类有ifstream,ofstream和fstream,而字符串流类有lstrstream,ostrstream和strgtream,类名前面几个字母str是string(字符串)的缩写。文件流类和字符串流类都是ostream,istream和iostream类的派生类,因此对它们的操作方法是基本相同的。向内存中的一个字符数组写数据就如同向文件写数据一样,但有3点不同:
(1)输出时数据不是流向外存文什,而是流向内存中的一个存储空间。输入时从内存中的存储空间读取数据。在严格的意义上说,这不属于输入输出,称为渎写比较合适。
因为输入输出一般指的是在汁算机内存与计算机外的文件(外部设备也视为文件)之间的数据传送。但由于C++的字符中流采用了C++的流输入输出机制,因此往往也用输入和输出来表述渎写操作。
(2)字符申流对象关联的不是文什,而是内存小的一个字符数组,因此不需要打开和关闭文件。
(3)每个文件的最后都有一个文件结束符,表示文什的结束。而字符串流所关联的字符数组中没有相应的结束标志,用户要自己指定一个特殊字符作为结束符,在向字符数组写入全部数据后要写入此宁符。
字符串流类没有open成员函数,因此要在建立字符串流对象时通过给定参数来确立字符中流与字符数组的关联。即通过调用构造函数来解决此问题。建立字符串流对象的法与含如下:
1.建立输出字符串流对象
ostrstream类提供的构造函数的原型为
ostrstream::ostrstream(char*buffer,iht n,int mode=ios::out);
buffer是指向字符数组首元素的指针,n为指定的流缓冲区的大小(一般选与宁符数组的大小相同,也可以不同),第3个参数足可选的,默认为lOS::out方式。可以用以卜语句建立输出字符申流对象并与字符数组建立关联:
ostrstream strout(ch],20);
作用是建立输出字符申流对象s~out,并使strout与字符数组chi关联(通过字符串流将
数据输出到字符数组chl),流缓冲区大小为20。

2.建立输入字符串流对象
istrstream类提供了两个带参的构造函数,其原型为
istrstream::istrstream(char *buffer);
istrstream::istrstream(char*buffer,intn);
buffer是指向字符数组首元素的指针,用它来初始化流对象(使流对象与字符数组建立关联)。可以用以下语句建立输入字符串流对象:
istrstream strln(ch2);
作用是建立输入字符串流对象stun,将字符数组ch2中的全部数据作为输入字符串流的内容。
istrstrcam strin(ch2.20):
流缓冲区大小为20,因此只将字符数组ch2中的前20个字符作为输入字符串流的内容。

3.建立输入输出字符串流对象
strstream类提供的构造函数的原型为
strstream::strstream(char *buffer,intn,int mode);
可以用以下语句建立输入输出字符串流对象:
strstream strio(ch3,sizeof(ch3),iOs::in门Os::out);
作用是建立输入输出字符申流对象,以字符数组ch3为输入输出对象,流缓冲区大小与数组ch3相同。
以上3个字符串流类是在头文件strstream中定义的,因此程序中在用到istrstream, ostrstream和strstream类时应包含头文件strstream。
通过下面的例子可以了解怎样使用字符串流。

例17 将一组数据保存在字符数组中。
请分析以下程序:
#include <strstream>
#include <iostream>
using namespace std;
struct student
{int num;
char name[20];
float score; };
int main()
{student stud[3]={1001,"Li",78,1002,"Wang",89.5,1004,"Fun",90};
char c[50]; //用户定义的字符数组
ostrstream strout(c,30); //建立输出字符申流,与数组c建立关联,缓冲区长30
for(int i=0;i<3;i++) //向字符数组c写3个学生的数据 
strout<<stud[i].num<<stud[i].name<<stud[i].score;
strout<<ends; //ends是C++的I/O操作符,插入一个'\O'
cout<<"array c:"<<endl<<c<<endl;//显示字符数组c中的字符
return 0; }
在程序中定义了结构体数组,包含3个学生的数据,通过字符串流strout向字符数组c写3个学生的数据。写完后再向字符数组写入操作符ends,即'\o',作为整个字符串的结束标志。最后在显示器上输出字符数组c中的字符串。
运行时在显示器上的输出如下:
array C:
1001Li781002Wang89.51004Fun90
以上就是字符数组c中的字符。可以看到:
(1)字符数组c中的数据全部是以ASCII代码形式存放的字符,而不是以二进制形式表示的数据。例如,输出结构体数组元素时并不是将内存中存放数组元素的存储单元中的数据不加转换地放到字符数组中,而是将它们转换为ASCII代码后再存放到字符数组中。
(2)在建立字符串流strout时指定流缓冲区大小为30字节,与字符数组c的大小不同,这是允许的,这时字符串流最多可以传送30个字符给字符数组c。请思考:如果将流缓冲区大小改为l0字节,即
ostrstream strout(c,l0);
运行情况会怎样?流缓冲区只能存放lo个字符,将这10个字符写到字符数组c中。运行时显示的结果是
1001Li7810
字符数组c中只有10个有效字符。一般都把流缓冲区的大小指定与字符数组的大小相同。
(3)字符数组c中的数据之间没有空格,连成一片,这是由输出的方式决定的。如果以后想将这些数据读回赋给程序中相应的变量,就会出现问题,因为无法分隔两个相邻的数据。渎者可以自己上机试一下。为解决此问题,可在输出时人为地加入空格。如
for(int i=O;i<3;i++)
stmut<<""<<smd[i].num<<""<<stud[i].name<<""<<stud[i].score;
同时应修改流缓冲区的大小,以便能容纳全部内容,今改为50字节。这样,运行时将输出
1001 Li 78 1002 Wang 89.5 1004 Fun 90
再读人时就能请楚地将数据分隔开。
例18 在一个字符数组c中存放了l0个整数,以空格相间隔,要求将它们放到整型数中,再按大小排序,然后再存放回字符数组c中。
#include <strstream>
#include <iostream>
using namespace std;
int main()
{char c[50]="12 34 65 -23 -32 33 61 99 321 32";
int a[10],i,j,t;
cout<<"array c:"<<c<<endl; //显示字符数组中的宁符串
istrstream strin(c,sizeof(c)); //建立输入中流对象strin并与字符数纽c关联
for(i=0;i<10;i++) //从字符数组c读入10个整数赋给整型数组a
strin>>a[i];
cout<<"array a:"; //显示整型数组a各元素
for(i=0;i<10;i++)
cout<<a[i]<<" "; //用起泡法对数组a排序
cout<<endl;
for(i=0;i<9;i++)
for(j=0;j<9-i;j++)
if(a[j]>a[j+1])
{t=a[j];a[j]=a[j+1];a[j+1]=t;}
ostrstream strout(c,sizeof(c));//建立输出申流对象strout并与字符数组c关联
for(i=0;i<10;i++)
strout<<a[i]<<" "; //将10个整数存放在字符数组c
strout<<ends; //加入'\0' 
cout<<"array c:"<<c<<endl; //显示字符数组c
return 0;
}
运行结果如下:
array c:12 34 65 -23 -32 33 61 99 321 32 (字符数组c原来的内容)
array a:12 24 65 -23 -32 33 61 99 321 32 (整型数组a的内容)
array c:-32-23 12 32 33 34 61 65 99 321 (字符数组c最后的内容)
可以看到:
(1)用字符串流时不需要打开和关闭文件。
(2)通过字符小流从字符数组读数据就如同从键盘读数据一样,可以从字符数组读入字符数据,也可以读人整数、浮点数或其他类型数据。如果不用宁符申流,只能从字符数组逐个访问字符,而不能按其他类型的数据形式读取数据。这是用字符串流访问字符数组的优点,使用方便灵活。
(3)程序中先后建立了两个字符串流strin和strout,与字符数组c关联。strln从字符数组c中获取数据,strout将数据传送给字符数组。分别对同一字符数组进行操作。甚至可以对字符数组交叉进行读写,输入字符串流和输出字符串流分别有流指针指示当前位置,互不干扰。
(4)用输出字符中流向字符数组c写数据时,是从数组的首地址开始的,阅此更新了数组的内容。
(5)字符串流关联的字符数组并不一定是专为字符串流而定义的数组,它与一般的字符数组无异,可以对该数组进行其他各种操作。
通过以上对字符串流的介绍,渎者可以看到:与字符串流关联的字符数组相当于内存中的临时仓库,可以用来存放各种类型的数据(以ASCII形式存放),在需要时再从中读回。它的用法相当于标准设备(显示器与键盘),但标准设备不能保存数据,而字符数组中的内容可以随时用AscII字符输出。它比外存文件使用方便,不必建立文件(不需打开与关闭),存取速度快。但它的生命周期与其所在的模块(如主函数)相同,该模块的生命周期结束后,字符数组也不存在了。因此只能作为临时的存储空间。

三、作业
1、P261:5、6题
2、实验八

 

 

 

 

 

0 0
原创粉丝点击