C++ Primer 总结索引 | 第八章:IO库

1、IO类

1、已经使用过的IO类型和对象 都是 操纵char数据的。默认情况下,这些对象 都是关联到 用户的控制台窗口的

但是 不能仅从控制台窗口 进行IO操作,应用程序 需要 读写命名文件,使用IO操作 处理string中的字符 会很方便,此外 引用程序还可能 读写需要 宽字符支持的语言

2、为了支持 这些不同种类的 IO处理操作,在istream和ostream之外,标准库 还定义了 其他一些IO类型

下表列出的类型 分别 定义在 三个独立的头文件中:iostream 定义了 用于 读写流的基本类型,fstream 定义了 读写命名文件的类型,sstream 定义了 读写内存string对象的类型

头文件 类型
iostream istream,wistream 从流读取数据;ostream,wostream 向流写入数据;iostream,wiostream 读写流
fstream ifstream,wifstream 从文件读取数据;ofstream,wofstream 向文件写入数据;fstream,wfstream 读写文件
sstream istringstream,wistringstream 从string读取数据;ostringstream,wostringstream 向string写入数据;stringstream,wstringstream读写string

为了支持 使用宽字符的语言,标准库 使用了 一组类型和对象 来操纵 wchar_t 类型的数据

宽字符版本的类型 和 函数的名字 以一个w开始,例如:wcin、wcout和wcerr 是分别对应cin、cout和cerr的 宽字符版对象

宽字符版本的类型 和 对象 与其对应的普通char版本的类型 定义在 同一个头文件中。例如:头文件fstream定义了ifstream和wifstream类型

3、IO类型间的关系:设备类型和字符大小 都不会影响 要执行的IO操作。可以用 >> 读取数据,而不用管是 从一个控制台窗口,一个磁盘文件,还是一个 string 读取。类似的,也不用管 读取的字符 能存入 一个char对象内,还是需要 一个wchar_t对象来存储

标准库 使能忽略 这些不同类型的流之间 的差异,这是通过 继承机制 实现的。利用模板 可以使用 具有继承关系的类

继承机制 使可以 声明一个 特定的类 继承自另一个类。通常可以 将一个派生类(继承类)对象 当作其基类(所继承的类)对象来使用

类型ifstream和istringstream都继承自 istream,可以像 使用istream对象一样 来使用ifstream和istringstream对象。跟cin一样,可以对一个 ifstream 或 istringstream 对象调用 getline,也可以使用 >> 从一个 ifstream 或 istringstream 对象中 读取数据

类似的,ofstream 和 ostringstream都继承自 ostream

本节 剩下部分 所介绍的标准库流特性 都可以 无差别地应用于 普通流、文件流和string流,以及 char或宽字符流的 版本

1.1 IO对象 无拷贝或赋值

不能拷贝 或 对IO对象赋值

cpp 复制代码
ofstream out1, out2;
out1 = out2; // 错误,不能对流对象 赋值
ofstream print(ofstream); // 错误,不能 初始化ofstream参数
out2 = print(out2); // 错误,不能拷贝流对象

由于 不能拷贝IO对象,也不能 将形参或返回类型 设置为 流类型,进行IO操作的函数通常以 引用方式传递 和 返回流。读写一个IO对象 会改变其状态,因此 传递和返回的引用 不能是const的

1.2 条件状态

1、IO类定义的 一些函数和标志,可以帮助 访问和操纵流的 条件状态

条件状态 作用
strm::iostate strm是一种IO类型,在上一张表中列出的都可以进行替换。iostate是一种机器相关的类型,提供了表达 条件状态 的完整功能
strm::badbit strm::badbit 用来指出 流已崩溃
strm::failbit strm::failbit 用来指出 一个IO操作失败了
strm::eofbit strm::eofbit 用来指出 流到达了文件结束
strm::goodbit strm::goodbit 用来指出 流未处于错误状态。此值保证为0
s.eof() 若流s的eofbit置位,则返回true
s.fail() 若流s的failbit或badbit置位,则返回true
s.bad() 若流s的badbit置位(崩溃),则返回true
s.good() 若流处于有效状态,则返回true
s.clear() 将流s中所有条件状态位复位,将流的状态设置为有效,返回void
s.clear(flags) 根据给定的flags标志位,将流s中对应条件状态位复位。flags的类型为strm::iostate,返回void
s.rdstate() 返回流s的当前条件状态,返回值类型为strm::iostate

2、一个流一旦发生错误,其上后续的IO操作 都会失败

由于 流可能处于 错误状态,因此 代码通常应该 在使用一个流之前 检查它是否处于良好状态。确定一个流对象的状态 的最简单的方法是 将它当作一个条件来使用:while (cin >> word)

3、查询流的状态:需要知道 流为什么失败。例如:在键入文件结束标识后 应对措施 与 遇到一个IO设备错误的处理方式 是不同的

IO库定义了 一个与机器无关的iostate类型,它提供了 表达流状态的完整功能,这个类型 应作为一个位集合 来使用。IO库定义了4个iostate类型的constexpr值,表示 特定的 位模式。这些值 用来表示特定类型的 IO条件,可以 与位运算符一起使用 来一次性检测 或设置多个标志位

badbit 表示系统级错误,如 不可恢复的读写错误,如 不可恢复的读写错误。一旦 badbit 被置位,流就无法再使用了

在发生 可恢复错误后,failbit 被置位,如期望读取数值 却读出一个字符等 错误。这种错误是可以修正的,流还可以继续使用

如果 到达文件结束位置,eofbit 和 failbit 都会被置位

goodbit 的值为0,表示 流未发生错误

如果 badbit、failbit 和 eofbit 任一个被置位,则 检测流状态的条件 会失败

标准库 还定义了一组函数 来查询这些标志位的状态。操作good在所有错误位 均未置位的情况下 返回true,而 bad、fail 和 eof 则在对应错误位 badbit / failbit / eofbit 被置位时 返回true

在 badbit 被置位时,fail 也会返回 true,所以 使用 good 或 fail 是确定流的总体状态的正确办法。将流当作条件使用的代码 就等价于 !fail()。而 eof 和 bad 操作只能表示特定的错误

4、管理条件状态:流对象的 rdstate 成员 返回一个 iostate 值,对应流的当前状态。setstate操作 将给定条件位置位,表示 发生了对应错误。clear 不接受参数的版本 清除(复位)所有错误标志位

cpp 复制代码
// 记住 cin 的当前状态
auto old_state = cin.rdstate(); // 记住cin的当前状态
cin.clear(); // 使cin有效
使用cin
cin.setstate(old_state); // 将cin置为原有状态

为了复位 单一的条件状态位,首先用 rdstate 读出当前条件状态,然后 用位操作 将所需位复位 来生成新的状态(如 将failbit 和 badbit 复位,但保持 eofbit 不变:cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

5、测试函数,调用参数为cin

cpp 复制代码
#include <iostream>
#include <string>

void iofunction(std::istream& is)
{
	std::string s;
	while (is >> s)
		std::cout << s << std::endl;
	is.clear();
}

int main()
{
	iofunction(std::cin);
	std::string s1;
	while (std::cin >> s1) // 因为有is.clear()这里的cin可以正常使用
		std::cout << s1 << std::endl;


	return 0;
}

运行结果:

6、什么情况下,下面的while循环会终止

cpp 复制代码
while (cin >> i) /*  ...    */

badbit、failbit 和 eofbit 任一个被置位,则检测流状态的条件会失败

1.3 管理输出缓冲

1、每个输出流 都管理一个缓冲区,用来 保存程序 读写的数据

cpp 复制代码
os << "please enter a value: ";

文本串可能立即被打印出来,但也有可能 被操作系统保存在 缓冲区中,随后 再打印。有了缓冲机制,操作系统 就可以将程序的多个输出操作组合成 单一的系统级写操作

由于 设备的写操作 可能很耗时,允许 操作系统将多个输出操作 组合为 单一的设备写操作 可以带来很大的性能提升

导致 缓冲区刷新(即,数据真正写出到 输出设备或文件)的原因有很多:

1)程序正常结束,作为 main函数的return操作的一部分,缓冲刷新 被执行

2)缓冲区满时,需要 刷新缓冲,而后新的数据 才能继续写入缓冲区

3)在每个输出操作之后,可以用 操纵符unitbuf 设置流的内部状态,来清空缓冲区。默认情况下,对 cerr 是设置 unitbuf 的,因此 写到cerr的内容 都是立即刷新的

4)一个输出流 可能被关联到 另一个流。当 读写被关联的流时,关联到的流的缓冲区 会被刷新

例如:默认情况下,cin 和 cerr 都关联到 cout。因此,读 cin 或写 cerr 都会导致 cout 的缓冲区被刷新

2、刷新输出缓冲流:endl 完成换行 并刷新缓冲区。IO库中 还有两个类似的操作符:flush 和 ends。flush 刷新缓冲区,但不输出任何额外的字符;ends 向缓冲区 插入一个空字符,然后刷新 缓冲区:

cpp 复制代码
cout << "hi!" << endl; // 输出hi和换行,刷新缓冲区
cout << "hi!" << flush; // 输出hi,刷新缓冲区
cout << "hi!" << ends; // 输出hi 和一个空字符,刷新缓冲区

3、unitbuf 操作符:在每次输出操作后 都刷新缓冲区,可以使用 unitsbuf 操纵符。它告诉流 在接下来的每次写操作之后 都进行一次 flush操作。而 nounitbuf (+no)操纵符 则重置流,使其恢复 使用正常的系统管理的缓冲区刷新机制

cpp 复制代码
cout << unitbuf; // 所有输出操作后 都会立即刷新缓冲区

// 任何输出都立即刷新,无缓冲

cout << nounitbuf; // 回到正常的缓冲方式

4、如果程序崩溃,输出缓冲区 不会被刷新,它所输出的数据 很可能停留在 缓冲区中 等待打印

当调试一个 已经崩溃的程序时,需要确认 那些认为已经输出的数据 确实已经刷新了。否则,可能将大量时间 浪费在 追踪代码为什么没有执行上,而实际上 代码已经执行了,只是 程序崩溃后 缓冲区没有被刷新,输出数据被挂起 没有打印

5、关联 输入和输出流:任何试图 从输入流读取数据的操作 都会先刷新 关联的输出流。标准库 将cout 和 cin关联在一起:cin >> ival; 导致cout 的缓冲区被刷新

交互式系统 通常应该 关联输入流 和 输出流。这意味着 所有输出,包括 用户提示信息,都会 在读操作之前 被打印出来

tie有两个重载的版本:

1)不带参数,返回指向 输出流的指针。本对象当前关联到 一个输出流,则 返回的就是 指向这个流的指针,如果对象 未关联到流,则 返回空指针

2)接受一个指向 ostream 的指针,将自己关联到此 ostream。x.tie(&o) 将流x关联到 输出流o,也是返回指向 输出流的指针(不是这次关联的,是之前关联的输出流)

既可以 将一个istream对象 关联到 另一个ostream,也可以 将一个ostream关联到 另一个ostream

cpp 复制代码
cin.tie(&cout); // 仅仅是用来展示:标准库将cin和cout关联在一起

// old_tie 指向当前关联到cin的流(不是nullptr,如果有的话,这里是cout)
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

2、文件输入输出

1、头文件 fstream 定义了 三个类型 来支持文件IO:

1)ifstream 从一个给定文件 读取数据

2)ofstream 向一个给定文件 写入数据

3)fstream 可以读写给定文件

2、除了 继承自iostream类型的行为之外,fstream中定义的类型 还增加了一些新的成员 来管理与流关联的文件

可以对 fstream、ifstream 和 ofstream 对象调用这些操作(fstream特有),但不能对 其他IO类型调用这些操作

类型/函数 操作
fstream fstrm; 创建一个未绑定的文件流。fstream是头文件fstream中定义的一个类型
fstream fstrm(s); 创建一个fstream,并打开名为s的文件。s可以是string类型,或者是一个指向C风格字符串的指针。这些构造函数都是explicit的,默认文件模式mode依赖于fstream的类型
fstream fstrm(s, mode); 与前一个构造函数类似,但按指定mode打开文件
fstrm.open(s) 打开名为s的文件,并将文件与fstrm绑定(!)。s可以是一个string或一个指向C风格字符串的指针。默认的文件mode依赖于fstream的类型。返回void
fstrm.close() 关闭与fstrm绑定的文件。返回void
fstrm.is_open() 返回一个bool值,指出与fstrm关联的文件是否成功打开 且 尚未关闭

2.1 使用文件流对象

1、当想要 读写一个文件时,可以定义 一个文件流对象,并将 对象与文件关联起来。每个文件流类 都定义了一个名为open的成员函数,定位给定的文件,并视情况 打开为读 或 写模式

2、创建文件流对象时,可以提供文件名(可选)。如果提供文件名,则 open 会自动被调用

定义了输入流in,输出流out

cpp 复制代码
ifstream in(ifile); // 构造一个 ifstream 并打开给定文件,ifile参数 为string类型的文件名
//(新版本可以是库类型string 也可以时C风格字符数组(老只允许这个))
ofstream out; // 输出文件流 未关联到 任何文件

3、用 fstream 代替 iostream&:在要求使用 基类型对象的地方,可以 使用继承类型的对象 来替代。接受一个 iostream类型引用(或指针)参数的函数,用一个对应的 fstream(或 sstream)类型来调用

也就是,有一个函数 接受一个 ostream& 参数,在调用这个函数时,可以传递给它一个 ofstream 对象,对 istream& 和 ifstream 也是类似

cpp 复制代码
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;

注意 对read和print的调用。虽然 两个函数定义时 指定的形参分别是 istream& 和 ostream&,但可以 向它们传递 fstream对象

4、成员函数 open和close:定义一个 空文件流对象,随后调用open将 它与文件关联起来:

cpp 复制代码
ifstream in(ifile); // 构筑一个 ifstream 并打开给定文件
ofstream out; // 输出文件流 未与任何文件关联
out.open(ifile + ".copy"); // 打开指定文件

如果 调用open失败,failbit 会被置位

所以 进行open是否成功的检测 是一个好习惯:

cpp 复制代码
if (out) // 检查open是否成功,如果open成功,就可以使用文件了

一旦 一个文件流已经打开,它就保持 与对应文件的关联。对一个 已经打开的文件流 调用open会失败,导致 failbit被置位。随后的 试图使用文件的操作 都会失败。为了 将文件流关联到 另外一个文件,必须首先 关闭已经关联的文件

cpp 复制代码
in.close(); // 关闭文件
in.open(ifile + "2"); // 打开另一个文件

如果open成功,open会设置 流的状态,使得 good() 为 true

5、自动构造 和 析构:main函数接受一个 要处理的文件列表

cpp 复制代码
// 对每个传递给程序的文件 执行循环操作
for (auto p = argv + 1; p != argv + argc; ++p) {
	ifstream input(*p); // 创建输入流 并打开文件
	if (input) { // 文件打开成功,处理此文件
		process(input);
	}
	else // 如果open失败,打印一条错误信息 并继续处理下一个文件
		cerr << "couldn't open:" + string(*p);
} // 每个循环步input都会离开作用域,因此会被销毁

当一个 fstream对象 离开其作用域时,与之关联的文件 会自动关闭

当一个fstream对象被销毁时,close会被 自动调用

6、编写函数,以读模式打开一个文件,将其内容读入到一个string的vector中,将 每一行 作为一个独立的元素 存于vector中(ifstream getline 使用)

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	ifstream ifs("data.txt");
	vector<string> str;
	if (ifs) {
		string tmp;
		while (getline(ifs, tmp)) {
			str.push_back(tmp);
		}
	}

	for (const auto& i : str) {
		cout << i << endl;
	}
	return 0;
}

重写上面的程序,将 每个单词 作为一个独立的元素 进行存储

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	ifstream ifs("data.txt");
	vector<string> str;
	if (ifs) {
		string tmp;
		while (ifs >> tmp) { // 跟上面代码不同的地方
			str.push_back(tmp);
		}
	}

	for (const auto& i : str) {
		cout << i << endl;
	}
	return 0;
}

7、从一个文件中读取交易记录。将文件名作为一个参数传递给main

8.6.cpp

cpp 复制代码
#include <fstream>
#include <iostream>
#include <string>
#include "D:/VisualStudio/WorkSpace/Primer7/Primer7/Sales_data_21.h" // 引之前的头文件

using namespace std;

int main(int argc, char* argv[]) {
	ifstream ifs(argv[1]); // 第0个是程序名
	if (!ifs) return 1;

	// 文件内容读取成功
	Sales_data s(ifs);
	if (!s.isbn().empty()) {
		Sales_data cur;
		while (read(ifs, cur)) {
			if (cur.isbn() == s.isbn()) {
				s.combine(cur);
			}
			else {
				print(cout, s);
				cout << endl;
				s = cur;
			}
		}
		print(cout, s);
		cout << endl;
	}
	else {
		std::cerr << "No data" << std::endl;
		return -1;
	}
	return 0;
}

data_6.txt

cpp 复制代码
123 10 10
123 12 12
12 10 10

运行结果:

2.2 文件模式

1、每个流 都有一个关联的文件模式,用来 指出如何使用文件

文件模式 含义
in 以读方式打开
out 以写方式打开
app 每次写操作前 均定位到文件末尾
ate 打开文件后 立即定位到文件末尾
trunc 截断文件
binary 以二进制方式进行IO

截断文件:即文件中原有的内容被删除,并将文件大小截断为零

当以 trunc 模式打开一个文件时,如果该文件已经存在,则会清空文件中的内容,将文件大小截断为零字节;如果文件不存在,则会创建一个空文件

无论用哪种方式 打开文件,都可以指定文件模式,调用open打开文件时可以,用一个文件名 初始化流 来隐式打开文件时 也可以

2、指定文件模式 有如下限制:

1)只可以对 ofstream 或 fstream 对象设定out模式

2)只可以对 ifstream 或 fstream 对象设定in模式

3)只有当 out 也被设定时 才可设定trunc模式

4)只要 trunc没被设定,就可以设定app模式。在app模式下,即使 没有显式指定out模式,文件也总以 输出方式被打开

5)默认情况下,即使 没有设定trunc,以 out模式打开的文件 也会被截断。为了保留以out模式 打开的文件的内容,必须同时指定app模式,这样只会将 数据追加读写到 文件末尾;或者 同时指定in模式,即打开文件 同时进行读写操作

6)ate和binary模式 可用于任何类型的文件流对象,且可以 与其他任何文件模式 组合使用

3、每个文件流类型 都定义了 一个默认的文件模式:

与ifstream关联的文件 默认以in模式打开;

与ofstream关联的文件 默认以out模式打开;

与fstream关联的文件 默认以in和out模式打开

4、以out模式 打开文件会丢弃。阻止一个ofstream 清空给定文件内容的方法 是同时指定app模式:

cpp 复制代码
// 在这几条语句中,file1都被截断
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);

关于 ofstream out3("file1", ofstream::out | ofstream::trunc);

1)"file1":这是要打开或创建的文件的名称。如果文件不存在,则会创建一个新文件;如果文件已经存在,则会截断文件(清空文件内容)

2)ofstream::out | ofstream::trunc:这是打开文件时指定的模式。ofstream::out 表示以输出模式打开文件,允许写入数据到文件中。ofstream::trunc 表示截断模式,如果文件已存在,则会清空文件内容;如果文件不存在,则会创建一个新的空文件。使用位运算符 | 将这两个模式组合在一起,表示同时使用这两个模式打开文件

这行代码的意思是以输出模式打开文件 "file1",如果文件已存在,则清空文件内容;如果文件不存在,则创建一个新的空文件,并将文件输出流对象 out3 与该文件关联

|(按位或):两个相应的二进制位只要有一个为 1 时,结果为 1,否则为 0

5、每次调用open时 都会确定文件模式:对于一个给定流,每当打开文件时,都可以 改变其文件模式

cpp 复制代码
ofstream out; // 未指定文件打开模式
out.open("data.txt"); // 模式隐含设置为 输出和截断
out.close(); // 关闭out,以便将其用于其他文件
out.open("data2.txt", ofstream::app); // 模式为 输出和追加
out.close();

第一个open调用 未显式指定输出模式,文件隐式地 以out模式打开。out模式 意味着 同时使用trunc模式。当前目录下 名为data.txt的文件将被清空。当打开名为 data2.txt的文件时,指定了append模式,文件中 已有的数据都得以保留,所有写操作 都在文件末尾进行

6、修改上一节的书店程序,将结果保存到一个文件中。将输出文件名作为第二个参数传递给main函数

8.7.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include <fstream>
#include "Sales_data.h"

int main(int argc, char** argv)
{
    std::ifstream ifs(argv[1]);
    std::ofstream ofs(argv[2]); // 打开ofstream才会清空,同一个ofstream使用多次是不会的,多次运行程序才会

    if (!ifs) return 1;

    Sales_data total(ifs);

    if (!total.isbn().empty())
    {
        Sales_data trans;

        while (read(ifs, trans))
        {
            if (total.isbn() == trans.isbn())
            {
                total.combine(trans);
            }
            else
            {
                print(ofs, total);
                ofs << std::endl;
                total = trans;
            }
        }
        print(ofs, total);
        ofs << std::endl;

        return 0;
    }
    else
    {
        std::cerr << "No data?!" << std::endl;
        return -1;  
    }
}

修改上一题的程序,将结果追加到给定的文件末尾。对同一个输出文件,运行程序至少两次,检验数据是否得以保留

8.8.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include <fstream>
#include "Sales_data.h"

int main(int argc, char** argv)
{
    std::ifstream ifs(argv[1]);
    std::ofstream ofs(argv[2], std::ofstream::app); // 只需要显式指定app模式即可,其他都是一样的

    if (!ifs) return 1;

    Sales_data total(ifs);

    if (!total.isbn().empty())
    {
        Sales_data trans;

        while (read(ifs, trans))
        {
            if (total.isbn() == trans.isbn())
            {
                total.combine(trans);
            }
            else
            {
                print(ofs, total);
                ofs << std::endl;
                total = trans;
            }
        }
        print(ofs, total);
        ofs << std::endl;

        return 0;
    }
    else
    {
        std::cerr << "No data?!" << std::endl;
        return -1;  
    }
}

运行结果:

3、string流

1、sstream 头文件定义了 三个类型支持内存IO,可以向string写入数据,从string读取数据,就像string是一个IO流一样

istringstream 从string读取数据,ostringstream 向string写入数据,而 头文件stringstream 既可从 string读取数据 也可向string写数据

与 fstream类型类似,头文件sstream中定义的类型 都继承自 iostream头文件中定义的类型。除了继承得来的操作,sstream中定义的类型 还增加了一些成员 来管理与流相关联的 string

2、stringstream特有的操作,不能对其他IO类型调用这些操作

对象或函数 解释
sstream strm; strm是一个未绑定的stringstream对象。sstream是头文件sstream中定义的一个类型
sstream strm(s); strm是一个sstream对象,保存 string s的一个拷贝。此构造函数是 explicit的
strm.str() 返回strm所保存的string的拷贝
strm.str(s) 将string s拷贝到strm中。返回void

3.1 使用 istringstream

1、处理行内的单个单词时,通常可以使用 istringstream

例:用getline从标准输入 读取整条记录。如果 getline 调用成功,line中将保存着 从输入文件而来的一条记录

while循环中,从一个string而不是 标准输入 读取数据。当string中的数据 全部读出后,会触发"文件结束"信号,在record上的 下一个输入操作会失败

cpp 复制代码
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

using namespace std;

struct PersonInfo{
	string name;
	vector<string> phones;
};

int main() 
{
	string line, word; // 分别保存来自输入的一行 和 单词
	vector<PersonInfo> people; // 保存来自输入的所有记录
	// 逐行从输入读取数据,直至cin遇到文件尾
	while (getline(cin, line)) {
		PersonInfo info; // 创建一个保存此纪录数据的对象
		istringstream record(line); // 将记录绑定到 刚读入的行
		// 这样就可以在此 istringstream 上使用输入运算符来读取 当前记录中的每个元素
		record >> info.name;
		while (record >> word) {
			info.phones.push_back(word);
		}
		people.push_back(info); // 记录这个记录
	}
	for (const PersonInfo& e : people) {
		cout << e.name << endl;
		for (const string& s : e.phones) {
			cout << s << endl;
		}
		cout << endl;
	}
	return 0;
}

运行结果:

2、编写函数打印一个istringstream对象的内容(+异常处理)

以下情况下 in.eof() 会为 true:

1)从文件中读取:当读取到文件末尾时,in.eof()会返回 true

2)从字符串流 中读取:当读取到字符串流的结尾时,in.eof()会返回true

逗号操作符。它允许在单个语句中执行多个操作,并返回最后一个操作的结果

in.ignore(100, '\0');// 忽略输入流中的字符,直到达到以下条件之一:已经忽略了100个字符;遇到了终止字符'\0'

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>

std::istream& f(std::istream& in) {
    std::string v;
    
    while (in >> v, !in.eof()) {// 逗号操作符。它允许在单个语句中执行多个操作,并返回最后一个操作的结果
        std::cout << v << std::endl;// 当字符串只有一个空格的时候不会进入这个循环
    }
    std::cout << v << std::endl;
    if (in.bad())// 在底层 IO 操作失败时返回 true,例如文件无法打开、设备错误、磁盘故障等
        throw std::runtime_error("IO 流错误");
    if (in.fail()) { // 会在输入操作失败时(类型不匹配,文件结束或无效数据,IO错误)返回true
        std::cerr << "数据错误,请重试" << std::endl;
        in.clear();// 清除流的状态标志
        in.ignore(100, '\0');// 忽略输入流中的字符,直到达到以下条件之一:已经忽略了100个字符;遇到了终止字符'\0'
        return in;
    }
    in.clear();
    return in;
}

int main() {
    std::istringstream in(" ");// 空会导致输入操作失败,输出 数据错误
    f(in);
    std::istringstream in2("C++ Primer 第五版");// 每碰到一个空格就进行切片s
    f(in2);
    return 0;
}

运行结果:

如果record 对象定义在循环之外,你需要对程序进行怎样的修改?重写程序,将record的定义移到while 循环之外

1)record.str(line) 是将字符串 line 赋值给 istringstream 对象 record 的成员变量 str

在这里,record 是一个 istringstream 对象,它可以用于从字符串中提取数据,而 line 是从标准输入读取的一行字符串。

通过 record.str(line),将 line 中的内容设置到 record 中,这样就可以像从输入流中读取数据一样,通过 record 逐个读取单词。这在代码中的作用是将每一行字符串分解为单词,以便后续处理

2)record.clear() 的目的是在每次循环迭代开始时清除 record 的状态,以便它可以重新读取新的一行输入数据

因为在每次循环迭代中,我们使用 record.str(line) 将 line 中的内容设置到 record 中,但同时 record 对象保留了之前的状态,包括可能的错误标志或文件结束标志

因此,在每次循环开始时,我们调用 record.clear() 来清除这些状态,以确保 record 对象在新的一行数据读取之前处于一个良好的状态

3)在C++中,istringstream 类具有两个主要的构造函数,它们的作用有些不同(record.str(line) 和 record(line) 区别):

1、record.str(line):这是调用 istringstream 对象的成员函数 str(),它的作用是将给定的字符串 line 赋值给 istringstream 对象 record。这个函数主要用于将一个已有的字符串赋值给 istringstream 对象,使得后续可以通过该对象读取该字符串中的内容

2、record(line):这是调用 istringstream 类的构造函数,它的作用是创建一个 istringstream 对象,并将给定的字符串 line 传递给该构造函数,从而初始化 istringstream 对象的内部状态,使得后续可以通过该对象读取该字符串中的内容

因此,两者的功能都是将一个字符串传递给 istringstream 对象,但是前者是通过成员函数来实现,而后者是通过构造函数来实现。在本程序中,两者的效果是相同的,都将 line 中的内容设置到 record 中

cpp 复制代码
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

using namespace std;

struct PersonInfo {
	string name;
	vector<string> phones;
};

int main()
{
	string line, word;
	vector<PersonInfo> people;

	istringstream record; // 修改

	while (getline(cin, line)) {
		PersonInfo info;
		
		record.str(line); // 修改

		record >> info.name;
		while (record >> word) {
			info.phones.push_back(word);
		}
		people.push_back(info);

		record.clear(); // 重复使用字符串流时,每次都要调用 clear

	}
	for (const PersonInfo& e : people) {
		cout << e.name << endl;
		for (const string& s : e.phones) {
			cout << s << endl;
		}
		cout << endl;
	}
	return 0;
}

运行结果:

为什么没有在PersonInfo中使用类内初始化

由于每个人的电话号码数量不固定,因此更好的方式不是通过类内初始化指定人名和所有电话号码,而是在缺省初始化之后,在程序中设置人名并逐个添加电话号码

3、编写程序,将来自一个文件中的行保存在一个vector中。然后使用一个istringstream从vector读取数据元素,每次读取一个单词

cpp 复制代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	ifstream ifs("data_10.txt");
	if (!ifs) // 注意对异常的处理
	{
		cerr << "open error" << endl;
		return -1;
	}
	vector<string> vs;
	string line;
	while (getline(ifs, line)) { // 从文件按行读取,读到line里面
		vs.push_back(line);
		cout << line << endl;
	}
	cout << endl;
	for (const string &s : vs) {
		istringstream iss(s);// 把vs存储的每一行每次跟iss绑定,用iss完成对s的读取
		string ss;
		while (iss >> ss) // 每次读取到的字符串(按空格分割)存在ss里面
			cout << ss << endl;
	}
	return 0;
}

data_10.txt

cpp 复制代码
ashergu is a good engineer
ashergu is a good boy
ashergu 1234

运行结果:

关于字符串中有个空格:

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::istringstream iss(" "); // 输入流只包含一个空格字符
    std::string str;
    if (iss >> str) // 为false,不会进入循环
        std::cout << "true" << std::endl;
    if (str.empty()) {
        std::cout << "输入为空" << std::endl; // 输出为空,没有东西读进去
    }
    else {
        std::cout << "输入为: " << str << std::endl;
    }
    return 0;
}

运行结果:

3.2 使用 ostringstream

1、当逐步构造输出,希望最后一起打印时,ostringstream 是有用的

例:上一节的例子,想逐个 验证电话号码 并改变其格式。如果 所有号码都是有效的人,输出一个新的文件,包含 改变格式后的号码;对于有无效号码的人,打印一条包含人名 和 无效号码的错误信息

因为 不希望输出有无效号码的人,所以 对每个人,直至 验证完所有的号码后 才能进行输出操作

先将 输出内容"写入"到一个内存 ostringstream 中

有两个函数,valid() 和 format(),分别完成 电话号码的验证 和 改变格式功能。"写入"操作 实际上转换为 string操作,分别向 formatted 和 badNums 中的string对象 添加字符

cpp 复制代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

using namespace std;

struct PersonInfo{
	string name;
	vector<string> phones;
};

string format(string& s) {
	return s;
}

bool valid(const string& s) {
	for (const char c : s) {
		if (!isdigit(c)) // 每个字符都需要是数字
			return false;
	}
	return true;
}

int main()
{
	ifstream ifs("data_13.txt");
	if (!ifs)
		cerr << "data load error" << endl;
	string line;
	vector<PersonInfo> people;
	vector<string> record;
	while (getline(ifs, line)) { // 一行一行读入
		record.push_back(line);
	}
	for (const string& s : record) {
		PersonInfo person;
		istringstream iss(s);// 对每一行进行切片
		iss >> person.name;
		string ss;
		while (iss >> ss) {
			person.phones.push_back(ss);
		}
		people.push_back(person); // 记录每一个人的信息
	}
	for (const auto& p : people) {
		ostringstream formatted, badNums; // 拆成两个ostringstream分别记录合法 / 非法号码
		for (const auto& s : p.phones) {
			if (!valid(s)) {
				badNums << s << " ";
			}
			else {
				formatted << s << " ";
			}
		}
		if (badNums.str().empty()) { // 没有非法的号码
			cout << p.name << " " << formatted.str() << endl;
 		}
		else {
			cerr << "input error: " << p.name << " " << badNums.str() << endl;
		}
	}
	return 0;
}

data_13.txt

cpp 复制代码
xiaoyi 13265606 51315 5315366
xiaoer 6843612 83514883 4dhjjk84513
xiaosan 513653 35135

运行结果:

术语表

1、条件状态:可被 任何流类型 使用的一组标志和函数,用来 指出给定流是否可用

2、istringstream:用来 从给定string读取 数据的字符串流

3、字符串流:用于读写 string的流对象。除了普通的iostream操作外,字符串流 还定义了一个名为str的重载成员

调用 str的无参版本 会返回字符串流关联的 string。调用时 传递给它 一个string参数,则会将字符串流 与 该string的一个拷贝关联

相关推荐
捕鲸叉20 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer25 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq27 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程2 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错2 小时前
C语言扫雷小游戏
c语言·开发语言·算法
小屁孩大帅-杨一凡3 小时前
java后端请求想接收多个对象入参的数据
java·开发语言