目录
已知的几种IO库设施:istream,ostream,cin,cout,cerr,>>,<<,getline
8.1 IO类
为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开始。列如wcin,wout,werr。 宽字符版本的类型和对象与其对应的普通 char 版本的类型定义在同一个头文件中。例如,头文件fstream定义了ifstream 和 wifstream类型。
IO类型间的关系->继承机制
8.1.1 IO对象无拷贝或赋值
ofstream out1, out2;
out1 = out2;//错误,不能对流对象赋值
ofstream print(ofstream); //错误:不能初始化ofstream参数
out2 = print(out2); //错误:不能拷贝流对象
1.由于不能搓贝IO对象,因此我们也不能将形参或返回类型设置为流类型
2.传递和返回的引用不能是const(进行IO操作的函数通常以引用方式传毒和返回流。读写一个IO对象会改变其状态)
8.1.2条件状态
while循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真
while(cin>>word)
//ok;读操作成功
查询流的状态
iostate类型:提供表达流的完整功能,此类型应该作为一个位集合来使用,使用方法与quizl相同
4个iostate类型的constexpr值:表示特定的位模式,表示特定类型的IO条件,可与位运算符一起使用来一次性检测或设置多个标志位
badbit:badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦 badbit被置位,流就无法再使用了。在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。如果到达文件结束位置,eofbit和 failbit 都会被置位。goodbit的值为0,表示流未发生错误。如果badbit、failbit和 eofbit任一个被置位,则检测流状态的条件会失败。
标准库还定义了一组函数来查询这些标志位的状态
管理条件状态
流对象的rdstate成员返回一个iostate值,对应流的当前状态
setstate操作:将给定条件条件位置位,表示发生了对应错误
//复位failbit和badbit,保持其他标志位不变
cin.clear(cin.rdstate () & ~cin.failbit & ~cin.badbit);
clear成员:重载成员,有一个不接受参数的版本,可清楚(复位)所有错误标志位,执行clear()后调用good会返回true;而另一个版本接受一个iostate类型的参数
//记住cin的当前状态
auto old_state = cin.rdstate();//记住cin的当前状态
cin.clear();//使cin有效
process_input(cin); //使用cin
cin.setstate(old_state);//将cin置为原有状态
带参数的clear版本接受一个iostate值,表示流的新状态。为了复位单一的条件状态位,我们首先用rdstate读出当前条件状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将failbit和 badbit复位,但保持eofbit不变:
//复位failbit和badbit,保持其他标志位不变
cin.clear(cin.rdstate () & ~cin.failbit & ~cin.badbit);
8.1.3管理输出缓冲
导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:
1·程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
2缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
3我们可以使用操纵符如 endl(参见1.2节,第6页)来显式刷新缓冲区。
4在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。
5一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。
刷新输出缓缓冲区
cout << "hi!" << endl;//输出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush;//输出hi,然会刷新缓冲区,不附加任何额外字符
cout << "hi!" << ends;//输出hi和一个空字符,然后刷新缓冲区
unitbuf操纵符
如果想在每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符。它告诉流在接下来的每次写操作之后都进行一次flush 操作。而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制: 如果想在每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符.它告诉流在接下来的每次写操作之后都进行一次操作.而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:
cout << unitbuf; Cout<<单位; //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf; Cout<<nounitbuf;
//回到正常的缓冲方式 //回到正常的缓冲方式
关联输入和输出流
cin >> ival; //导致cout的缓冲区被刷新
两个tite重载的版本:不带参数,返回指向输出流的指针。or接受一个指向ostream的指针,将自己关联到此ostream,即x.tie(&o)将流x关联到输出流o
cin.tie(&cout); //仅仅是用来展示:标准库将cin和cout关联在一起
//old_tie指向当前关联到cin的流(如果有的话)
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。 在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了领带。为了彻底解开流的关联,我们传递了一个空指针.每个流同时最多关联到一个流,但多个流可以同时关联到同一个Ostream.
8.2 文件输入输出
头文件fstream定义了三个类型来支持IO:
ifstream从一个给定文件读取数据
ofstream向一个给定文件写入数据
fstream可以读写给定文件
可以对fstream,ifstream,ofstream对象调用一下操作,但不能对其他IO类型调用这些操纵
8.2.1 使用文件流对象
创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则open会自动调用
ifstream if(ifile); //构造一个ifstream并打开给定文件
ofstream out; //输出文件流为关联到任何文件
这段代码定义了一个输入流in,它被初始化为从文件读取数据,文件名由 string类型的参数ifile指定。第二条语句定义了一个输出流 out,未与任何文件关联。在新C++标准中,文件名既可以是库类型string对象,也可以是C风格字符数组(参见3.5.4节,第109页)。旧版本的标准库只允许C风格字符数组。
用fstream代替iostream&
我们在8.1节(第279页)已经提到过,在要求使用基类型对象的地方,我们可以用继承类型的对象来替代。这意味着,接受一个iostream类型引用(或指针)参数的函数,可以用一个对应fstream(或sstream)类型来调用。也就是说,如果有一个函数接受一个 ostream&参数,我们在调用这个函数时,可以传递给它一个ofstream对象,对istream&和 ifstream也是类似的。
用7.13节中的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.isbin() == trans.isbn()) //检查isbn
total.combine(trans); //更新销售总额
else {
print(output, total) << endl; //打印结果
total = trans; //处理下一本书
}
}
print(output, total) << endl; //打印最后一本数的销售额
}
else
cerr << "No data?!" << endl;
成员函数open和close
如果我们定义了一个空文件流对象,可以随后调用open来将它与文件关联起来
ifstream if (ifile); //构筑一个ifstream并打开给定文件
ofstream out; //输出文件流未与任何文件相关联
out.open(ifile + ".copy"); //打开指定文件
调用open失败,failbit会被置为,因为调用open可能失败,进行open能否成功的检测通常是一个好习惯
if (out) //检查open能否成功
//open成功,就可以使用文件了
一旦一个文件流已经打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用open会失败,并会导致failbit被置位。随后的试图使用文件流的操作都会失败。为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。一旦文件成功关闭.我们可以打开新的文件,
in.close(); //关闭文件
in.open(ifile + "2"); //打开另一个文件
如果open成功,则open会设置流的状态,使得good()为true
自动构造和析构
考虑这样一个程序,其main函数接受一个要处理的文件列表,这种程序可能会有如下的循环
//对每个传递给程序的文件执行循环操作
for (auto p = argv + 1; p != argv + argc; ++p)
{
ifstream input(*p); //创建输出流并打开文件
if (intput) { //如果文件打开成功,”处理“此文件
process(input);
}
else
{
cerr << "couldnt't open:" + string(*p);
}
}//每个循环步input都会离开作用域,因此会被销毁
每个循环步构造一个新的名为input的ifstream对象,并打开它来读取给定的文件。像之前一样,我们检查open是否成功。如果成功,将文件传递给一个函数,该函数负责读取并处理输入数据。如果 open 失败,打印一条错误信息并继续处理下一个文件。
因为input是while循环的局部变量,它在每个循环步中都要创建和销毁一次(参见5.4.1节,第165页)。当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。在下一步循环中,input会再次被创建。
当一个fastream对象被销毁时,close会被自动调用
8.2.2文件模式
每个流都有一个关联的文件模式,用来指出如何使用文件
无论那种方式,都可以指定文件模式。指定文件模式有如下限制
、
每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。
以out模式打开文件会丢弃已有的数据
阻止一个ofstream清空给定文件内容的方法是同时指定app模式:
//在这几条语句中,filel都被截断
ofstream out("file1");//隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out);//隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
//为了保留文件内容,必须显示指定app模式
ofstream app("file2", ofstream::app);//隐含为输出模式
ofstream app2("file2", ofstream::out | ofstream::app);
每次调用open时都会确定文件模式
ofstream out; //未指定文件打开模式
out.open("scratchpad"); //模式隐含设置为输出和截断
out.close();//关闭out,以便我们将其用于其他文件
out.open("Precious", ofstream::app);//模式为输出和追加
out.close();
第一个open调用未显式指定输出模式,文件隐式地以out模式打开。通常情况下,out模式意味着同时使用trunc模式。因此,当前目录下名为scratchpad的文件的内容将被清空。当打开名为precious 的文件时,我们指定了append模式。文件中已有的数据都得以保留,所有写操作都在文件末尾进行。
8.3 string流
sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像是string是一个IO流一样
istringstream从string读取数据,ostringstream向string写入数据,头文件stringstream既可以从string读数据也可向string写数据。
与fstream类型类似,头文件sstream中定义的类型都继承我们已经使用过的iostream头文件中定义的类型,处理继承得来得操作,sstream中定义得类型还增加了一些成员来管理与流相关联得string
上述操作可以对stringstream对象调用这些操作。但不能对其他IO类型调用这些操作
8.3.1 使用istringstream
某些工作是对整行文本进行处理,而其他一些工作是处理行内得单个单词时,通常可以使用istringstream
首先定义一个简单得类来描述输入数据
//成员默认为公有
struct PersonInfo {
string name;
vector<string>phones;
};
程序读取数据文件,创建一个PersonInfo得vector。vector中每个元素对应文件中的一条记录。在一个循环中处理输入元素,每个循环步读取一条记录,提取出一个人名和若干个电话号码
string line, word;//分别保存来自输入的一行和单词
vector<PersonInfo>people; //保存来自输入的所有记录
//运行从输入读取数据,直至cin遇到文件尾(或其他错误)
while (getline(cin, line)) {
PersonInfo info; //创建一个保存此纪录数据的对象
istringstream record(line); //将记录绑定到刚读入的行
record >> info.name; //读取名字
while (record >> word) //读取电话号码
info.phones.push_back(word); //保持它们
people.push_back(info); //将此纪录追加到people末尾
}
这里我们用getline 从标准输入读取整条记录。如果getline 调用成功,那么line中将保存着从输入文件而来的一条记录。在while 中,我们定义了一个局部PersonInfo对象,来保存当前记录中的数据。
接下来我们将一个 istringstream与刚刚读取的文本行进行绑定,这样就可以在此 istringstream 上使用输入运算符来读取当前记录中的每个元素。我们首先读取人名,随后用一个while循环读取此人的电话号码。
当读取完line中所有数据后,内层 while循环就结束了。此循环的工作方式与前面章节中读取 cin 的循环很相似,不同之处是,此循环从一个string 而不是标准输入读取数据。当string中的数据全部读出后,同样会触发“文件结束”信号,在record上的下一个输入操作会失败。
我们将刚刚处理好的 PersonInfo追加到vector中,外层while循环的一个循环步就随之结束了。外层while循环会持续执行,直至遇到cin 的文件结束标识。
8.3.2 使用ostringstream
由于我们不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作。但是,我们可以先将输出内容“写入”到一个内存ostrinastream 中:
for (const auto& entry : people) {//对people中每一项
ostringstream formatted, badNums; //每个循环步创建的对象
for (const auto& nums : entry.phones) {//对每个数
if (!valid(nums)) {
badNums << " " << nums;//将数的字符串形式存入badNums
}
else
//将格式化的字符串”写入“formatted
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,分别完成电话号码验证和改变格式的功能。程序最有趣的部分是对字符串流formatted和 badNums 的使用。我们使用标准的输出运算符(<<)向这些对象写入数据,但这些“写入”操作实际上转换为string操作,分别向formatted和l badNums中的string对象添加字符。
小结
术语表