《C++Primer 第五版》——第八章 IO 库
C++语言不直接处理输入输出,而是通过一组定义在标准库中的class类型,即 流类类型(stream class type) 来处理IO操作。这些类支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等,还有一些类允许内存IO,即,从内存读取数据,向内存写入数据。
IO 库定义了读写内置类型值的操作。除此之外,一些类,比如 string ,通常也会定义类似的IO操作,来读写class类型自己的对象。
istream
输入流类型,提供输入操作。ostream
输出流类型,提供输出操作。cin
一个 istream 对象,从标准输入读取数据。cout
一个 ostream 对象,向标准输出写入数据。cerr
一个 ostream 对象,用户输出程序错误消息,写入到标准错误。>>
运算符,用来从一个 istream 对象读取输入数据。<<
运算符,用来从一个 ostream 对象写入输出数据。getline
函数,从一个给定的 istream 读取一行数据,存入一个给定的 string 对象中。
8.1 IO 类
实际上,我们可以实现对控制台窗口、文件内容、string 对象中的字符进行 IO 操作。此外,应用程序可能读写需要宽字符支持的语言 (2.1.1中提及)。
为了支持不同类型的IO操作,在 istream 和 ostream 之外,标准库还定义了其他一些 IO 类型,比如:
头文件 | 类型 |
---|---|
iostream | istream,wistream 从流中读取数据 ostream,wostream 向流中写入数据 iostream,wiostream 读写流 |
fstream | ifstream,wifstream 从文件中读取数据 ofstream,wofstream 向文件中写入数据 fstream,wfstream 读写文件 |
sstream | istringstream,wistringstream 从 string 对象中读取数据 ostringstream,wostringstream 向 string 对象中写入数据 stringstream,wstringstream 读写 string 对象 |
其中头文件 iostream 定义了用于读写流的基本类型;fstream 定义了读写命名文件的类型;sstream 定义了读写内存 string 对象的类型。
同时,这些 IO 类型还分为两个版本,除了常规版本外,还有一个专门处理宽字符的版本,这个版本的class类型和函数的名字以一个 w 开头。例如wcin
、wcout
和wcerr
是分别对应cin
、cout
和cerr
的宽字符版对象。宽字符版本的类型和对象与其对应的普通 char 版本的类型定义在同一个头文件中,例如,头文件 fstream 定义了 ifstream
和 wifstream
类型。
IO 类型间的关系
概念上,设备类型和字符大小都不会影响到要执行的 IO 操作。例如,我们可以用 >>
读取数据,而不用管是从一个控制台窗口,一个磁盘文件,还是一个 string 对象中读取数据。类似的,也不用管读取的字符能存入一个 char 变量内,还是需要一个 wchar_t
类型对象。
标准库使得程序员可以无视不同类型的流之间的差异,这是通过 继承机制(inheritance) 实现的。
利用模板,程序员可以使用具有继承关系的class类型,而不必了解继承机制工作的细节(后面将在第15章和18.3节介绍C++如何支持继承机制)。
简单说,继承机制使程序员可以声明一个特定的类继承自另一个类。我们通常可以将一个派生类(子类,又叫继承类)对象当作基类(父类,又叫所继承的类)对象来使用,这是很重要的一个事情。
举个简单的例子,类型 ifstream
和 istringstream
都继承自 istream
。因此,可以像使用 istream
对象一样来使用 ifstream
和 istringstream
对象。换句话说,可以像使用 cin
一样使用这些类型的对象。比如,可以对一个 ifstream
或 istringstream
对象调用 getline
函数,也可以使用 >>
运算符从一个 ifstream
或 istringstream
对象中读取数据。
Note:
下面介绍的标准库特性都可以无差别应用于普通流、文件流和 string 流及 char 或宽字符流版本。
8.1.1 IO 对象无拷贝或赋值
如下所示,对于标准库的 IO 类型,无法拷贝 IO 对象或对 IO 对象赋值:
ofstream out1, out2;
out1 = out2; // error: 不能对流对象赋值
ofstream print(ofstream); // error: 不能初始化ofstream参数
out2 = print(out2); // error: 不能拷贝流对象
因为不能拷贝 IO 对象,所以不能将函数形参类型或返回类型设置为流class类型。进行 IO 操作的函数通常使用引用参数来 传递和返回 流。又因为读写 IO 对象会改变它的状态,所以引用不能是 const 。
8.1.2 条件状态
IO 操作可能会发生错误,其中一些是可恢复的,而其他错误则发生在系统内部,超出了应用程序可修正的范围。
下面的表格列出了 IO 类定义了一些函数和标志,它帮助用来访问和操作流的条件状态 (condition state)。
函数或标志 | IO 库条件状态的说明 |
---|---|
strm::iostate | strm 是一个标识符,它可以是标准库中列出的任意一种 IO 类型。iostate 是一种依赖机器的整型,代表流的条件状态。 |
strm::badbit | strm::iostate 值,用来指示流已崩溃 |
strm::failbit | strm::iostate 值,用来指示一个 IO 操作失败。 |
strm::eofbit | strm::iostate 值,用来指示流到达文件结束。 |
strm::gootbit | strm::iostate 值,用来指示流不处于错误状态。该值保证为零。 |
s.eof() | s 是一个标识符,表示标准库中列出的任意一种 IO 类型的对象,比如 cin 和 cout 。 若流 s 的 eofbit 置位,则返回 true。 |
s.fail() | 若流 s 的 failbit 或 badbit 置位,则返回 true。 |
s.bad() | 若流 s 的 badbit 置位,则返回 true。 |
s.good() | 若流 s 处于有效状态,则返回 true。 |
s.clear() | 将流 s 中所有条件值重置为有效状态。返回 void。 |
s.clear(flags) | 将 s 中的条件重置为 flags 。flags 的类型是 strm::iostate 。返回 void 。 |
s.setstate(flags) | 添加指定的条件到 s 。flags 的类型是 strm::iostate 。返回 void 。 |
s.rdstate() | 返回 s 的当前状态,返回类型为 strm::iostate 。 |
下面是一个简单的 IO 错误例子:
int ival;
cin >> ival;
如果我们在标准输入上用键盘输入 Boo ,读操作就会失败。因为代码中的输入运算符期待读取一个 int 类型的值,但却先得到了一个字符 B 。这样, IO 对象 cin 会进入错误状态。类似的,如果我们输入一个文件结束标识,cin 也会进入错误状态。
一个流一旦发生错误,其后续的 IO 操作都会失败。只有当一个流处于无错状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用:
while (cin >> word)
// ok: 读取操作成功
if(cin);
While 循环检查 >> 表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为 true 。
查询流的状态
将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。有时我们也需要知道流为什么失败。 例如,在键入文件结束标识后我们的应对措施,可能与遇到一个 IO 设备错误的处理方式是不同的。
因此 IO 库定义了一个与机器无关的 iostate 类型,它提供了表达流状态的完整功能。这个类型应作为一个位的集合来使用,IO 库定义了 4 个 iostate 类型的 constexpr 值,用于表示特定的位模式。这些值用来表示特定类型的 IO 条件,可以与位运算符一起使用,从而一次性检测或设置多个标志位。
关于流的状态标志
①
badbit
表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit
被置位(即代表badbit
的那一位设置为1),流就无法再使用了。
②在发生可恢复错误后,failbit
被置位。如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
③如果到达文件结束位置,eofbit
和failbit
都会被置位。
④goodbit
的值为 0 (即没有一位发生置位),表示流未发生错误。
so:
如果badbit
,failbit
和eofbit
任一个被置位,则检测流状态的条件会失败。
标准库还定义了一组成员函数来查询这些标志位的状态。 good()
成员函数在所有错误位均未置位的情况下返回 true,而 bad()
、 fail()
和 eof()
则在对应错误位被置位时返回 true。此外,在 badbit
被置位时,fail()
也会返回 true。这意味着,使用 good()
或 fail()
是确定流的总体状态的正确方法。
管理条件状态
标准库为 IO 类型定义了一些成员函数。下面用 strm 来指代特定的 IO 类型。
strm 类型的流对象的 rdstate()
成员函数返回一个 strm::iostate
类型的值,对应流的当前状态。
setstate(flags)
成员函数将对象的指定条件位置位(即 flags ,这是一个 strm::iostate
类型的值),表示发生了其对应的错误。
clear()
是一个重载的成员函数:它有一个不接受参数的版本,而另一个版本接受一个 strm::iostate
类型的参数。
其中不接受参数的版本会清除所有错误标志位(即复位全部位)。执行 clear()
成员函数后,调用 good()
会返回 true 。我们可以这样使用这些成员:
// 己住 cin 的当前状态
auto old_state = cin.rdstate(); // 记住 cin 的当前状态
cin.clear(); // 使 cin 有效
process_input(cin); // 使用 cin
cin.setstate(old_state); // 将 cin 置为原有状态
带参数的版本接受一个 strm::iostate
类型的值,表示流的新状态。为了复位单一的条件状态位,我们首先用 rdstate()
成员函数读出流对象的当前条件状态,然后用位操作将所需位复位来生成新的状态。
例如,下面的代码将 failbit 和 badbit 复位,但保持 eofbit 不变:
// 复位 failbit 和 badbit, 其他位置保持不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
8.1.3 管理输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面代码:
cout << "please enter a value..";
文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升——即将多次写操作合并为一次写操作,减少了写操作的次数。
输入/输出操纵符(Input/output manipulators)
标准库定义了一组输入/输出操纵符来控制该流的格式状态。
导致缓冲刷新(即数据真正写到输出设备或文件)的原因有很多:
- 程序正常结束,作为
main()
函数的return
操作的一部分,缓冲刷新被执行。- 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
- 我们可以使用操纵符如
endl
来显式刷新缓冲区。- 在每个输出操作之后,我们可以用操作符
unitbuf
设置流的内部状态,来清空缓冲区。默认情况下,对关联到标准错误的一个 iostream 对象,即cerr
是设置unitbuf
的,因此写到cerr
的内容都是立即刷新的。- 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,
cin
和cerr
都关联到cout
。因此,读cin
或写cerr
都会导致cout
的缓冲区被刷新。
刷新输出缓冲区
我们已经使用过操纵符 endl
,它负责完成换行并刷新缓冲区的工作。IO 库中还有两个类似的操纵符: flush
和 ends
。 flush
刷新缓冲区,但不输出任何额外的字符。而 ends
向缓冲区插入一个空字符,然后刷新缓冲区。
值得一提的是,cout
所属 ostream
类中还提供有 flush()
成员函数,它和 flush
操纵符的功能完全一样,仅在使用方法上( 即cout.flush()
)有区别。
cout << "hi" << endl; // 输出hi和一个换行,然后刷新缓冲区
cout << "hi" << flush; // 输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi" << ends; // 输出hi和一个空字符,然后刷新缓冲区
unitbuf 操纵符
如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf
操纵符。unitbuf
告诉流在接下来的每次写操作之后都进行一次 flush
操作。而 nounitbuf
操纵符则重置流, 使其恢复使用正常的系统管理的缓冲区刷新机制。
cout << unitbuf; //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf; //回到正常的缓冲方式
警告:如果程序崩溃,输出缓冲区不会被刷新
如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。
当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。 标准库将 cout 和 cin 关联在一起,因此下面语句导致 cout 的缓冲区被刷新:
cin >> ival;
Note: 交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。
tie()
成员函数可以用来为流对象绑定输出流,它有两个重载的版本:
第一个版本不带参数,返冋指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个输出流的指针,如果该对象未关联到流,则返回空指针。
第二个版本接受一个指向输出流的指针,将本对象关联到此输出流,比如 x.tie(&o)
将流 x 关联到输出流 o。
tie
成员函数的函数声明如下(适用于任何一个输出流类型):
//返回指向绑定的输出流的指针。
basic_ostream<char_type,traits_type>* tie() const;
//将 os 指向的输出流绑定的该对象上,并返回上一个绑定的输出流指针。
basic_ostream<char_type,traits_type>* tie(basic_ostream<char_type,traits_type>* tiestr);
既可以将一个输入流对象关联到另一个输出流,也可以将一个输出流关联到另一个输出流:
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
。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个输出流, 但多个流可以同时关联到同一个输出流。
8.2 文件输入输出
头文件 fstream 定义了三个类型来支持文件IO:ifstream
从一个给定文件读取数据,ofstream
向一个给定文件写入数据,以及fstream
可以读写给定文件。在17.5.3节中将介绍如何对同一个文件流既读又写。
这些类型提供的操作与之前已经使用过的对象cin
和cout
的操作一样。特别是可以用IO运算符(<<
和>>
)来读写文件,可以用getline
函数从一个ifstream
读取数据。也就是说,包括8.1节中介绍的操作也都适用于这些类型。
除了继承自iostream
类型的行为之外,头文件 fstream 中定义的类型还增加了一些新的成员来管理与流关联的文件。在下面列出了这些操作,我们可以对fstream
,ifstream
和ofstream
对象调用这些操作,但不能对其他IO类型调用这些操作。
头文件 fstream 中定义的这些类型的单参数构造函数都都是 explicit
的,即不能通过隐式转换参数。
头文件 fstream 中定义的类型的特有操作 | 操作详情 |
---|---|
fs fstrm; | 创建一个未绑定的文件流。 fs 是定义在 fstream 头文件的一个类型。 |
fs fstrm(s); | 创建一个 fs 对象,并打开名为 s 的文件。 s 可以是 string 类型或C风格字符串指针。头文件 fstream 中定义的这些类型的构造函数都是 explicit 的。 默认文件的打开模式 mode 取决于是什么类型。 |
fs fstrm(s,mode); | 与前一个构造函数类似,但是按指定模式mode 打开文件。其中mode 是定义在头文件 fstream 中的一系列标识符。 |
fstrm.open(s) | 打开名为 s 的文件,并将文件与对象绑定。 默认文件的打开模式 mode 取决于是什么类型。 s 可以是 string 类型或C风格字符串指针。返回 void。 |
fstrm.open(s,mode) | 以给定的 mode 打开 s,并并将文件与对象绑定。 |
fstrm.close() | 关闭与该对象绑定的文件。返回 void。 |
fstrm.is_open() | 返回一个 bool 值,指示与该对象关联的文件是否成功打开且尚未关闭。 |
8.2.1 使用文件流对象
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了名为 open
和 close
的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则 open
成员函数会自动被调用:
ifstream in(ifile); // 构造一个 ifstream 对象,打开给定文件并将双方绑定
ofstream out; // 输出文件流out未关联到任何文件
这段代码定义了一个输入流 in ,它被初始化为从文件读取数据,文件名由参数 ifile 指定。第二条语句定义了一个输出流out,未与任何文件关联。在新C++标准中,文件名既可以是库类型string对象,也可以是C风格字符数组(参见3.5.4节)。旧版本的标准库只允许C风格字符数组。
用 fstream 代替 iostream&
在8.1节已经提到过,在要求使用父类对象的地方,我们可以用子类对象来替代。
这意味着,接受一个iostream
类型引用(或指针)参数的函数,可以用一个对应的fstream
或sstream
类型的复合类型来调用。也就是说,如果有一个函数接受一个ostream&
参数,我们在调用这个函数时,可以传递给它一个ofstream
对象,对istream&
和ifstream
也是类似的。
成员函数 open和 close
如果我们定义了一个空文件流对象(即未绑定文件的文件流对象),可以随后调用 open
成员函数来将它与文件关联起来:
ifstream in(ifile); // 构筑一个ifstream并打开给定文件
ofstream out; // 输出文件流未与任何文件相关联
out.open(ifile + ".copy"); // 打开指定文件
如果调用open失败, failbit
会被置位。 因为调用open
可能失败,进行open是否成功的检测通常是一个好习惯:
if(out) // 检查open是否成功
// open成功,我们可以使用文件了
这个条件判断与我们之前将cin
用作条件相似。如果open失败,条件会为假,我们就不会去使用out
了。
一旦一个文件流已经打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用open会失败,并会导致 failbit
被置位。而随后的试图使用该文件流的操作自然都会失败。 为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。一旦文件成功关闭,我们可以打开新的文件:
in.close(); // 关闭文件
in.open(ifile + "2"); // 打开另一个文件
如果open成功,则open会设置流的状态,使得其调用good()
返回 true 。
自动构造和析构
考虑这样一个程序,它的main
函数接受一个要处理的文件列表(参见6.2.5节 mian:处理命令行选项 )。这种程序可能会有如下的循环:
//对每个传递给程序的文件执行循环操作
for (auto p = argv + 1; p < argv + argc; ++p) {
std::ifstream input(*p); //创建输出流对象input并打开文件p
if (input){ //如果文件打开成功,“处理”此文件
process(input);
} else{
std::cerr << "couldn`t open: " + std::string(*p);
}
}//每个循环步input都会离开作用域,因此会被销毁再在下一次循环中被创建
每个循环步构造一个新的名为input的ifstream对象,并打开它来读取给定的文件。像之前一样,我们检查open是否成功。如果成功,将文件传递给一个函数,该函数负责读取并处理输入数据。如果open失败,打印一条错误信息并继续处理下一个文件。因为input是while循环的局部变量,它在每个循环步中都要创建和销毁一次。当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。在下一步循环中,input会再次被创建。
当一个 fstream 对象被销毁时,close
成员函数会自动被调用。
8.2.2 文件模式
每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。下面列出了文件模式和它们的含义。
文件模式 | 含义 |
---|---|
in | 以读方式打开 |
out | 以写方式打开 |
app | 每次写之前定位到文件末尾(追加) |
ate | 打开文件后立即定位到文件末尾 |
trunc | 截断文件 |
binary | 以二进制方式进行 IO 操作 |
无论用哪种方式打开文件,我们都可以指定文件模式,调用open
成员函数显式打开文件时可以,用一个文件名初始化流来隐式打开文件时也可以。
有的文件模式可以同时指定。
通过|
可以组合文件模式,比如: ofstream::out | ofstream::trunc
指定文件模式有如下限制:
- out 只能设定于 ofstream 或 fstream 对象。
- in 只能设定于 ifstream 或 fstream 对象。
- 只有在指定 out 时,才能设定 trunc。
- 只要没有设定 trunc,就可以设定 app 模式。如果指定 app,文件总是以输出模式打开,即使没有显式指定 out。
- 默认情况下,以 out 模式打开的文件会被截断,即使没有指定 trunc。为了保存以 out 打开的文件的内容,可以指定 app,这样只会将数据写入到文件末尾;或者指定 in,这样文件可以同时用于输入和输出。
- ate 和 binary 模式可用于任何文件流类型对象,且可与其他任何模式组合。
每个文件流类型的默认文件模式:
- ifstream 关联的文件默认以 in 模式打开;
- ofstream 关联的文件默认以 out 模式打开(虽然默认参数没有 trunc ,但是实际上同时使用了);
- 与 fstream 关联的文件默认以 in 和 out 模式打开。
以out模式打开文件会丢弃已有数据
默认情况下,当我们打开一个ofstmam时,文件的内容会被丢弃。
// 在这几条语句中,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);
Note: 保留被ofstream
对象打开的文件中已有数据的唯一方法是显式指定 app 或 in 模式。因为 ofstream 关联的文件默认以 out 模式打开,会抹去文件已存在的数据。
每次调用open时都会确定文件模式
对于一个给定流,每当(显式或隐式)打开文件时,都可以改变其文件模式。
ofstream out;// 未指定文件打开模式
out.open("scratchpad");// 模式隐合设置为out和trunc
out.close();// 关闭out,以便将其用于其他文件
out.open("precious", ofstream::app);// 模式为out和app,默认是out
out.close();
第一个open
调用未显式指定输出模式,文件隐式地以 out 模式打开。通常情况下,使用 out 模式意味着同时使用 trunc 模式。因此,当前目录下名为scratchpad的文件的内容将被清空。当打开名为precious的文件时,我们指定了 append 模式(app)。文件中已有的数据都得以保留,所有写操作都在文件末尾进行。
Note: 在每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值。
8.3 string 流
头文件 sstream 定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是一个IO流一样。
istringstream
从string读取数据,ostringstream
向string写入数据,而stringstream
既可从string读数据也可向string写数据。与头文件fstream类似,头文件sstream中定义的类型都继承自iostream头文件定义的类型。除了继承得来的操作,sstream中定义的类型还增加了一些成员来管理与流相关联的string。
头文件sstream中定义的这些类型的单参数构造函数都是explicit
的,,即不能通过隐式转换参数。
下面列出了一部分特有操作,可以对string流对象调用这些操作,但不能对其他IO类型调用这些操作。
头文件 sstream 定义的类型的特有操作 | 详解 |
---|---|
ss strm; | strm 是一个未绑定的string流对象。 ss 指代头文件 sstream 中定义的一个类型 |
ss strm(s); | strm 是一个ss 对象,保存 string 对象 s 的一个拷贝。此构造函数是explicit 的 |
strm.str() | 返回 strm 所保存的 string 的拷贝 |
strm.str(s) | 将 string 对象 s 拷贝到 strm 中,并返回 void 。 |
8.3.1 使用 istringstream
当我们的某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,通常可以使用 istringstream
。
考虑这样一个例子,假定有一个文件,列出了一些人和他们的电话号码。某些人只有一个号码,而另一些人则有多个——家庭电话、工作电话、移动电话等。这种输入文件看起来可能是这样的:
文件中每条记录都以一个人名开始,后面跟随一个或多个电话号码。
morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000
首先定义一个简单的类来描述输入数据:
// struct 成员默认为公有
struct Personinfo {
string name;
vector<string> phones;
};
类型Personinfo的对象会有一个成员来表示人名,还有一个vector
容器来保存此人的所有电话号码。
下面程序会读取数据文件,并创建一个储存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 中定义了一个局部Personlnfo对象,来保存当前记录中的数据。
接下来将一个 istringstream
与刚刚读取的文本行进行绑定,这样就可以在 此 istringstream
上使用输入运算符来读取当前记录中的每个元素。首先读取人名,随后用一个 while 循环读取此人的电话号码。
当读取完line中所有数据后,内层 while 循环就结束了。此循环的工作方式与前面章节中读取 cin
的循环很相似,不同之处是,此循环从一个 string
而不是标准输入 读取数据。当 string 中的数据全部读出后,同样会触发“文件结束”信号,在 record 上的下一个输入操作会失败。
将刚刚处理好的Personinfo追加到 vector
中,外层 while 循环的一个循环步就随之结束了。外层 while 循环会持续执行,直至遇到 cin
的文件结束标识。
8.3.2 使用 ostringstream
当逐步构造输出,希望最后一起打印时, ostringstream
是很有用的。例如,对上一节的例子,可能想逐个验证电话号码并改变其格式。如果所有号码都是有效的,则希望输出一个新的文件,包含改变格式后的号码。对于那些无效的号码,我们不会将它们输出到新文件中,而是打印一条包含人名和无效号码的错误信息。
由于不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作。但是可以先将输出内容“写入”到一个内存 ostringstream
对象中:
for (const auto &entry : people) // 对people中每一项
{
ostringstream formatted, badNums; // 每个循环步创建一个对象
for (const auto &nums : entry.phones) // 对每个数
{
if (!validTel(nums))
badNums << " " << nums; // 将数的字符串形式存入badNums
else
// 将格式化的字符串"写入"formatted
formatted << " " << (nums);
}
if (badNums.str().empty()) // 没有错误的数
os << entry.name << " " // 打印名字到os绑定的string流中
<< formatted.str() << endl; // 与格式化的数
else
// 否则,打印名字和错误的数
cerr << "input error: " << entry.name
<< " invalid number(s) " << badNums.str() << endl;
}
在此程序中,假定已有两个函数,valid和format,分别完成电话号码验证和改变格式的功能。程序最有趣的部分是对字符串流formatted和badNums的使用。我们使用标准的输出运算符(<<
)向这些对象写入数据,但这些“写入操作实际上转换为 string
操作,分别向formatted和badNums中的 string
对象添加字符。