第八章 IO库
找回部分翻译
内容
8.1 IO类
8.2 文件输入和输出
8.3 string流
本章小结
专业术语
c++语言没有直接处理输入和输出。相反,通过定义在标准库中的一组类型来操作IO。这些类型支持控制台和文件的IO操作。还有些类型支持string内存的IO操作。
IO库定义了读写内置类型的操作。另外,类,例如string,通常也定义了类似的IO操作。
本章介绍IO库的基础。后面的章节将会介绍另外的功能:14章将会介绍如何编写自己的输入和输出操作。17章将会介绍怎么控制格式,以及怎么在文件中执行随机访问。
以前的程序已经使用了许多IO库功能。事实上,我们已经在1.2小节中介绍了这些功能:
istream(输入流)类型,它提供了输入操作
ostream(输出流)类型,它提供输出操作
cin,一个istream的对象,它从标准输入中读数据
cout,一个ostream对象,它写到标准输出中
cerr,一个ostream对象,通常将错误信息,输出到标准错误中
>>运算符,它被用来从istream对象中读数据
<<运算符,它被用来向ostream对象中输出数据
getline函数(3.2.2小节),它从istream中读入一行,然后存入一个string中。
8.1 IO类
我们迄今为止使用的IO类型和对象都是操作的char数据。默认情况下,这些对象连接到用户的控制台窗口。当然,真正的程序不局限于从控制台中进行IO操作。程序经常需要读写命名文件。并且使用IO操作来处理string中的字符也非常方便。应用常常也不得不读写需要宽字符支持的语言。
为了支持这些不同种类的IO操作,库除了定义istream和ostream以外,还定义其他的类型。这些类型被列在了表8.1中。这些类型都被分别定义在了不同的头文件中了:iostream定义了从流中读写的类型,fstream定义了从文件中读写的类型,sstream定义了从string中进行读写的类型。
为了支持宽范围的字符,库也定义了一些列的类型和对象用于操作wchar_t数据(2.1.2).宽字符的名字以w开头,例如,wcin,wcout,wcerr都是分别对应于cin,cout,cerr的宽字符版本。宽字符类型和对象与普通的char类型定义在同一个头文件中。例如,fstream头文件定义了ifstream和wifstream类型。
IO类型之间的关系
概念上来讲,设备类型和字符大小不会影响我们要执行的IO操作。例如,使用>>运算符读取数据,而不用管是否从控制台窗口,磁盘文件,或者一个string中读取。同样的,我们也不用关心读取的字符到底是char,还是wchar_t.
标准库让我们忽略这些不同流之间的差异,是通过继承机制实现的。跟模板一样,使用类的继承机制,不用关心这些机制是如何实现的。在15章和18.3小节将会介绍c++如何支持继承的。
ifstream和istreamstream继承于istream。因此,使用ifstream和istreamstream对象就好像使用istream对象一样。即,可以使用这些对象跟使用cin一样。例如,可以在ifstream或者istringstream对象上调用getline,并且可是使用>>运算符从ifstream和istringstream中读取数据。类似的,ofstream 和ostringstream继承于ostream.因此,可以使用这些类型的对象跟使用cout一样。
注意:本段之后的所有介绍都可以应用到普通流,文件流,string流,以及应用到字符流和宽字符流的版本中。
8.1.1 IO对象没有复制和赋值
正如7.3.1小节中介绍的那样,不能赋值或者复制一个IO类型的对象:
ofstream out1,out2;
out1 = out2; //错误:不能够赋值流对象
ofstream print(ofstream); //错误:不能初始化ofstream形参
out2 = print(out2); //错误:不能复制流对象
因为不能复制IO类型,所以形参类型和返回类型都不能为这种类型。执行IO操作的函数,传递和返回的通常是IO类型的引用。读写一个IO对象会改变它的状态,因此引用不能是const的。
8.1.2 条件状态
操作IO就具备了会发生错误。一些错误是可恢复的;还有一些错误是系统深层次的错误,已经超出了程序处理的范围。IO类定义的函数和标志在表8.2中,接下来让我们访问并操作流的条件状态。
下面是一个错误的例子:
int ival’
cin >> ival;
如果输入Boo,则读取失败。输入运算符希望得到一个int类型,但是读到了B。所以,cin将处在一个错误的状态中。同样的,如果我们键入文件结束符,cin也会处在一个错误的状态中。
一旦一个错误发生,在同一个流中的后续的IO操作将会失败。只有当流处在非错误状态时,才可以从流中进行读写。因为流可能处在错误状态中,那么代码就应该在使用之前进行检查。判断流最简便的方式就是在条件表达式中使用这个流对象:
while(cin >> word)
//正确:读操作成功。。。
while的条件表达式检查从>>中返回的状态。如果输入操作成功,状态合法,提交表达式返回true
查询流的状态
使用流做为条件表达式,仅仅是告诉我们这个流是否合法。它还没有告诉我们当这个流非法时,发生了什么。有时,我们也想知道这个流为什么是变得非法了。例如,遇到文件结束符执行其他操作,而遇到输入错误则执行另外的操作。
IO库定义了一个跟机器相关的整数类型,叫做iostate,它被用来传达流的状态信息。这个类型被当做位的集合。它的使用跟4.8小节中对quizl的使用一样。IO类定义了四个类型为iostate 的constexpr值。这些值代表不同的位模式。这些值用来表示特定的IO条件。他们可以和位运算一起使用,用于测试或者设置多个标志位。
badbit代表系统层的错误,例如不可恢复的读或者写错误。通常一个流如果badbit被置位,那么这个流就不能再使用了。failbit只有在可恢复的错误发生之后 ,被置位。例如,需要数字的时候,读到的是字符。这种可以被修正,然后继续使用这个流。到达文件结尾之后,会将eofbit和failbit置位。goodbit的值为0,表示流没有错误。badbit,failbit,eofbit的任何一个被置位,条件表示求值这个流的时候,返回的就是false
库也定义了函数来查询这些标志位的状态。如果没有错误位被置位,good返回true。bad,fail,eof在对应位被置位的情况下返回true。另外,如果bad被置位,fail也会返回true。这表示,判断流的状态是否可用,可以用good和fail。事实上,当使用一个流作为条件表达式时,等价于调用!fail().而eof和bad只能表示指定的错误。
管理条件状态
rdstate成员返回流当前的iostate。setstate将给定的条件位置位,表明某种错误发生。clear成员被重载:其中一个没有参数,另外一个带有一个iostate类型的参数。
没有参数的clear成员将对所有的失败位,进行清除操作。调用clear()之后,调用good()肯定返回true。例子如下:
//记住当前的状态
auto old_state = cin.rdstate(); //记住cin的当前状态
cin.clea(); //使cin合法
process_input(cin);//使用cin
cin.setstate(old_state); //复位cin到以前的状态
带有一个iostate的clear函数,使用这个iostate值作为流的新的状态。为了关掉某个条件,使用rdstate 和位操作运算符来处理相应的位。
例如,下面关闭failbit和badbit,但是eofbit保持不变:
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
8.1.3 管理输出缓冲
每一个输出流,都有一个缓冲,这个缓冲用于保存程序产生的读写数据。例如,下面的代码被执行:
os << “please enter a value:”;
字符串字面量可能会立刻被打印,或者系统将其存入以后被打印的缓冲中。使用缓冲可以让系统将几个输出,组合成一个系统级的写调用。因为写到设备上的操作,通常是耗时的,操作系统将几个输出合并成一个写操作,可以提高性能。
此处有几种情况会,将缓冲数据刷新到真正的输出设备和文件中:
- 程序正常运行完成。作为main函数的return的一部分,所有的输出缓冲被刷新
- 在程序的运行期间,缓冲可能变慢,在下一次写之间,会将缓冲刷新
- 可以使用endl,显式的让其刷新缓冲。
- 可以使用unitbuf操作改变流内部的状态,让其清空缓冲,已达到刷新的目的。默认情况下,unitbuf操作对于cerr来说,一直打开的,因此,写到cerr的数据会立刻刷新。
- 一个输出流可能关联到另外一个流上。此时,无论被关联流是读还是写,关联流的缓冲会被刷新,默认情况下,cin和cerr都关联到了cout。因此,当读cin或者写cerr的时候,会刷新cout的缓冲。
刷新输出缓冲
我们的程序使用了endl,它结束当前行,并刷新缓冲。还有两个相似的操作:flush和ends。flush刷新流,但是不会增加新的字符。ends在默认增加一个空字符,然后刷新流。
cout << “hi!” << end; //打印hi!,然后一个新行,然后刷新缓冲
cout << “hi!” << flush; //打印hi!,然后刷新缓冲,不会增加新的数据
cout << “hi!” << ends; //打印hi!,然后增加一个空字符,最后刷新缓冲
unitbuf操作符
如果我们想刷新每一条输出,可以使用unitbuf操作符。这个操作符,告诉流,每次写之后,都刷新缓冲。nounitsbuf操作符,则复位流成普通的状态,由系统来管理缓冲的刷新:
cout << unitbuf; //所有的写操作将立刻被刷新
//其他的输出将会立刻刷新,不会在缓存
cout << nounitbuf; //复位到正常的缓冲管理
注意:程序崩溃是,缓冲不会被刷新
如果程序非正常中断,缓冲不会被刷新。当程序崩溃时,程序写的数据,可能依然停留在缓冲中等待被打印。当你调试一个崩溃的程序时,最基本的就是保证,你想要的输出已经被刷新。否则大量的时间被浪费在为什么没有执行代码,事实上,则是代码已经被执行,只是输出停在缓冲中。
绑定输入和输出流
当输入流绑定到输出流时,任何读输入流的操作都会刷新对应的输出流。标准库将cout和cin绑定在一起,所以下面的语句:
cin >> ival;
就会导致cout的缓冲立刻刷新。
注意:交互式系统通常应该绑定输入流和输出流。这意味着所有的输出流,都会在读取之前打印所有消息,包括提示消息。
有两个tie函数用于绑定流:其中一个不需要参数,并且返回本对象当前绑定的输出流的指针。如果本对象没有绑定输出流,该函数返回空指针。
另外一个需要传递一个输出流指针,并且将这个输出流与自身绑定。即,x.tie(&o)绑定流x到输出流o上。
可以将ostream或者istream绑定到ostream上:
cin.tie(&cout); //只是为了说明:库已经将cin和cout绑定在了一起。
//old_tie 指向当前绑定的流(如果有的话)
ostream *old_tie = cin.tie(nullptr); //cin不在绑定
//绑定cin到cerr,这不是一个好主意,因为cin应该与cout绑定
cin.tie(&cerr); //读cin,会刷新cerr,而不是cout
cin.tie(old_tie); //重新将cin和cout绑定
为了将一个新流绑定到输出流,需要给tie传递这个新流的指针。而为了彻底解绑,则传递一个空指针。每个流同时最多绑定到一个流。但是,多个流可以绑定到同一个ostream.
8.2 文件输入和输出
fstream头文件定义了三种类型,用于文件的IO操作:ifstream从给定的文件中读;ofstream写入给定文件;fstream读写给定文件。在17.5.3小节将会介绍,如何对同一个文件进行读写操作。
这些类型提供了跟cin和cout相同的操作。事实上,可以使用IO运算符(<<和>>)读写文件。也可以使用getline读取ifstream中的数据,并且在8.1节中介绍的内容也适用于这些类型。
除了从iostream类型继承来的行为以外,定义在fstream头文件中的类型,还增加了管理文件流相关的成员。这些操作列在了表8.3中。表8.3中的成员可以在fstream对象,ifstream对象,ofstream对象上面调用,而不可以在其他的IO类型上面调用。
8.2.1 使用文件流对象
当想要读或者写文件的时候,定义一个文件流对象然后将这个对象和需要读写的文件进行关联。每一个文件流类都定义了一个叫做open的函数,这个函数用系统支持的操作定位文件,然后为后续的读写,打开这个文件。
创建file流对象的时候,可以提供文件名。一旦提供了文件名,open自动被调用。
ifstream in(ifile); //创建一个ifstream对象并,打开给定的文件
ofstream out; //out对象还没有关联文件
定义in的为输入流,这个输入流初始化从ifile传递来的文件。定义out为输出流,它还未绑定任何文件。在c++11新标准中,文件名可以是string类型,也可以是c风格的字符数组。而以前的版本仅仅c风格的字符数组。
使用fstream替换iostream&
正如8.1小节中提到的,可以使用子类对象来替换父类对象。这就意味着,接收iostream引用(或者指针)的函数可以使用fstream(或者sstream)类型。即,如果一个函数,需要传递一个ostream&,那么可以传递这个函数一个ofstream对象。类似的也可以用在istream&和ifstream上。
例如,可以使用7.1.3小节中的read和print函数,向文件中读取和写入。在这个例子中,假设输入和输出的文件名已经传递给了main:
ifstream input(argv[1]);
ofstream output(argv[2]);
Sales_data total;
if(read (input,total)){
Sales_data trans;
while(read(input,trans)){
if(total.isbn() == trans.isbn())
total.combine(trans);
else {
print(output,total) << endl;
total = trans;
}
}
print(output,total) << endl;
}else{
cerr << “No data ?! ” << endl;
}
open和close成员
当定义一个空的文件流对象时,可以在后面使用open函数在进行关联:
ifstream in(ifile);
ofstream out;
out.open(ifile+”.copy”);
如果open调用失败,failbit被设置(8.1.2小节)。因为调用open可能会失败,所以总是对这个调用进行检查是个好主意。
if(out) //检查open是否成功
//open成功,才能使用这个文件
这个条件表达式跟使用cin类似,如果open失败,这个条件表达式返回false,就不应该继续使用out对象。
一旦文件流被打开,就一直保存跟文件的联系。事实上,对一个已经打开的文件流再次调用open,将会失败,并且failbit被置位。后续对这个流的使用也都将失败。为了关联不同的文件,必须先关闭以前的文件。只有以前的文件被关闭之后,才能再次打开一个新的文件。
in.close ();
in.open(iflile + “2”);
如果open调用成功,它将设置流中的状态为正确的状态,即,good()返回true
自动构造和析构
考虑如下的程序,这个程序的main函数带有一组需要被处理的文件。这种程序可能有如下的循环:
for(auto p = argv +1;p!=argv+argc;++p){
ifstream input(*p);
if(input){
process(input);
}else
cerr << “couldn’t open:”+string(*p);
}
每次迭代都创建一个新的名为input的ifstream对象。然后打开对应的文件并读。通常,需要判断open是否成功,如果成功,才将这个文件流传递给一个函数,这个函数负责对这个流进行读,并且处理相应的输入。如果不成功,打印错误信息,然后继续后续的迭代。
因为input是while的局部变量,在每次的迭代中,进行创建和销毁。当fstream对象超出了作用域,绑定的文件自动关闭。在下一次迭代中,input为一个新的文件流。
注意:当fstream对象销毁时,close自动调用。
8.2.2 文件模式
每个文件流都有一个对应的文件模式,这个文件模式代表了文件应该怎么被使用。表8.4列出了文件模式和对应的意思:
在打开文件的时候,可以提供一个文件模式。可以是在调用open的时候,传递传递文件模式,也可以是在初始化一个文件流的时候,传递一个文件模式。传递的文件模式有如下的限制
- out只能设置ofstream对象和fstream对象
- in 只能设置ifstream对象和fstream对象
- trunc 只有在out被设置之后,才能设置
- app只有在trunc没有设置的时候,才能使用。当app被设置,即使没有显示的指定out,这个文件也一定是以输出模式打开的。
- 默认情况下,即使没有指定trunc,以out打开的文件也可以被截断。为了防止这种情况,要么使用app模式,或者in模式。在app模式中,数据会在文件的末尾进行追加。而in模式,会为了读写而打开文件(将在17.5.3小节中介绍)
- ate和binary文件模式,适用于所有的文件流类型。并且还可以和其他的文件模式联合使用。
每一个文件流类型都有一种默认的文件模式。ifstream为in模式,ofstream为out模式,fstream为in和out模式。
以out模式打开以文件,会丢失以前的数据
默认情况下,当打开一个ofstream,文件中的内容将会丢失。为了防止情况文件的唯一方法是指定app模式:
//file1在下面的模式中会被清空
ofstream out(“file1”);//out和trunc是隐式的
ofstream out2(“file1”,ofstream::out);//trunc是隐式的
ofstream out3(“file1”,ofstream::out | ofstream::trunc);
//为了保留文件中的内容,必须显式的指定app模式
ofstream app(“file2”,ofstream::app); //out是隐式的
ofstream app2(“file2”,ofstream::out | ofstream::app);
警告:防止ofstream清空文件内容的方法就是,显式指定app模式或者in模式。
每次调用open时,文件模式被确定
给定流的文件模式在文件被打开的时候,可以改变:
ofstream ouot;
out.open(“scratchpad”);
out.close();
out.open(“precious”,ofstream::app);
out.close();
第一句调用没有指定打开的文件模式。此时,使用的是out模式打开。通常,out实现了trunc。因此,叫做scratchpad的文件将会被截断。当打开precious文件时,使用app模式。所以,在文件中的内容将会保留,后续写入的内容,将出现在文件的末尾。
注意:调用open的时候,文件模式会被设置,或者显式或者隐式。当为指定模式时,就使用默认模式。
8.3 string流
sstream头文件定义了三种类型,用于内存的IO操作;这些类型将string当做IO流,进行读写。
istringstream类型读string,ostringstream写string,stringstream读写string。跟fstream类型一样,定义在sstream头文件中的类型,继承于iostream头文件中的类型。除了继承来的操作以外,定义在sstream头文件中的类型还增加了以流来管理string的操作。这些操作被列在了表8.5中。表8.5中的操作只能在stringstream对象上面使用
注意尽管fstream和sstream都共享iostream的接口,但是他们没有任何关系。事实上,不能再stringstream上调用open和close,也不能再fstream上面使用str。
8.3.1 使用istringstream
当需要对整行进行处理,并且对行内的某些单词进行处理,通常使用istringstream.
例如,有一个文件里面包含用户和其电话。但是有些用户只有一个电话号码,而另外的用户则有好几个,如,家庭电话,工作电话,移动电话等等。有一个如下的输入文件:
morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000
文件每行以名字开始,后面跟着一个或者多个电话号码。首先定义一个简单的类代表输入的数据:
struct PersonInfo{
string name;
vector<string> phones;
};
PersonInfo对象含有一个表示人名的成员,还有一个vector成员,用于保存对应的电话号码。
我们的程序读取数据文件,然后创建一个PersonInfo的vector成员。在vector中元素就
string line,word;
vector<PersonInfo> people
while(getline(cin,line)){
PersonInfo info;
istringstream record(line);
record >> info.name;
while(record >> word)
info.phones.push_back(word);
people.push_back(info);
}
例子中,使用了getline从标准输入中读取记录。如果getline调用成功,那么line就保存有来自于输入文件中的一条记录。在while内部,定义一个PersonInfo的局部对象,用于保存当前记录中的数据。
接下来使用istringstream绑定读取到的数据。现在可以使用输入运算符在istringstream对象上,用于读取当前记录中的每个元素。首先读取名字,然后跟着一个while循环,用于读取电话号码。
在内层while结束之后,则读取完了line中的所有数据。这个循环跟使用cin来读取比较相似。不同之处就在于这个循环从字符串而不是标准输入中读取数据。当字符串被读取完,产生一个文件结束符,并且下一个在record上面的输入操作将会失败。
将刚刚处理的PersonInfo添加在vector末尾,然后结束外层while。继续下一次的迭代,直到cin中的文件结束符。
8.3.2 使用ostringstream
当需要逐步构建输出,然后在后面一次性输出,则ostringstream是有用的。例如,需要对上面程序读到的电话号码进行验证和格式化。如果所有的电话号码都是合法的,那么将这电话号码重新格式化之后保存在另外一个新文件中。如果用户有非法的电话号码,则不保存在新文件中,而是输出包含名字和非法号码的错误信息。
因为不想包含任何带有错误电话号码的用户信息。所以要处理完所有的电话号码之后,才能进行输出。所以,我们将输出保存在内存中的ostringstream:
for(const auto & entry:people){
ostringstream formatted,badNums;
for(const auto&nums:entry.phones){
if(!valid(nums)){
badNums << ‘ ’ <<nums;
}else
formatted << “ ” << format(nums);
}
if(badNums.str().empty())
os << entry.name << “ ”
<<formatted.str() << endl;
else
cerr << “input error: ”<< entry.name
<< “ invalid number(s)” << badNums.str() << endl;
}
在这个程序中, 我们假定两个函数,valid和format,分别用于验证和格式化电话号码。这个程序应该关注的部分是使用了string流formatted和badNums。我们使用常用的输出运算符(<<)写入这些对象中。但是这些写实际是字符串操作。他们分别增加字符到formatted和badNums内部的字符串中。
本章小结(译略)
专业术语(译略)
最开始翻译放入word文档中,然后复制粘贴到这上面,可能会有错误,望指正