C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO.这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即,从string读取数据,向string写入数据。
IO库定义了读写内置类型值的操作。此外,一些类,如string, 通常也会定义类似的IO操作,来读写自己的对象。
我们曾经用过的很多IO库设施,列举一下大部分用过的IO库设施:
istream(输入流)、ostream(输出流)类型,提供输入输出操作。cin、cout,分别对应istream和ostream对象,从标准输入读取数据,向标准输出写入数据。cerr,一个ostream对象,通常用于输出程序错误消息,写入到标准错误。>>运算符,用来从一个istream对象读取输入数据;<<运算符,用来向一个ostream对象写入输出数据。getline函数,从一个给定的istream读取一行数据,存入一个给定的string对象中。
IO类
到目前为止,我们已经使用过的IO类型和对象都是操纵char数据的。默认情况下,这些对象都是关联到用户的控制台窗口,同时,使用IO操作处理string中的字符会很方便。此外,应用程序还可能读写需要宽字符支持的语言。(什么是宽字符?比如中文)
概念上,设备类型和字符大小都不会影响我们要执行的IO操作。例如,我们可以用>>读取数据,而不需要管是从一个控制台窗口,一个磁盘文件,还是一个string读取。
标准库使我们能忽略这些不同类型的流之间的差异,这部分是通过继承机制(inheritance)实现的。
利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。
如果涉及到写入读取的各种细节机制,比如iostream、fstream、sstream等头文件所对应的各种应用场景,那将是非常繁杂的处理机制。
下面将针对IO对象进行细节描述:
IO对象无拷贝或赋值
1 2 3 4 5 6
ofstream out1, out2; out1 = out2; // 错误:不能对流对象赋值 // 下面的print函数估计都没有传引用参,这份笔记不是那么完整 ofstream print(ofstream); // 错误:不能初始化ofstream参数,因为不能赋值 out2 = print(out2); // 错误:不能拷贝流对象
不能将形参或者返回类型设置为流类型,因为无法对其进行拷贝,进行IO操作的函数通常以引用方式传递和返回流;
读写一个IO对象会改变其状态,因此传递和返回的引用不能是
const;
流(stream)的条件状态(condition state)
IO操作可能发生错误,有些是可恢复的,有些则是发生于操作系统内部,已经超出了应用程序可修正的范围。
书本中有一系列关于IO库条件状态的细节展示,在这里暂且不表。举一个简单的例子:1
2
3
int ival;
cin >> ival; // 如果键入的是char类型或者string,读操作就会失败
// cin将进入错误状态,如果输入的是一个文件结束标识,cin同样进入错误状态。
一个流一旦发生错误,其后续的IO操作都会失败,只有一个流处于无错状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。
1
while (cin >> word) // 正常则....
查询流的状态
将流作为条件使用,可以让我们知晓流是否有效,而无法告诉我们具体发生了什么,为了更进一步了解到流失败的具体原因,IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。IO库定义了4个iostate类型的constexpr值,表示特定的位模式。这些值用来表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
badbit表示系统级错误,如不可恢复的读写错误,一旦发生将被置位。failbit表示可恢复错误,一旦发生将被置位,如期望读取数值却读出一个字符。eofbit表示读取到了文件结束符位置,一旦到达,eofbit和failbit都会被置位。goodbit的值为0,表示流未发生错误,前三者任何一个被置位,则检测流状态的条件会失败。
标准库还定义了一组函数来查询这些标志位的状态。操作good应该是一个成员函数在所有错误位均未置位的情况下返回true,而bad、fail、eof则在对应错误位被置位时返回true。
- 因为各自所表示的都是标志位,错误标志位返回信息意味着出现了对应的错误;
此外,在badbit被置位时,fail也会返回true。因此使用good或fail是确定流的总体状态的正确方法。而eof和bad操作只能表示特定的错误。
管理条件状态
主要是以下几个方面:
- 流对象的
rdstate成员返回一个iostate值,对应流的当前状态。 setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员:分为不接受参数的版本以及接受一个iostate类型的参数。不接受参数的版本清除(复位)所有错误标志位,调用
good后会返回true。1 2 3 4
auto old_state = cin.rdstate(); // 记住cin的当前状态 cin.clear(); // 清除所有错误标志位 process_input(cin); // 使用一次cin cin.setstate(old_state); // 将cin置为原有状态
带参数的版本接受一个
iostate值,表示流的新状态。一般是先用rdstate读出当前的条件状态,然后用位操作将所需位复位来生成新的状态。1 2 3 4 5 6
// 先前的理解一直有误,在此重新更新 // failbit是一个固定常数,以8位二进制为例,他代表的是00000001,这个二进制对应了failbit // 对他取反,再做与运算,意味着将对应位(也就是最后一位)进行复位,该位重新变为0 // badbit同样是一个固定数,它代表的是00000100,取反再做与运算,意味着倒数第三位的会变成0 // 就是这么简单,之前想复杂了 cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
怎么理解某个位被置位呢,以
badbit为例,出现了系统级错误,那么位状态的第三个位置就被置为1,则如果我们想要将该位进行复位,就需要通过上述的位操作运算来进行处置。为了保证所有的处置可以正常进行,IO库设置了一些特定的枚举状态,比如说badbit对应的枚举状态就是00000100,也就是我们所说的4。
管理输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据,通过缓冲机制,操作系统可以将程序的多个输出操作组合成单一的系统级写操作,从而带来性能提升。导致缓冲刷新的原因有很多,详细情形以下将做出一个大体的介绍:
刷新输出缓冲区
endl操纵符完成换行并刷新缓冲区的工作,类似的操纵符:flush和ends。flush刷新缓冲区,但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新。1 2 3
cout << "hi!" << endl; //刷新,换行 cout << "hi!" << flush; //刷新,不附加任何额外字符 cout << "hi!" << ends; //刷新,输出一个空字符
unitbuf操纵符unitbuf操纵符可以在每次输出操作后都刷新缓冲区,它告诉流在接下来的每次写操作之后都进行一次flush操作。nounitbuf则重置流,将使用正常的系统管理的刷新机制。Notes: 如果程序崩溃,输出缓冲区将不会被刷新,也就是说,它所输出的数据很可能停留在输出缓冲区中等待打印,这一点在调试的过程中尤为注意。
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。
标准库将
cout和cin相互关联,cin操作会导致cout缓冲区被刷新,反之类似。关联操作通过tie函数实现,它有两个重载的版本:- 不带参数的版本返回指向输出流的指针,如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,若未关联到流则返回空指针。
带参数的版本接受一个指向
ostream的指针,将自己关联到此ostream.1 2 3 4
cin.tie(&cout); // 仅仅用来展示,将cout与cin关联 ostream *old_tie = cin.tie(nullptr); // 不关联或者说取消关联 cin.tie(&cerr); // 将cin与cerr关联,不是个好主意 cin.tie(old_tie); // 重建cin和ostream对象(cout)之间的正常关联
文件输入输出
头文件fstream(文件流)定义了三个类型来支持文件IO:
ifstream(读取文件流)从一个给定文件读取数据;ofstream(输出文件流)向一个给定文件写入数据;fstream(文件流)则可以读写给定文件。
这些类型可以用IO运算符(<<或>>)来读写文件,可以用getline从一个ifstream读取数据。除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流相关的文件。
使用文件流对象
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成员函数,它完成一些系统相关的工作,来定位给定的文件,并视情况打开为读或写模式。
创建文件流对象时,我们可以提供文件名(可选)。如果提供了一个文件名,则open会自动被调用:
1
2
ifstream in(ifile); // 构造一个ifstream并打开给定文件,infile在这里一般是文件路径
ofstream out; // 输出文件流未关联到任何文件,先定义了一个输出流
代码定义了一个输入流in,它被初始化为从文件读取数据,文件名由string类型的参数ifile指定。第二条语句定义了一个输出流out,未与任何文件关联。
在新C++标准中,文件名既可以是库类型string对象,也可以是C风格字符数组,旧版本的标准库只允许C风格字符数组。
用fstream代替iostream&
在要求使用基类型对象的地方,我们可以用继承类型的对象来替代,接受一个iostream类型引用(或指针)参数的函数,可以用一个对应的fstream类型调用;如果接受一个ostream&参数,在调用这个函数时,可以传递给它一个ofstream对象。
这里涉及到的是对象的继承策略,iostream应该是IO对象的基类。
自动构造和析构
考虑一个程序,它的main函数接受一个要处理的文件列表:
1
2
3
4
5
6
7
8
9
// 第一个参数默认应该是程序名字
for (auto p = argv + 1; p != argv + argc; ++p)
{
ifstream input(*p); // 创建输出流并打开文件,p为指针,代表文件地址,*p代表某文件
if (input) {
process(input);
} else
cerr << "couldn't open: " + string(*p);
} // 每个循环步input都会离开作用域,因此会被销毁
因为input是while循环的局部变量,它在每个循环步中都要创建和销毁一次,当一个fstream对象被销毁时,close会自动被调用。
成员函数
成员函数
open和close1 2 3 4
ifstream in(ifile); // 构筑一个ifstream并打开给定文件 ofstream out; // 输出文件流未与任何文件相关联(也是定义了一个文件输出流对象) out.open(ifile + ".copy"); // 要通过这个输出对象,打开指定文件 if(out) // 检测open是否成功,调用失败,failbit会被置位
一旦一个文件流已经打开,他就保持与对应文件的关联。对一个已经打开的文件流调用
open会失败,并会导致fallbit被置位。随后的试图使用文件流的操作都会失败。为了将文件流关联到另外一个文件,首先关闭已经关联的文件。一旦文件成功关闭,我们可以打开新的文件:
1 2
in.close(); // 关闭文件,这一步确实重要 in.open(ifile + "2"); // 打开另一个文件,open成功,则good()为true.
文件模式
每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。
| 文件模式 |
|---|
| in:以读方式打开 |
| out:以写方式打开 |
| app:每次写操作前均定位到文件末尾 |
| ate:打开文件后立即定位到文件末尾 |
| trunc:截断文件 |
| binary:以二进制方式进行IO |
无论用哪种方式打开文件,指定文件模式都有限制:
- 只可以对
ofstream或fstream对象设定out模式(out理解为将缓冲区内的内容->文件,即写入模式) - 只可以对
ifstream或fstream对象设定in模式(in理解为将文件的内容->缓冲区,即读取模式) - 只有当
out也被设定时才可设定trunc模式(截断模式一般配合写方式) - 只要
trunc没被设定,就可以设定app模式,在app模式下,即使没有显式指定out模式,文件也总以输出方式被打开; - 默认情况下,即便没有指定
trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,打开文件的同时进行读写操作。 ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。
- 与
ifstream关联的文件默认以in模式打开;与ofstream关联的文件默认以out模式打开;与fstream关联的文件默认以in和out模式打开。
以out模式打开文件会丢弃已有数据
默认情况下,当我们打开一个ofstream时,文件的内容会被丢弃(写方式)。阻止一个ofstream清空给定文件内容,解决方案:
1
2
3
4
5
ofstream out("file1"); // 隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out); // 等价于上
ofstream out3("file1", ofstream::out | ofstream::trunc); // 明摆着打开并截断文件
ofstream app("file2", ofstream::app); // 隐含为追加写模式
ofstream app2("file2", ofstream::out | ofstream::app); // 显式追加
在上述代码中,|符号所表示的含义
- 理解为或者的关系即可;
每次调用open时都会确定文件模式
对于一个给定流,每当打开文件时,都可以改变其文件模式。
1
2
3
4
5
ofstream out; // 未指定文件打开模式
out.open("scratchpad"); // 模式隐含设置为输出和截断
out.close(); // 关闭out,以便我们将其用于其他文件
out.open("precious", ofstream::app); // 模式为输出和追加
out.close();
第一个open调用未显式指定输出模式,文件隐式地以out模式打开,也意味着同时使用了trunc模式;当打开precious文件时,我们指定了app模式。文件中已有的数据都得以保留,所有写操作都在文件末尾进行。
Notes:在每次打开文件时,都要设置文件模式,这一点是较重要的步骤。
string流
sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入、读取数据,类似一个IO流。
istringstream类型:从string读取数据;ostringstream类型:从string写入数据;stringstream类型:既可以从string读数据也可向string写数据,类似fstream的功能;
与fstream类型类似,头文件sstream中定义的类型都继承自我们已经使用过的iostream头文件中定义的类型。
除了继承得来的操作,还增加了一些成员来管理与流相关联的string,stringstream有一些特有的操作!
使用istringstream
当我们的某些工作是对整行文本进行处理,而其他的一些工作是处理行内的单个单词时,通常考虑使用istringstream。
1
2
3
4
5
# 比如我们考虑这么一个场景:
# 有一份文件,列出了一些人和他们的电话号码。某些人只有一个号码,另一些人有多个,那么输入文件可能是:
# morgan 2015552368 8625550123
# drew 9735550130
# lee 6095550132 2015550175 8005550000
我们需要针对上述人的号码进行针对性保存,因此考虑设计这么一个类:
1
2
3
4
struct PersonInfo {
string name; // 定义人名
vector<string> phones; // 定义号码数组,数组中每个元素都是号码
};
设计一个读取数据文件的程序,创建一个PersonInfo的vector。vector中每个元素对应文件中的一条记录。
1
2
3
4
5
6
7
8
9
10
string line, word; // 分别保存来自输入的一行和号码(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); // 最后保存该记录的信息
}
上述内层的while循环在string中的数据全部读出后,即会触发文件结束信号,则record下一个操作就会失败。
istringstream对象并不从屏幕读入数据,而是从一个string对象中读取数据。
使用ostringstream
当我们逐步构造输出,希望最后一起打印时,ostringstream是比较有用的。接着上述案例,如果号码都是有效的,我们希望输出一个新的文件,包含改变格式后的号码。对于无效号码,我们将不会输出,而是打印一条包含人名和无效号码的错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (const auto &entry : people) { // 对people中的每一项
ostringstream formatted, badNums; // 每个循环步创建的对象
for (const auto &nums : entry.phones) { // 对每个号码
if(!valid(nums)) {
badNums << " " << nums; // 将数的字符串形式存入badNums(有点像cin的用法)
} 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,完成号码验证以及改变格式的功能。
上述代码对字符串流formatted和badNums的使用比较有趣,使用标准输出运算符<<向这些对象写入数据,但写入操作实际上转换为string操作,分别向formatted和badNums中的string对象添加字符。
ostringstream对象并不直接输出到屏幕,而是将其内部的字符串缓存起来,可以通过str()方法获取这个字符串,再通过其他方式将其输出到屏幕或者其他地方。
总结:
- 我们称
istringstream为输入string流,就是读入string数据信息,读入的信息保存到istringstream对象中; - 称
ostringstream为输出string流,就是将要输出的信息保存至ostringstream对象中,因此会有<<和>>;
IO库的几个要点:
iostream处理控制台IO;fstream处理文件内容IO;stringstream完成内存string的IO。- 类
fstream和stringstream继承自类iostream;其中输入类继承自istream,输出类继承至ostream; - 因此可以在
istream对象上执行的操作,也可在ifstream或istringstream对象上执行;
有关IO流的一些使用说明,见Github源码;