C++_IO与文件5-文件的输入与输出
大多数计算机程序都使用了文件。文件本身是存储在某种设备上的一系列字节。
通常,操作系统管理文件,跟踪它们的位置、大小、创建时间等。
除非在操作系统级别上编程,否则通常不必担心这些事情。
真正需要的是将程序与文件相连的途径、让程序读取文件内容的途径以及让程序创建和写入文件的途径。
C++ I/O类软件包处理文件输入和输出的方式与处理标准输入和标准输出的方式非常相似。
要写入文件,需要创建一个ofstream对象,并使用ostream方法。
要读取文件,需要创建一个ifstream对象,并使用istream方法。
然而,与标准输入和输出相比,文件的管理更加复杂。
例如:必须将新打开的文件和流关联起来。可以以只读模式、只写模式或读写模式打开文件。
为了帮助处理这些任务,C++在头文件fstream中定义了多个新类,其中包括用于文件输入的ifstream类和用于输出的ofstream类。
C++还定义了一个fstream类,用于同步I/O。这些类都是从头文件iostream中派生而来的。
====================================================
一、简单的文件I/O
要让程序写入文件,必须这样做:
1、创建一个ostream对象来管理输出流;
2、将该对象与特定文件关联起来;
3、以使用cout的方式使用该对象,唯一的区别是输出将进入文件,而不是屏幕;
首先要包含fstream头文件,一般包含了该文件就不需要显式包含iostream。
ofstream fout;
接下来,将这个对象与特定的文件关联起来:
fout.open("jar.txt");
可以使用另一个构造函数将这两步合并成一条语句:
ofstream fout("jar.txt");
然后像使用cout的方式使用该对象fout,例如:将Dull Data放到文件中
fout << "Dull Data";
由于ostream是ofstream的基类,因此可以使用所有ostream的方法。例如:各种插入运算符的定义、格式化方法和控制符。
ofstream类使用被缓冲的输出,因此在程序创建像fout这样的ofstream对象时,将为输出缓冲区分配空间。
如果创建了两个ofstream对象,程序将创建两个缓冲区,每个对象各一个。
fout这样的ofstream对象从程序一个一个字节地收集输出,当缓冲区填满后,它便将缓冲区内容一同传输给目标文件。
由于磁盘驱动器被设计称以大块的方式传输数据,而不是逐字节地传输,因此通过缓冲可以大大提高从程序到文件传输数据的速度。
读取文件的要求与写入文件相似:
1、创建一个ifstream对象来管理输入流;
2、将该对象与特定的文件关联起来;
3、以使用cin的方式使用该对象;
ifstream fin;
fin.open("jellyjar.txt");
也可以用一条语句实现:ifstream fin("jellyjar.txt");
char ch;
fin >> ch; //read a character from the jelly
char buf[80];
fin >> buf; // read a word from the file
fin.geline(buf, 80) // read a line from the file
string line;
fin.getline(fin, line);
输入和输出一样,也是被缓冲的。同样,创建一个对象fin时,也将创建一个相应的输出缓冲区。
通过缓冲区传输数据的速度比以字节的方式传输要快得多。
当输入和输出流对象过期时,到文件的连接将自动关闭。
当然也可以使用close()方法来显式地关闭到文件的连接:
fout.close();
fin.close();
关闭这样的连接并不会删除流,而只是断开流到文件的连接。然而流管理装置仍然被保留。例如fin对象与它管理的输入缓冲区仍然存在。
接下来看一个例子:
1 //fileio.cpp -- saving to a filebuf 2 #include<iostream> 3 #include<fstream> 4 #include<string> 5 6 int main() 7 { 8 using namespace std; 9 string filename; 10 11 cout << "Enter name for new file:"; 12 cin >> finname; 13 14 ofstream fout(filename.c_str()); //关联一个文件,用c_str()方法给构造函数提供C-风格字符串参数 15 16 fout<<“For your eyes only!\n”; 17 cout<<"Enter your secret number: "; 18 float secret; 19 cin>> secret; 20 fout<< "Your secret number is "<<secret<<endl; 21 fout.close() //取消关联文件 22 23 ifstream fin(filename.c_str()); 24 cout<<"Here are the contents of "<<filename<<": \n"; 25 char ch; 26 while (fin.get(ch)) 27 cout<<ch; 28 cout<<"Done\n"; 29 fin.close(); 30 31 return 0; 32 }
====================================================
二、流状态检查和is_open()
C++文件流类从ios_base类那里继承了一个流状态成员。
该成员存储了指出流状态的信息;一切顺利,已达到文件尾,I/O操作失败等;
如果一切顺利,则流状态为零。
其他状态都是通过特定位设置为1来记录的。
文件流类还继承了ios_base类中报告流状态的方法。
例如:检查试图打开文件时是否成功?试图打开一个不存在的文件进行输入时,将设置failbit位。
fin.open(argv[file]);
if( fin.fail() )
{
...
}
新的C++实现了提供了一种更好的检查文件是否被打开的方法——is_open()方法:
if(!fin.is_open())
{
...
}
====================================================
三、打开多个文件
程序可能需要打开多个文件,打开多个文件的策略取决于怎么使用它们;
如果需要同时打开两个文件,则需要为每个文件创建一个流;
例如,可能要计算某个名称在10个文件中出现的次数,在这种情况下,可以打开一个流,并将它依次关联到各个文件。
这在节省计算机资源方面,比为每个文件打开一个流的效率更高。
====================================================
四、命令行处理技术
文件处理程序通常使用命令行参数来指定文件。
命令行参数是用户在输入命令时,在命令行中输入的参数。
C++有一种让在命令行环境中运行的程序能够访问命令行参数的机制。
int main(int argc, char *argv[ ])
argc 命令行中的参数个数,其中包括命令名本身;
argv 变量为一个指针,它指向一个指向char的指针。这看起来有点抽象,可以将argv看作是一个指针数组。
其中的指针的指向命令行参数。argv[0]是一个指针,指向存储第一个命令行参数的字符串的第一个字符,
1 //count.cpp -- counting characters in a list of files 2 #include<iostream> 3 #include<fstream> 4 #include<cstdlib> 5 6 int main() 7 { 8 using namespace std; 9 if (argc == 1) //quit if no arguments 10 { 11 cerr<<"Usage: "<<argv[0]<<"filenames[s]\n"; 12 exit(EXIT_FAILURE); 13 } 14 15 ifstream fin; 16 long count; 17 long total = 0; 18 char ch; 19 20 for(int file = 1; file<argc; file++) 21 { 22 fin.open(argv[file]); 23 if(!fin.is_open()) 24 { 25 cerr<<"Could not open "<<argv[file]<<endl; 26 fin.clear(); 27 continue; 28 } 29 count =0; 30 while (fin.get(ch)) 31 count++; 32 cout<<count<<" characters in "<<argv[file]<<endl; 33 total += count; 34 fin.clear(); 35 fin.close(); 36 } 37 38 cout<< total << "characters in all files\n"; 39 return 0; 40 }
====================================================
五、文件模式
文件模式描述的是文件将如何被使用:读、写、追加等;
将流与文件关联的时候,都可以提供指定文件模式的第二个参数;
ifstream fin ("banjo", mode1); // constructor with mode arguments
ofstream fout();
fout.open("harp", mode2); // open() with mode arguments
iosbase类定义了一个openmode类型,用于表示模式;
这是一种bitmask类型。可以选择ios_base类中定义的多个常量来制定模式
常量如下:
ios_base::in 打开文件,以便读取
ios_base::out 打开文件,以便写入
ios_base::ate 打开文件,并移到文件尾
ios_base::app 追加到文件尾
ios_base::trunc 如果文件存在,则截短文件
ios_base::binary 二进制文件
当然如果没有提供第二个参数,也没有关系;
因为还有默认值;
例如:ifstream open()方法和构造函数用ios_base::in(打开文件以读取)作为模式参数的默认值;
ofstream open()方法和构造函数用ios_base::out|ios_base::trunc(打开文件,以读取并截短文件)作为默认值;
位运算符OR(|)用于将两个位值合并成一个可用于设置两个位的值。
fstream类不提供默认值,因此在创建这种类的对象时,必须显式地提供模式;
ios_base::trunc 意味着打开已有的文件,在接受程序的输出时将被截短,也就是说,其以前的内容将被删除;
如果不希望打开文件时将内容删除,则可以有其他选择如下:
ofstream fout("bagels", ios_base::out | ios_base::app);
上述代码使用|运算符来合并模式;因此使用ios_base::out | ios_base::app意味着启用模式out和app。
追加文件
接下来来看一个在文件尾追加数据的程序:
1 //append.cpp -- appending information to a file 2 #include<iostream> 3 #include<fstream> 4 #include<string> 5 #include<cstlib> //for exit() 6 7 const char * file = "guests.txt"; 8 int main() 9 { 10 using namespace std; 11 char ch; 12 13 ifstream fin; 14 fin.open(file); 15 16 if(fin.is_open()) 17 { 18 cout<<"Here are the current constents of the "<<file<<" file:\n"; 19 while(fin.get(ch)) 20 cout<<ch; 21 fin.close(); 22 } 23 24 ofstream fout(file, ios::out | ios::app); 25 if(!fout.is_open()) 26 { 27 cerr<<"Can't open "<<file<<" file for for output.\n"; 28 exit(EXIT_FAILURE); 29 } 30 31 cout<<"Enter guest names(enter a blank line to quit):\n"; 32 string name; 33 while(getline(cin, name) && name.size() >0) 34 { 35 fout<<name<<endl; 36 } 37 fout.close(); 38 39 fin.clear(); //not necessary for some compilers 40 fin.open(file); 41 if(fin.is_open()) 42 { 43 cout<<"Here are the new contents of the "<<file<<" file:\n"; 44 while(fin.get(ch)) 45 cout<<ch; 46 fin.close(); 47 } 48 cout<<"Done.\n"; 49 return 0; 50 }
二进制文件
文本格式是指将所有内容都存储为文本;
二进制格式指的是存储值的计算机内部表示;
对于数字来说,二进制表示和文本表示有很大差异;
每种格式都有自己的优点:
文本格式便于读取,可以使用编辑器或字处理器来读取和编辑文本文件;可以很方便地将文本文件从一个计算机系统传输到另一个计算机系统;
二进制格式对于数字来说比较精确,因为不需要转换,并可以大块地存储数据。二进制格式通常占用空间比较小,这取决于数据的特征。
二进制文件和文本文件:文件格式有两种-文本格式和二进制格式
到底什么是二进制文件、和文本文件?
首先文件无非就是一堆二进制数的集合;八个二进制成一个字节,也就是一堆字节的集合;
其实文本文件就是基于字符编码的文件;
二进制文件是基于值编码的文件;
文本文件和二进制文件的区别不是物理上的,而是逻辑上的;二者是在编码层上有差异;
文本文件基本上是定长编码;
而二进制文件可以看成是变长编码,多少个比特代表一个值,完全由你决定;完全基于具体应用,指定某个值代表什么意思,用户一般不能直接读懂它,要借助软件才行;
以上是定义上的区别,接下来讨论一下存储上的区别:
文本工具打开一个文件,首先读取文件物理上所对应的二进制比特流,然后按照所选择的解码方式来解释这个流,然后将解释结果显示出来。
一般来说,你选取的解码方式会是ASCII码形式(ASCII码的一个字符是8个比特),接下来,它8个比特8个比特地来解释这个文件流。
记事本无论打开什么文件都按既定的字符编码工作(如ASCII码),所以当他打开二进制文件时,出现乱码也是很必然的一件事情了,解码和译码不对应。
文本文件的存储与其读取基本上是个逆过程。而二进制文件的存取与文本文件的存取差不多,只是编/解码方式不同而已。
二进制文件就是把内存中的数据按其在内存中存储的形式原样输出到磁盘中存放,即存放的是数据的原形式。文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,即存放的是数据的终端形式。
使用二进制文件模式的时候,程序将数据从内存传输给文件时,将不会发生任何隐藏的转换。
但是文本模式确并不是这样:
对于Windows系统来说,它们使用两个字符的组合(回车和换行)表示换行符;
对于Macintosh文本来说,使用回车来表示换行符;
对于UNIX和Linux文件来说,使用换行符来表示换行符;
C++由于是从Linux继承过来的,也使用换行符表示换行;
为了增加可移植性,Windows在写文本模式文件时,自动将C++换行符替换为回车和换行;->对文本做了改动;如果放到Linux下运行时,就会出问题;
对于二进制数据,文本格式会引起问题,
另外,检测文件尾的方式也有所不同;
要以二进制格式存储数据,可以使用write()成员函数。这种方法将指定数目的字节复制到文件中。只逐字节地复制数据,而不进行任何格式转换。
C++中可以将文件模式设置为ios_base::binary常量来使用二进制文件格式;
如果要使用二进制格式保存数据,应使用二进制文件格式;
1 //binary.cpp -- binary file I/O 2 #include<iostream> // not required by most systems 3 #include<fstream> 4 #include<iomanip> 5 #include<cstlib> //for exit() 6 7 inline void eatline() {while (std::cin.get() != '\n') continue;} 8 struct planet 9 { 10 char name[20]; 11 double pupulation; 12 double g; 13 } 14 15 const char * file = "planets.dat"; 16 17 int main() 18 { 19 using namespace std; 20 planet pl; 21 cout<<fixed<<right; 22 23 ifstream fin; 24 fin.open(file, ios_base::in|ios_base::binary); 25 26 if(fin.is_open()) 27 { 28 cout<<"Here are the current constents of the "<<file<<" file:\n"; 29 while(fin.read(ch *) &pl, sizeof pl) //用户自定义读取的二进制数据的方式;从文件中复制sizeof pl个字节到pl结构中 30 cout<<setw(20)<<pl.name<<" :"<<setprecision(0)<<setw(12)<<pl.population<<setprecision(2)<<setw(6)<<pl.g<<endl; 31 fin.close(); 32 } 33 34 ofstream fout(file, ios::out | ios::app|ios::binary); 35 if(!fout.is_open()) 36 { 37 cerr<<"Can't open "<<file<<" file for for output.\n"; 38 exit(EXIT_FAILURE); 39 } 40 41 cout<<"Enter planet names(enter a blank line to quit):\n"; 42 cin.get(pl.name, 20); 43 while(pl.name[0] != '\0') 44 { 45 eatline(); 46 cout<<"Enter planetary population: "; 47 cin>> pl.population; 48 cout<<"Enter planet's acceleration of gravity: "; 49 cin>>pl.g; 50 eatline(); 51 fout.write((char *) &pl, sizeof pl); 52 cout<<"Enter planet names(enter a blank line to quit):\n"; 53 cin.get(pl.name,20); 54 } 55 fout.close(); 56 57 fin.clear(); //not necessary for some compilers 58 fin.open(file, ios_base::in|ios_base::binary); 59 if(fin.is_open()) 60 { 61 cout<<"Here are the new contents of the "<<file<<" file:\n"; 62 while(fin.read(ch *) &pl, sizeof pl) 63 cout<<setw(20)<<pl.name<<" :"<<setprecision(0)<<setw(12)<<pl.population<<setprecision(2)<<setw(6)<<pl.g<<endl; 64 fin.close(); 65 } 66 cout<<"Done.\n"; 67 return 0; 68 }
这里补充解释一下,关于eatline函数:
while (std::cin.get() != '\n') continue; //读取并丢弃输入中换行符之前的内容。
上述代码中planet结构作为一个整体保存或读取,将不能使用文本形式读取,因此要以二进制文件模式读写,使用成员函数write()、read();
另外还要注意:
不能用string对象来取代planet结构中的字符数组name成员;
因为string对象本身并没有包含字符串,而是包含了一个指向字符串存储单元的指针。因此将结构复制到文件中时,复制的将不是字符串数据,而是字符串的存储地址。
当再次运行该程序时,该地址将变得毫无意义。
====================================================
六、随机存取
随机存取指的是直接移动到文件的任何位置。
随机存取常被用于数据库文件,程序维护一个独立的索引文件,该文件指出数据在主数据文件中的位置。
程序可以直接跳到这个位置,读取(可能还会修改)其中的数据。
接下来是一个随机读写文件的例子:
1 //random.cpp -- random access to a binary file 2 #include <iostream> 3 #include <fstream> 4 #include <iomanip> 5 #include <cstdlib> 6 const int LIM = 20; 7 struct planet 8 { 9 char name[LIM]; 10 double population; 11 double g; 12 }; 13 14 const char * file = "planet.dat"; 15 inline void eatline() {while(std::cin.get() != '\n') continue;} 16 17 int main() 18 { 19 using namespace std; 20 planet pl; 21 cout<<fixed; 22 23 fstream finout; 24 finout.open(file, ios_base::in|ios_base::out|ios_base::binary); 25 int ct = 0; 26 if(finout.is_open()) 27 { 28 finout.seekg(0); //go to beginning 29 cout<<"Here are the current contents of the "<<file<<" file:\n"; 30 while(finout.read((char *) pl, sizeof pl)) 31 { 32 cout<<ct++<<" : "<<setw(LIM)<<pl.name<<": "<<setprecision(0)<<setw(12)<<pl.population<<setprecision(2)<<setw(6)<<pl.g<<endl; 33 } 34 if(finout.eof()) 35 finout.eat(); 36 else 37 { 38 cerr<<"Error in reading "<<file<<" .\n"; 39 exit(EXIT_FAILURE); 40 } 41 } 42 else 43 { 44 cerr<<file<<" could not be opened -- bye.\n"; 45 exit(EXIT_FAILURE); 46 } 47 48 //change a record 49 cout<<"Enter a record number you wish to change:"; 50 long rec; 51 cin>>rec; 52 eatline(); 53 if(rec<0|rec>=ct) 54 { 55 cerr<<"Invalid record number -- bye.\n"; 56 exit(EXIT_FAILURE); 57 } 58 Streampos place = rec * sizeof pl; 59 finout.seekg(place); 60 if(finout.fail()) 61 { 62 cerr<<"Error on attempted seek\n"; 63 exit(EXIT_FAILURE); 64 } 65 66 finout.read((char *) &pl, sizeof pl); 67 cout<<"Your selection:\n"; 68 cout<<rec<<": "<<setw(LIM)<<pl.name<<" : "<<setprecision(0)<<setw(12)<<pl.population<<setprecision(2)<<setw(6)<<pl.g<<endl; 69 if(finout.eof()) 70 finout.clear(); 71 72 cout<<"Enter planet name: "; 73 cin.get(pl.name, LIM); 74 eatline(); 75 cout<<"Enter planetary population: "; 76 cin>>pl.population; 77 cout<<"Enter planet's acceleration of gravity"; 78 cin>>pl.g; 79 finout.seekp(place); 80 finout.write((char *) &pl, sizeof pl)<<flush; 81 if(finout.fail()) 82 { 83 cerr<<"Error on attempted write\n": 84 exit(EXIT_FAILURE); 85 } 86 87 //show revised file 88 ct = 0; 89 finout.seekg(0); 90 cout<<"Here are the new contents of the "<<file<<" file:\n"; 91 while(finout.read((char *) &pl, sizeof pl)) 92 { 93 cout<<rec<<": "<<setw(LIM)<<pl.name<<" : "<<setprecision(0)<<setw(12)<<pl.population<<setprecision(2)<<setw(6)<<pl.g<<endl; 94 } 95 finout.close(); 96 cout<<"Done.\n"; 97 return 0; 98 99 }
fstream类继承了两个方法seekg()和seekp();
前者将输入指针移到指定的文件位置;
后者将输出指针移到指定的文件位置;
如果打算扩展该程序,最好通过类和函数来重新组织它;
====================================================
七、内核格式化
iostream族提供程序与终端之间的I/O;
fstream族提供程序与文件之间的I/O;
C++库还提供sstream族,使用相同的接口提供程序和string对象之间的I/O;
读取string对象的格式化信息或将格式化信息写入string对象中被称为内核格式化(incore formatting)。
头文件sstream定义了一个从ostream类派生而来的ostringstream类。如果创建了一个ostringstream对象,则可以将信息写入其中,它将存储这些信息。
可以将用于cout的方法用于ostringstream对象。也就是说,可以这样做:
ostringstream outstr;
double price = 380.0;
char * ps = " for a copy of the ISO/EIC C++ Standard! ";
outstr.precision(2);
outstr << fixed;
outstr << "Pay only CHF "<< price << ps <<endl;
格式化文本流入缓冲区,在需要的情况下,该对象将使用动态内存分配来增大缓冲区。
ostringstream类有一个名为str()的成员函数,该函数返回一个被初始化为缓冲区的内容的字符串对象;
string mesg = outstr.str();
下面有个简短的例子,关于内核格式化
//strout.cpp -- incore formatting (output) #include<iostream> #include<sstream> #include<string> int main() { using namespace std; ostringstream outstr; string hdisk; cout<<"What's the name of your hard disk?"; getline(cin, hdisk); int cap; cout << "What's its capacity in GB? "; cin >> cap; outstr << "The har disk "<<hdisk<< " has a capacity of "<<cap<<" gigabytes.\n"; string result = outstr.str(); cout << result; return 0; }
istringstream类允许使用istream方法族读取istringstream对象中的数据。istringstream对象可以使用string对象进行初始化;
假设facts是一个string对象,则要创建与该字符串相关联的istringstream对象,可以这样做:
istringstream instr(facts);
int n;
int sum = 0;
while(instr>>n)
sum +=n;
1 //strin.cpp -- formatted reading from a char array 2 #include<iostream> 3 #include<sstream> 4 #include<string> 5 6 int main() 7 { 8 using namespace std; 9 string lit = "It was a dark and stormy day, and the full moon glowed brilliantly."; 10 istringstream instr(lit); 11 string word; 12 while (instr>>word) 13 cout<<word<<endl; 14 return 0; 15 }
总是istringstream类和ostringstream类能够使用istream和ostream类的方法来管理存储在字符串中的字符数据。
====================================================
八、总结
流是进出程序的字节流。
缓冲区是内存中的临时存储区域,是程序与文件或其他I/O设备之间的桥梁。
信息在缓冲区和文件之间传输时,将使用匹配设备最快传输方法的大块数据的方式进行传输;
信息在缓冲区和程序之间传输时,是逐字节传输的;
这种带缓冲区的方式对于程序的处理操作更为方便。
iostream和fstream头文件构成了I/O类库;该类库定义了大量用于管理流的类。
包含iostream文件的C++程序将自动打开8个流,并使用8个对象来管理它们。cout、cin、cerr、clog、wcin、wcout、wcerr、wclog
I/O类库提供了大量有用的方法;
get()方法族和getline()方法为单字符输入和字符串输入提供了进一步的支持;
fstream文件提供了将iostream方法扩展到文件I/O的类定义。
ifstream是从istream类派生而来的。
通过将ifstream对象与文件关联起来,可以使用istream方法来读取文件;
通过将ofstream对象与文件关联起来,可以使用ostream方法来写文件;
通过将fstream对象与文件关联起来,可以将输入和输出方法用于文件;
要将文件与流关联起来。可以在初始化文件流对象时提供文件名;
也可以先创建一个文件流对象,
open()方法将这个流与文件关联起来。
close()方法终止流与文件之间的连接。
类构造函数和open()方法提供第二个参数:该参数提供文件模式,文件模式决定文件是否被读和或/写、打开文件以便写入时是否截短文件、试图打开不存在的文件时是否会导致错误、是使用二进制模式还是文本模式等;
文本文件以字符格式存储所有信息。二进制文件使用计算机内部使用的二进制表示存储信息。
与文本文件相比,二进制文件存储数据(尤其是浮点值)更为精确、简洁、但可移植性较差。
read()和write()方法都支持二进制输入和输出;
seekg()和seekp()函数提供对文件的随机存取。这些类方法使得能够将文件指针放置到相对于文件开头、文件尾和当前位置的某个位置。
tellg()和tellp()方法报告当前的文件位置。
sstream头文件定义了istringstream和ostringstream类,这些类使得能够使用istream和ostream方法来抽取字符串中的信息,并对要放入到字符串中信息进行格式化;