文件的基本概念
本节中文件指的是磁盘文件。
C++根据文件(file)内容的数据格式,可分为两类:
- 文本文件:由字符序列组成,在文本文件中存取的最小信息单位为字符(character),也称ASCII码文件。
- 二进制文件:存取的最小信息单位为字节(Byte)。
C++把每个文件都看成一个有序的字节流,每一个文件或者以文件结束符(end of file marker)结束,或者在特定的字节号处结束,如下图所示。
当打开一个文件时,该文件就和某个流关联起来了。对文件进行读写实际上受到一个文件定位指针(file position pointer)的控制。
输入流的指针也称为读指针,每一次提取操作将从读指针当前所指位置开始,每次提取操作自动将读指针向文件尾移动。输出流指针也称写指针,每一次插入操作将从写指针当前位置开始,每次插入操作自动将写指针向文件尾移动。
文件的打开与关闭
文件使用的5步骤:
①说明一个文件流对象,这又被称为内部文件:
ifstream ifile;//只输入用
ofstream ofile;//只输出用
fstream iofile;//既输入又输出用
②使用文件流对象的成员函数打开一个磁盘文件。这样在文件流对象和磁盘文件名之间建立联系。文件流中说明了三个打开文件的成员函数。
void ifstream::open(const char*,int=ios::in,int=filebuf::openprot);
voidofstream::open(const char*,int=ios::out,int=filebuf::openprot);
void fstream::open(const char*,int,int=filebuf::openprot);
第一个参数为要打开的磁盘文件名。第二个参数为打开方式,有输入(in),输出(out)等,打开方式在ios基类中定义为枚举类型。第三个参数为指定打开文件的保护方式,一般取默认。所以第二步可如下进行:
iofile.open(“myfile.txt”,ios::in|ios::out);
上面三个文件流类都重载了一个带默认参数的构造函数,功能与open函数一样:
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::operprot);
所以①和②两步可合成: fstream iofile(”myfile.txt”,ios::in|ios::out);
③打开文件也应该判断是否成功,若成功,文件流对象值为非零值,不成功为0(NULL),文件流对象值物理上就是指它的地址。因此打开一个文件完整的程序为:
fstream iofile(”myfile.txt”,ios::in|ios::out);
if(!iofile)
{ //“!”为重载的运算符
cout<<”不能打开文件:”<<”myfile,txt”<<endl;
return -1;
} //失败退回操作系统
④使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写,这在下一节中讨论。
⑤关闭文件。三个文件流类各有一个关闭文件的成员函数 :
void ifstream::close();
void ofstream::close();
void fstream::close();
使用很方便,如:
iofile.close();
关闭文件时,系统把该文件相关联的文件缓冲区中的数据写到文件中,保证文件的完整,收回与该文件相关的内存空间,可供再分配,把磁盘文件名与文件流对象之间的关联断开,可防止误操作修改了磁盘文件。如又要对文件操作必须重新打开。
关闭文件并没有取消文件流对象,该文件流对象又可与其他磁盘文件建立联系。文件流对象在程序结束时,或它的生命期结束时,由析构函数撤消。它同时释放内部分配的预留缓冲区。
文本文件的读写
文本文件的顺序读写:顺序读写可用C++的提取运算符(>>)和插入运算符(<<)进行。
例1:文件复制
#include<iostream> #include<fstream> #include<cstdlib> using namespace std; int main(){ char ch; ifstream sfile("d:\Ex9_6\Ex9_6.cpp"); ofstream dfile("e:\Ex9_6.cpp"); //只能创建文件,不能建立子目录,如路径不存在则失败 if(!sfile){ cout<<"不能打开源文件:"<<"d:\Ex9_6\Ex9_6.cpp"<<endl; return -1; } if(!dfile){ cout<<"不能打开目标文件:"<<"e:\Ex9_6.cpp"<<endl; return -1; } sfile.unsetf(ios::skipws); //关键!把跳过空格控制位置0,即不跳过空格,否则空格全部未拷贝 while(sfile>>ch)dfile<<ch; sfile.close(); //如没有这两个关闭函数,析构函数也可关闭 dfile.close(); return 0; }
例2:按行复制文本文件
#include<iostream> #include<fstream> #include<cstdlib> using namespace std; int main(){ char filename[256],buf[100]; fstream sfile,dfile; cout<<"输入源文件路径名:"<<endl; cin>>filename;//对路径各方面而言空格是无关紧要的,否则要用getline()等成员函数 sfile.open(filename,ios::in);//打开一个已存在的文件 while(!sfile){ cout<<"源文件找不到,请重新输入路径名:"<<endl; sfile.clear(0);//清状态字 cin>>filename; sfile.open(filename,ios::in); } cout<<"输入目标文件路径名:"<<endl; cin>>filename; //只能创建文件,不能建立子目录,如路径不存在则失败 dfile.open(filename,ios::out); if(!dfile){ cout<<"目标文件创建失败"<<endl; return -1; } while(sfile.getline(buf,100)){//按行拷贝 A行 if(sfile.gcount()<100) dfile<<buf<<' ';//因回车符未送到 B行 else dfile<<buf;//本行大于99个字符,还未读到回车换行符,所以不加' ' } sfile.close(); dfile.close(); return 0; }
例3:文本式数据文件的创建与读取数据
#include<fstream> #include<iostream> #include<iomanip> #include<string> using namespace std; class inventory{ string Description; string No; int Quantity; double Cost; double Retail; public: inventory(string="#",string="0",int=0,double=0,double=0); friend ostream&operator<<(ostream&dist,inventory&iv);//重载插入运算符 friend istream&operator>>(istream&sour,inventory&iv);//重载提取运算符 }; //流类作为形式参数必须是引用 inventory::inventory(string des,string no,int quan,double cost,double ret){ Description=des; No=no; Quantity=quan; Cost=cost; Retail=ret; } ostream &operator<<(ostream&dist,inventory&iv){ dist<<left<<setw(20)<<iv.Description<<setw(10)<<iv.No; dist<<right<<setw(10)<<iv.Quantity<<setw(10)<<iv.Cost<<setw(10)<<iv.Retail<<endl; return dist; }//写入文件是自动把数转为数字串后写入 istream&operator>>(istream&sour,inventory&iv){ sour>>iv.Description>>iv.No>>iv.Quantity>>iv.Cost>>iv.Retail; return sour; }//从文件读出是自动把数字串转为数读出,函数体内>>功能不变 int main(){ inventory car1("夏利2000","805637928",156,80000,105000),car2; inventory motor1("金城125","93612575",302,10000,13000),motor2; ofstream distfile("d:\Ex9_9.data"); distfile<<car1<<motor1;//注意ofstream是ostream的派生类 distfile.close(); cout<<car1; cout<<motor1; cout<<car2; cout<<motor2; ifstream sourfile("d:\Ex9_9.data");//这样分两次打开,可避免读文件时,误改了源文件 sourfile>>car2>>motor2; sourfile.close(); cout<<car2; cout<<motor2; return 0; }
资源获取是由构造函数实现,而资源释放是由析构函数完成。所以与内存动态分配一样,由文件重构对象放在构造函数中,把对象存入文件则放在析构函数中。参见后面章节。
二进制文件的读写
1、对二进制文件进行读写的成员函数:
istream&istream::read(char *,int);
//从二进制流提取
istream&istream::read(unsigned char*,int);
istream&istream::read(signed char *,int);
//第一个参数指定存放有效输入的变量地址,第二个参数指定提取的字节数,
//函数从输入流提供指定数量的字节送到指定地址开始的单元
ostream&ostream::write(const char *,int);
//向二进制流插入
ostream&ostream::write(const unsigned char *,int);
ostream&ostream::write(const signed char *,int);
//函数从该地址开始将指定数量的字节插入输入输出流
2、文件结束判断:读函数并不能知道文件是否结束,可用状态函数int ios::eof()来判断文件是否结束。必须指出系统是根据当前操作的实际情况设置状态位,如需根据状态位来判断下一步的操作,必须在一次操作后立即去调取状态位,以判断本次操作是否有效。
3、例4:创建二进制数据文件,以及数据文件的读取。
#include<fstream> #include<iostream> #include<iomanip> #include<string> using namespace std; class inventory{ string Description; string No; int Quantity; double Cost; double Retail; public: inventory(string="#",string="0",int =0,double =0,double =0); friend ostream &operator<<(ostream&,inventory&); void Bdatatofile(ofstream&dist); //文件流类作为形式参数必须是引用 void Bdatafromfile(ifstream&sour); }; inventory::inventory(string des,string no,int quan,double cost,double ret){ Description=des; No=no; Quantity=quan; Cost=cost; Retail=ret; } ostream &operator<<(ostream&dist,inventory&iv){ dist<<left<<setw(20)<<iv.Description<<setw(10)<<iv.No; dist<<right<<setw(10)<<iv.Quantity<<setw(10)<<iv.Cost<<setw(10)<<iv.Retail<<endl; return dist; } void inventory::Bdatatofile(ofstream&dist){ dist.write(Description.c_str(),20); //由string类的c_str()函数转为char* dist.write(No.c_str(),10); dist.write((char*)&Quantity,sizeof(int)); dist.write((char*)&Cost,sizeof(double)); dist.write((char*)&Retail,sizeof(double)); } void inventory::Bdatafromfile(ifstream&sour){ char k[20]; sour.read(k,20); Description=k; sour.read(k,10); No=k; sour.read((char*)&Quantity,sizeof(int)); sour.read((char*)&Cost,sizeof(double)); sour.read((char*)&Retail,sizeof(double)); }//由此可见读和写是完全对称的过程,次序决不能错 int main(){ inventory car1("夏利2000","805637928",156,80000,105000),car2; inventory motor1("金城125","93612575",302,10000,13000),motor2; ofstream ddatafile("d:\Ex9_10.data",ios::out|ios::binary); car1.Bdatatofile(ddatafile); motor1.Bdatatofile(ddatafile); cout<<"对象car1:"<<endl; cout<<car1; cout<<"对象motor1:"<<endl; cout<<motor1; cout<<"对象car2:"<<endl; cout<<car2; cout<<"对象motor2:"<<endl; cout<<motor2; ddatafile.close(); ifstream sdatafile("d:\Ex9_10.data",ios::in|ios::binary);//重新打开文件,从头读取数据 car2.Bdatafromfile(sdatafile); //从文件读取数据拷贝到对象car2 if(sdatafile.eof()==0) cout<<"读文件成功"<<endl; cout<<"对象car2:"<<endl; cout<<car2; motor2.Bdatafromfile(sdatafile); //继续从文件读取数据拷贝到对象motor2 if(sdatafile.eof()==0) cout<<"读文件成功"<<endl; cout<<"对象motor2:"<<endl; cout<<motor2; sdatafile.close(); return 0; }
这两项操作设计为成员函数,给出与例3不同的读写方式。
4、二进制文件优点:可以控制字节长度,读写数据时不会出现二义性,可靠性高。同时不知格式是无法读取的,保密性好。文件结束后,系统不会再读(见eofbit的说明),但程序不会自动停下来,所以要判断文件中是否已没有数据。如写完数据后没有关闭文件,直接开始读,则必须把文件定位指针移到文件头。如关闭文件后重新打开,文件定位指针就在文件头。