再上一篇:14.2 C++的基本流类体系
上一篇:14.3 标准设备的输入/输出
主页
下一篇:第十五章 MFC程序设计基础
再下一篇:15.2 文档与视图结构
文章列表

14.4 文件流

《VC++程序设计基础》,讲述C++语言的语法和标准库,以及Visual C++ 函数库和MFC类库的使用,并附相关代码示例。

在C++中,“文件”有两种含义,一种是指一个具体的外部设备,如可以把打印机看作一个文件,也可以把显示器看作一个文件;另一种是指一个磁盘文件。本节讨论磁盘文件的建立、打开、读写和关闭等操作。

14.4.1 C++文件概述

文件是一组有序的数据集合。文件通常存放在磁盘上,每一个文件有一个文件名。文件名通常由字母开头的字母数字序列所组成。在不同的计算机系统中文件名的组成规则是不同的。

在C++语言中,根据组成文件实体的数据格式,可将文件分为二种:一种是二进制文件,它包含了二进制数据;另一种是文本文件(也称为ASCII码文件),它由字符序列组成。

使用文件的方法基本上是统一的,首先打开一个文件,然后从文件中读取数据或将数据写入到文件中。当以某一种形式的数据写入到一个文件后;再从该文件中读取数据时,只有按写入的方式依次读取数据,读取的数据才是正确的,否则读取的数据是不正确的。换言之,从文件中读取数据的类型是否正确,系统是无法检查的。由读写文件的数据类型不正确造成的错误,C++编译程序是不管的,程序设计者必须保证从文件中依次取出的数据与原先写入的数据类型一致。最后,当不再使用文件时,要关闭文件。

C++通过对标准输入/输出流类的进一步扩展,提供了很强的文件处理能力,使得程序设计者在建立和使用文件时,就像使用cin和cout一样方便。14.4.2 C++的文件流类体系

ios streambuf

istream ostream filebuf

ifstream ofstream

iostream

fstream

图14-2 C++预定义的文件流类体系

C++在头文件fstream.h中定义了C++的文件流类体系,其体系结构如图14-2所示。当程序中使用文件时,要包含头文件fstream.h。

在文件流类体系中,类filebuf用于管理文件的缓冲区,应用程序中一般不涉及该类。类ofstream由类ostream公有派生而来,它实现把数据写入到文件中的各种操作。类ifstream由类 istream公有派生而来,它支持从输入文件中提取数据的各种操作。类iostream由类istream和类ostream公有派生而来,实现数据的输入和输出。类fstream由类iostream公有派生而来,它提供从文件中提取数据或把数据写入文件的各种操作。

14.4.3文件的打开与关闭

在C++中,对文件的操作与对键盘和显示器的输入/输出不一样,并没有预定义的文件流类供直接使用。要使用一个文件流时,必须在程序中先打开一个文件,其目的是将一个文件流类与某一个磁盘文件联系起来;其后,使用文件流类提供的成员函数,将数据写入到文件中或从文件中读取数据;当不再使用该文件流时,关闭已打开的文件,将该磁盘文件与文件流类已建立的关系脱离。C++中使用文件的方法可概括为以下几点。

(1)说明一个文件流对象。它只能是类ifstream、ofstream或fstream的对象。例如:

ifstream infile;

ofstream outfile;

fstream iofile;

(2)使用文件流类的成员函数或者构造函数,打开一个文件。打开文件的作用是在文件流对象与要使用的文件名之间建立联系。例如:

infile.open("myfile1.txt");

outfile.open("myfile2.txt");

(3)使用提取运算符、插入运算符或成员函数对文件进行读写操作。例如:

infile>>ch;

(4)用完文件后,使用成员函数关闭文件。例如:

infile.close();

下面先讨论文件的打开和关闭。

1.打开文件

在文件流类体系中说明了以下三个打开文件的成员函数,它们分别对应于输入文件流、输出文件流和输入/输出文件流:

void ifstream ::open(const char *, int =ios::in, int = filebuf::openprot);

void ofstream ::open(const char *, int =ios::out, int = filebuf::openprot);

void fstream : :open(const char *, int, int = filebuf::openprot);

其中第一个参数为要打开文件的文件名或文件的全路径名;第二个参数指定打开文件的方式,输入文件流的缺省值ios::in为按输入文件方式打开文件,输出文件流的缺省值ios::out为按输出文件方式打开文件;第三个参数指定打开文件时的保护方式,该参数与具体的操作系统有关,一般情况下只要使用缺省值filebuf::openprot,而不要给出实参。

在头文件ios.h中,定义了文件打开方式的公有枚举类型:

enum open_mode {

in = 0x01, //按读方式打开文件

out = 0x02, //按写方式打开文件

ate = 0x04, //打开文件时,将指针移到文件的尾处

app = 0x08, //按增补方式打开文件

trunc = 0x10, //将文件的长度截为0,并清除文件原有内容

nocreate = 0x20, //打开已存在的文件

noreplace = 0x40, //A

binary = 0x80 //打开二进制文件

};

显然,每一种打开方式是以一个二进位来表示的,所以可以用运算符 “|”(二进制按位或),将允许的几种打开方式组合起来使用。

以in方式打开的文件,只能从文件中读取数据。以out方式打开的文件,只能将数据写入文件中。单独用该方式打开文件时,若文件不存在,则产生一个空文件;若文件存在,则先删除文件的内容,使其成为一个空文件(相当于先删除该文件,再产生一个空文件)。ate方式不能单独使用,要与in、out或noreplace同时使用,例如,out | ate,其作用是在文件打开时,将文件指针移到文件的结尾处,文件中原来的内容不变,向文件中写入的数据增加到文件中。app是以写方式打开文件,当文件存在时,它等同于out | ate;而当文件不存在时,它等同于out。以trunc方式打开文件时,若单独使用,则与out打开文件相同。 以nocreate方式打开文件时,若文件不存在时,则打开文件的操作失败,即打开不成功;通常这种方式不单独使用,它总是与读或写方式同时使用,但它不能与noreplace同时使用。noreplace通常用来创建一个新文件,这种方式也不单独使用,总是与写方式同时使用。若与ate或app同时使用时,也可以打开一个已存在的文件。不以binary方式打开的文件,都是文本文件,只有明确指定以binary方式打开的文件,才是二进制文件,它总是与读或写方式同时使用。 根据上面介绍的打开文件方式,并结合上面打开文件的三个成员函数的原型可知,ifstream的成员函数open缺省的打开方式为读文件方式;ofstream的成员函数open缺省的打开方式为写文件方式;fstream的成员函数open没有缺省的打开方式,在使用该成员函数打开文件时,必须指明打开文件的方式。例如:

fstream file;

file.open("myfile.txt", ios::in | ios::out );

表示以输入/输出方式打开文本文件myfile.txt。

以上三个文件流类中都重载了相应的构造函数:

ifstream :: ifstream (const char *, int =ios::in, int = filebuf::openprot);

ofstream :: ofstream (const char *, int =ios::out, int = filebuf::openprot);

fstream :: fstream (const char *, int, int = filebuf::openprot);

由构造函数的原型可知,它们所带的参数与各自的成员函数open所带的参数完全相同。因此,在说明这三种文件流类的对象时,通过调用各自的构造函数,也能打开文件。例如:

ifstream f1("file.dat");

ofstream f2("fileo.txt");

fstream f3("file2.dat",ios::in);

以上三个语句调用各自的构造函数,分别以读方式打开磁盘文件file.dat,以写方式打开文件fileo.txt和以读方式打开文件file2.dat。因此语句

ifstream f1("file.dat");

的作用等同于以下二个语句:

ifstream f1;

f1. open("file.dat");

通常,不论是调用成员函数open来打开文件,还是用构造函数来打开文件,打开后都要判断打开是否成功。若打开成功,则文件流对象值为非零值;否则其值为0。为此,打开文件的格式为:

ifstream f1("file.dat");

if (!f1) {

cout <<"不能打开输入文件:"<<"file.dat"<<'\n';

exit(1);

}

char filename[256];

cout <<"输入文件名:";

cin>> filename;

ifstream f5;

f5.open(filename,ios::in | ios::nocreate);

if (!f5) {

cout <<"不能打开输入文件:"<

exit(1);

}

注意,打开输入文件时,若指定 ios::nocreate,则文件不存在时打开失败;否则若文件不存在,仍产生一个空的输入文件。

2.关闭文件

打开文件后,对文件进行的读或写操作做完后,应该调用文件流的成员函数来关闭相应的文件。尽管在程序执行结束时,或在撤消文件流的对象时,由系统自动关闭仍打开的文件,但在用完文件后,仍应立即关闭相应的文件。理由如下:首先,当打开一个文件时,系统要为打开的文件分配一定的资源,如缓冲区等,在关闭文件时,系统就收回了该文件所占用的相应资源;第二,一个文件流类的对象在任何时候只能与一个文件建立联系,通过关闭文件,就可使得一个文件流类的对象可与多个文件建立联系;第三,在任何操作系统下执行C++程序时,允许同时打开的文件数是限定的。例如,在UNIX操作系统下允许同时打开的文件数为64个。

与打开文件相对应,这三个文件流类各有一个关闭文件的成员函数:

void istream::close();

void oftream::close();

void fstream::close();

这三个成员函数都没有参数,用法完全相同。例如:

istream infile("f1.dat");

......

inflile.close(); //关闭文件f1.dat

关闭文件时,系统把与该文件相关联的内存缓冲区中的数据写到文件中,收回与该文件相关的内存空间,把文件名与文件对象之间建立的关联断开。

应该说明的是,当一个文件流类的对象通过打开文件函数,建立起文件名与该对象间的联系后,就可以对文件进行读或写操作,而一旦关闭文件后,文件流对象与文件名之间所建立的联系就断开了,不能再对该文件进行读或写操作。如果要再次使用该文件,必须重新打开文件。

14.4.4文本文件的使用

文件流类ifstream、ofstream和fstream并没有直接定义文件操作的成员函数,对文件的操作是通过调用其基类ios、istream、ostream中说明的成员函数来实现的。采用这种方式的明显好处是,对文件的基本操作与标准输入/输出流的使用方式相同,可通过提取运算符>>和插入运算符<<来读写文件。

例14.10 使用构造函数打开文件,并把源程序文件拷贝到目的文件中。

分析:先打开源文件和目的文件,依次从源文件中读取一个字节,并把所读的字节写入目的文件中,直到把源文件中的所有字节读写完为止。程序如下:

#include

#include

#include

void main(void )

{

char filename1[256],filename2[256];

cout <<"输入源文件名:";

cin >>filename1;

cout <<"输入目的文件名:";

cin >>filename2;

ifstream infile(filename1,ios::in | ios::nocreate); //按文本文件方式打开

ofstream outfile(filename2); //按文本文件方式打开

if (!infile ) {

cout << "不能打开输入文件:"<

exit(1);

}

if (!outfile ) {

cout << "不能打开目的文件:"<

exit(2);

}

infile.unsetf(ios::skipws); //A

char ch;

while (infile >> ch) //B

outfile <

infile.close();

outfile.close();

}

程序首先要求输入源程序文件名和目的文件名,然后把源程序文件中的内容依次拷贝到目的文件中。A行设置为不要跳过文件中的空格。在缺省的情况下,提取运算符是跳过空格的,而文件的拷贝必须连同空格一起拷贝。从上例可以看出,对于文本文件的读写与标准输入/输出流cin和cout的用法是相同的。

B行依次从源文件中取一个字符,C行将取到的字符写到目的文件中。当到达源文件的结束位置时(无数据可取),infile >> ch的返回值为0,结束循环;否则其返回值不为0,继续循环。

实际上,该程序能正确拷贝任意类型的文件,在拷贝二进制文件时,只要逐个字节拷贝(每一个字节作为一个字符来处理)即可。

例14.11 使用构造函数打开文件,使用成员函数来实现文件的拷贝。

#include

#include

#include

void main(void )

{

char filename1[256],filename2[256];

cout <<"输入源文件名:";

cin >>filename1;

cout <<"输入目的文件名:";

cin >>filename2;

ifstream infile(filename1, ios::in | ios::nocreate);

ofstream outfile(filename2);

if (!infile ) {

cout << "不能打开输入文件:"<

exit(1);

}

if (!outfile ) {

cout << "不能打开目的文件:"<

exit(2);

}

char ch;

while (infile.get(ch)) //C

outfile.put(ch); //D

infile.close();

outfile.close();

}

在该程序中没有例14.10中的语句:

infile.unsetf(ios::skipws);

因用成员函数读取字符时是不跳过空格的。C行中的infile.get(ch),完成从源文件中取出一个字符到ch中,D行将ch中的字符写到目的文件中。当到达源文件结束位置时,infile.get(ch)的返回值为0,否则返回值不为0。当到达源文件结束位置时,结束拷贝。

这个程序也能实现任意类型文件的拷贝。

例14.12 使用成员函数打开文件,并实现文件的拷贝。

#include

#include

#include

void main(void )

{

char filename1[256],filename2[256];

char buff[300];

cout <<"输入源文件名:";

cin >>filename1;

cout <<"输入目的文件名:";

cin >>filename2;

fstream infile,outfile;

infile.open(filename1,ios::in | ios::nocreate);

outfile.open(filename2,ios::out);

if (!infile ) {

cout << "不能打开输入文件:"<

exit(1);

}

if (!outfile ) {

cout << "不能打开目的文件:"<

exit(2);

}

while (infile.getline(buff,300)) //D

outfile<

infile.close();

outfile.close();

}

D行中的infile.getline(buff,300)从源文件中读取一行字符,F行将读取的一行字符写到目的文件中。同样地,到达源文件结束位置时,infile.getline的返回值为0,表明拷贝结束;否则返回值不为0,表示要继续拷贝。F行中插入字符'\n'是必要的,因D行从源文件中读取一行时,换行符取出来后,并不放入buff中,所以写入目的文件中时,要加入一个换行符。

该程序只能实现文本文件的拷贝,不能实现二进制文件的拷贝。在拷贝文本文件时,效率要比前两个程序高一些,因前两个程序都是逐个字符拷贝的,而该程序是逐行拷贝的。

例14.13 设文本文件data.txt中有若干个实数,每一个实数之间用空格或换行符隔开。求出文件中的这些实数的平均值。

分析:各设一个计数器和累加器,每从文件中读取一个实数时,计数器加1,并把该数加到累加器中,直到把文件中的数据读完为止。把累加器的值除以计数器的值,得到平均值。程序如下:

#include

#include

#include

void main(void )

{

ifstream infile("data.txt",ios::in | ios::nocreate);

if (!infile ) {

cout << "不能打开输入文件:\n";

exit(1);

}

float sum=0,temp;

int count=0;

while (infile>>temp){ //依次读一个实数

sum+=temp;

count++;

}

cout<<"平均值="<

if (!outfile ) {

cout << "不能打开目的文件data.dat\n";

exit(1);

}

int i;

for(i=2;i< 500;i+=2 )

outfile.write((char*)&i,sizeof(int)); //B

outfile.close();

}

A行指定按二进制方式打开目的文件data.dat。在B行,须将整数的地址强制转换成字符指针,因为该函数的第一个参数为字符型指针。从二进制数据文件中读取非字符类型的数据(如整型、实型或导出数据类型)时,均要作类似的强制转换。

例14.15 从例14.14中产生的数据文件data.dat中读取二进制数据,并在显示器上按每行10个数的形式显示。

#include

#include

void main(void )

{

ifstream infile("data.dat",ios::in | ios::binary | ios::nocreate); //A

if (!infile ) {

cout << "不能打开目的文件data.dat\n";

exit(1);

}

int i,a[250];

infile.read((char *)a,sizeof(int)*249); //B

for(i=0;i< 249;i++ ){

cout<

if( (i+1) %10 ==0 ) cout<<'\n';

}

cout<<'\n';

infile.close();

}

A行指定按输入文件方式打开二进制文件data.dat。当知道文件中整数的个数时,可以一次把文件中的所有数据全部读出,如B行中一次从文件data.dat中读取249个整数。

例14.16 把0到90度的sin函数值写到二进制文件SIN.BIN中。

#include

#include

#include

void main (void )

{

fstream f1("SIN.BIN",ios::out | ios::binary);

int i ;

if(!f1){

cout << "不能产生输出文件SIN.BIN\n";

exit(1);

}

double s[91];

for ( i=0;i<=90;i++) s[i]=sin(i*3.1415926/180);

for (i=0;i<=90;i++) cout << s[i]<<'\n';

f1.write((char *)s,sizeof(double)*91); //一次写入91个实数

f1.close();

}

使用读写二进制数据的成员函数,由于一次读写的字节数可以很大,这样可减少文件的输入/输出次数,从而提高了对文件进行操作的速度。

例14.17 使用成员函数read和write来实现文件的拷贝。

#include

#include

void main(void )

{

char filename1[256],filename2[256];

char buff[4096];

cout <<"输入源文件名:";

cin >>filename1;

cout <<"输入目的文件名:";

cin >>filename2;

fstream infile,outfile;

infile.open(filename1,ios::in | ios::binary | ios::nocreate);

outfile.open(filename2,ios::out | ios::binary);

if (!infile ) {

cout << "不能打开输入文件:"<

exit(1);

}

if (!outfile ) {

cout << "不能打开目的文件:"<

exit(2);

}

int n;

while (!infile.eof()){ //文件不结束,继续循环

infile.read(buff,4096); //一次读4096个字节

n=infile.gcount(); //取实际读的字节数

outfile.write(buff,n); //按实际读的字节数写入文件

}

infile.close();

outfile.close();

}

该程序可以实现任意文件类型的拷贝,包括文本文件,数据文件或执行文件等。在while循环中,使用函数eof来判断是否已到达文件的结尾。由于从源文件中最后一次读取的数据不一定正好是4096个字节,所以使用函数gcount来获得实际读入的字节数,并按实际读的字节数写到目的文件中。

2. 随机访问文件的函数

前面介绍的文件读写操作,都是依次按存放在文件中信息的先后顺序来进行读写的。在打开文件时,系统为打开的文件建立一个长整数变量(设变量名为point),它的初值为0。文件的内容可以看成是由若干个有序的字节所组成,依次给每一个字节从0开始顺序编号。当从文件中读取n个字节时,则系统修改point的值为point+=n。每一次从文件中读取数据时,均从第point个字节开始读取,读完后修改point的值。显然,可以将point的值看成是指向文件内容的一个指针,它指向每一次开始读取数据的开始位置。每一次把数据写入文件时,都要修改point的值,使它指向文件尾,如图14-3所示。图14-3(a)假定在文件操作期间的某一时刻point指向文件内容的位置,在从文件中读取n个字节后,指针后移n个字节,如图14-3(b)所示。

文件内容 文件内容

0

point

n point

(a) 读前 (b)读后

图14-3 文件指针移动示意图

在 C++中也允许从文件中的任何位置开始进行读或写数据,这种读写称为文件的随机访问。在文件流类的基类中定义了几个支持文件随机访问的成员函数,它们是:

istream& istream ::seekg(streampos);

istream& istream ::seekg(streamoff,ios::seek_dir);

streampos istream ::tellg( );

ostream& ostream ::seekp(streampos);

ostream& ostream ::seekp(streamoff,ios::seek_dir);

streampos ostream ::tellp( );

其中,streampos和streamoff等同于类型long,而seek_dir在类ios中定义为一个公有的枚举类型:

enum seek_dir {

beg=0, //文件开始处作为参考点

cur=1, //文件当前位置作为参考点

end=2 //文件结束处作为参考点

};

函数名中的g是get的缩写,表示要移动输入流文件的指针;而p是put的缩写,表示要移动输出流文件的指针。四个 seek函数都是用来移动文件流中的文件指针位置。函数seekg(streampos)和 seekp(streampos)都是将文件指针移动到由参数所指定的字节处。函数seekg(streamoff,ios::seek_dir)和seekp(streamoff, ios::seek_dir)是根据第二个参数的值来确定移动文件指针的方向。其值若为ios::beg,则将第一个参数值作为文件指针的值;若为ios::cur,则将文件指针的当前值加上第一个参数值的和作为文件指针的值;若为ios::end,则将文件尾的字节编号值加上第一个参数值的和作为文件指针的值。设按输入方式打开了二进制文件流对象f,移动文件指针的例子为:

f.seekg(-50,ios::cur); //当前文件指针值前移50个字节

f.seekg(50,ios::cur); //当前文件指针值后移50个字节

f.seekg(-50,ios::end); //若文件尾的编号为5000,则文件指针移到4950处

注意,在移动文件指针时,必须保证移动后的指针值大于等于 0且小于等于文件尾字节编号,否则将导致接着的读/写数据不正确。

函数tellg和tellp分别返回输入文件流和输出文件流的当前文件指针值。

例14.18 产生一个5~1000之间的奇数文件(二进制文件),将文件中的第20~29之间的数依次读出并输出。

#include

#include

void main(void )

{

ofstream outfile("data.dat",ios::out| ios::binary); //按输出方式打开文件

if (!outfile ) {

cout << "不能打开目的文件data.dat\n";

exit(1);

}

int i;

for(i=5;i< 1000;i+=2 )

outfile.write((char*)&i,sizeof(int)); //将奇数写入文件

outfile.close(); //关闭文件

ifstream f1("data.dat",ios::in| ios::binary); //按输入方式重新打开文件

if (!f1 ) {

cout << "不能打开目的文件data.dat\n";

exit(1);

}

int x;

f1.seekg(20*sizeof(int)); //将文件指针移到第20个整数的位置

for(i=0;i<10;i++){

f1.read((char *)&x,sizeof(int)); //依次读出第20~29个奇数

cout<< x<< '\t';

}

f1.close();

}

随机文件的读写实际上是通过二步来实现的:第一步是将文件指针移到要开始读写的位置;第二步再用前面已介绍的文件读写函数进行读或写操作。

下面再举几个使用以上函数的例子,假定已成功地打开了输入流对象infile和输出流对象outfile:

infile.seekg(-100,ios::cur); //文件指针从当前位置前移100个字节

infile.seekg(100,ios::cur); //文件指针从当前位置后移100个字节

outfile.seek(-100,ios::end); //文件指针从文件尾开始向前移100个字节

infile.seekg(500); //文件指针移到第500个字节处

注:当文件指针值从大向小方向的移动称为前移,反之称为后移。

练习题

1.标准流cerr和clog的作用是什么?这两个流有何异同?

2.设计一个程序,实现整数的八进制、十进制、十六进制的输入和输出,并实现实数的指数格式和定点数格式的输入和输出。

3.设计一个程序,实现整数、实数、字符和字符串的输入和输出,当输入的数据不正确时,要进行流的错误处理,要求重新输入数据,直到输入正确为止。

4.重载提取和插入运算符,实现对象的输入和输出。

5.编写产生一个文本文件的程序(依次接收输入行,并将输入行送到输出文件中)。要求使用成员函数实现文件的打开和关闭。

6.设计一个通用的实现二进制文件的拷贝程序。源程序文件名和目的文件名均从键盘输入,且可包含文件的相对路径名或全路径名。要求使用构造函数打开文件。注:当输入的一个文件名为“test\abc.exe”,要将这文件名转换为“test\\abc.exe”或“ test/abc.exe”(因为C++把字符“\”作为一个转义字符,而操作系统将它作为分隔符)。

7.求出2~1000之间的所有素数,将求出的素数分别送到文本文件PRIME.TXT和二进制文件 PRIME.DAT中。送到文本文件中的结果,要求以表格形式输出,每一行输出五个素数,每一个数占用10个字符宽度。

8.用编辑程序产生一个包含若干个实数的文本文件。编写一个程序,从该文本文件中依次读取每一个数据,求出这批数据的平均值和实数的个数。

9.把从键盘上输入的4×4矩阵(二维数组)送到二进制文件data.dat中。然后从该数据文件中读数据,并送至4×4矩阵中。将该矩阵转置后,输出到文本文件data.txt中。

10.对于第7题中产生的二进制文件PRIME.DAT,输出其中第20~30个素数。要求通过移动文件的指针来实现文件的随机存取。