IO 类
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 |
为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵 wchar_t 类型的数据。宽字符版本的类型和函数的名字以一个 w 开始。例如,wcin、wcout 和 wcerr 是分别对应 cin、cout 和 cerr 的宽字符版对象。宽字符版本的类型和对象与其对应的普通 char 版本的类型定义在同一个头文件中。例如,头文件 fstream 定义了 ifstream 和 wifstream 类型。
IO 类型间的关系
IO 对象无拷贝或赋值
条件状态
IO 操作一个与生俱来的问题是可能发生错误。一些错误是可以恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。下面是 IO 类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态。
函数和标志 | 说明 |
---|---|
strm::iostate | strm 是一种 IO 类型,iostate 是一种机器相关的类型,提供了表达条件状态的完整功能 |
strm::badbit | 用来指出流已崩溃 |
strm::failbit | 用来指出一个 IO 操作失败了 |
strm::eofbit | 用来指出流到达了文件结束 |
strm::goodbit | 用来指出流未处于错误状态,此值为 0 |
s.eof() | 若流 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) | 根据给定的 flags 标志位,将流 s 中对应条件状态位复位。flags 的类型为 strm::iostate。返回 void |
s.setstate(flags) | 根据给定的 flags 标志位,将流 s 中对应条件状态位置位。flags 的类型为 strm::iostate。返回 void |
s.rdstate() | 返回流 s 的当前条件状态。返回值类型为 strm::iostate |
在下面的测试代码中
int nTest;
cin >> nTest;
如果我们在标准输入中输入 test,读操作就会失败。代码中的输入运算符期待读取一个 int,但是却得到了一个字符 t。这样,cin 就会进入错误状态。类似的,如果我们输入一个文件结束标识,cin 也会进入错误状态。
一个流一旦发生错误,其上后续的 IO 操作就会失败。只要当一个流处于无错误状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用:
while (cin >> nTest) {
// todo something
}
查询流的状态
将流作为条件状态使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。有时我们也需要知道流为啥失败。例如,在输入文件结束符之后我们该如何处理,可能与一个 IO 设备错误的处理方式是不同的。
IO 库定义了一个与机器无关的 iostate 类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用。IO 库定义了 4 个 iostate 类型的 constexpr 值,表示特定的位模式。这些值用来表示特定类型的 IO 条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
badbit 表示系统级错误,如不可恢复的读写错误。通常情况下,一旦 badbit 被置位,流就无法再使用了。在发生可恢复错误后,failbit 被置位,如期望读取数值却读出一个字符等错误,这种问题通常是可以修正的,流还可以继续用。如果到达文件结束位置,eofbit 和 failbit 都会被置位。goodbit 的值为 0,表示流未发生错误。如果 badbit、failbit 和 eofbit 任意一个被置位,则检测流状态的条件会失败。
标准库还定义了一组函数来查询这些标志位的状态。操作 good 在所有错误位均未置位的情况下返回 true,而 bad、fail 和 eof 则在对应的错误被置位时才返回 true。此外,在 badbit 被置位时,fail 也会返回 true。这意味着,使用 good 或 fail 是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于 !fail()。而 eof 和 bad 操作只能表示特定的错误。
管理条件状态
流对象的 rdstate 成员返回一个 iostate 值,对应流的当前状态。setstate 操作将给定条件位置位,表示发生了对应错误。clear 成员是一个重载的成员:它有一个不接受参数的版本和一个接受一个 iostate 类型参数的版本。
不带参数的版本清除(复位)所有错误标志位。执行 clear() 后,调用 good 会返回 true。我们可以这样使用这些成员:
auto oldState = cin.rdstate(); // 记录 cin 的当前状态
cin.clear(); // 使 cin 有效
todoSomething(cin); // 使用 cin
cin.setstate(oldState); // 将 cin 置为原有状态
带参数的 clear 版本接受一个 iostate 值,表示流的新状态。为了复位单一的条件状态位,我们首先用 rdstate 读出当前状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将 failbit 和 badbit 复位,但是保持 eofbit 不变:
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
管理输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码:
os << "please enter a value: ";
文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。
导致缓冲刷新(即,将数据真正写到输出设备或文件)的原因有很多:
- 程序正常结束,作为 main 函数的 return 操作的一部分,缓冲刷新被执行。
- 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
- 我们可以使用操作符如 endl 来显示地刷新缓冲区。
- 在每个输出操作之后,我们可以用操作符 unitbuf 设置流的内部状态,来清空缓冲区。默认情况下,对 cerr 是设置 unitbuf 的,因此写到 cerr 的内容都是立即刷新的。
- 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin 和 cerr 都关联到 cout。因此,读 cin 或写 cerr 都会导致 cout 的缓冲区被刷新。
刷新输出缓冲区
我们已经使用过操作符 endl,它完成换行并刷新缓冲区的工作。IO 库中还有两个类似的操作符:flush 和 ends。flush 刷新缓冲区,但不输出任何额外的字符;ends 向缓冲区插入一个空字符,然后刷新缓冲区:
cout << "hi!" << endl; // 输出 hi! 和一个换行,然后刷新缓冲区
cout << "hi!" << flush; // 输出 hi!,然后刷新缓冲区,不附加任何额外的字符
cout << "hi!" << ends; // 输出 hi! 和一个空字符,然后刷新缓冲区
unitbuf 操作符
如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf 操作符。它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而 nounitbuf 操作符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:
cout << unitbuf;
// 任何输出都立即刷新,无缓冲
cout << nounitbuf;
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将 cout 和 cin 关联在一起,因此下面语句
cin >> ival;
导致 cout 的缓冲区被刷新。
tie 有两个重载的版本:一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。tie 的第二版本接受一个指向 ostream 的指针,将自己关联到此 ostream。即,x.tie(&o) 将流 x 关联到输出流 o。
我们既可以将一个 istream 对象关联到另一个 ostream,也可以将一个 ostream 关联到另一个 ostream:
cin.tie(&cout); // 仅仅用于展示:标准库将 cin 和 cout 关联在一起
// old_tie 指向当前关联到 cin 的流(如果有的话)
ostream *old_tie = cin.tie(nullptr);
// 将 cin 与 cerr 关联;这不是一个好主意,因为 cin 应该关联到 cout
cin.tie(&cerr); // 读取 cin 会刷新 cerr 而不是 cout
cin.tie(old_tie); // 重建 cin 和 cout 间的正常关联
在上面的代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了 tie。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流,但多个流可以同时关联到同一个 ostream。
文件的输入输出
#include <fstream>
ofstream //文件写操作 内存写入存储设备
ifstream //文件读操作,存储设备读区到内存中
fstream //读写操作,对打开的文件可进行读写操作
fstream 的一些常见的操作方法:
#include <fstream>
// ...
string fname("c:/Users/toby/Downloads/test.txt");
ios_base::openmode mode = ios_base::in;
fstream fstrm; // 创建一个未绑定的文件流。
fstream fstrm1(fname); // 创建一个 fstream,并打开名为 fname 的文件
fstream fstrm2(fname, mode); // 创建一个 fstream,并以 mode 指定的方式打开名为 fname 的文件
fstrm2.open(fname);
if (fstrm2.is_open()) { // 返回一个 bool 值,指出与 fstrm 关联的文件是否成功打开且尚未关闭
fstrm2.close(); // 关闭与 fstrm 绑定的文件
}
使用文件流对象
#include <fstream>
#include <string>
#include <iostream>
struct Sales_data {
std::string bookNo;
unsigned units_sold;
double revenue;
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data&);
};
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
std::istream &read(std::istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
std::ifstream input(argv[1]); // 打开销售记录文件
std::ofstream output(argv[2]); // 打开输出文件
Sales_data total; // 保存销售总额的变量
if (read(input, total)) { // 读取第一条销售记录
Sales_data trans; // 保存下一条销售记录的变量
while (read(input, trans)) { // 读取剩余记录
if (total.isbn() == trans.isbn()) { // 检查 ISBN
total.combine(trans); // 更新销售总额
} else {
print(output, total) << std::endl; // 打印结果
total = trans; // 处理下一本书
}
}
print(output, total) << std::endl; // 打印最后一本书的销售额
} else {
std::cerr << "Not data!" << std::endl; // 文件中无输入数据
}
return a.exec();
}
指定文件模式
模式 | 说明 |
---|---|
ios::in | 以读方式打开文件 |
ios::out | 以写方式打开文件 |
ios::ate | 打开文件后立即定位到文件末尾 |
ios::app | 每次写操作前均定位到文件末尾 |
ios::trunc | 如果文件已存在则先删除该文件 |
ios::binary | 以二进制方式进行 IO |
指定文件模式有如下限制:
- 只可以对 ofstream 或 fstream 对象设定 out 模式
- 只可以对 ifstream 或 fstream 对象设定 in 模式
- 只有当 out 被设定时才可以设定 trunc 模式
- 只要 trunc 没被设定,就可以设定 app 模式。在 app 模式下,即使没有显式指定 out 模式,文件也总是以输出方式被打开
- 默认情况下,即使我们没有指定 trunc,以 out 模式打开的文件也会被截断。为了保留以 out 方式打开的文件的内容,我们必须同时指定 app 模式,这样只会将数据追加写到文件末尾;或者同时指定 in 模式,即打开文件同时进行读写操作。
- ate 和 binary 模式可以用于任何类型的文件流对象,且可以与其他任何文件模式组合使用
每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。与 ifstream 关联的文件默认以 in 模式打开;与 ofstream 关联的文件默认以 out 模式打开;与 fstream 关联的文件默认以 in 和 out 模式打开。
以 out 模式打开文件会丢弃已有数据
默认情况下。当我们打开一个 ofstream 时,文件的内容会被丢弃。阻止一个 ofstream 清空给定文件内容的方法是同时指定 app 模式:
// 在这几条语句中,file1 都被截断
std::ofstream out("file1"); // 隐含以输出模式打开文件并截断文件
std::ofstream out1("file1", std::ofstream::out); // 隐含地截断文件
std::ofstream out2("file1", std::ofstream::out | std::ofstream::trunc);
// 为了保留文件内容,我们必须显式地指定 app 模式
std::ofstream out3("file1", std::ofstream::app); // 隐含为输出模式
std::ofstream out4("file1", std::ofstream::out | std::ofstream::app);
每次调用 open 时都会确定文件模式
对于一个给定流,每当打开文件时,都可以改变其文件模式。
std::ofstream out; // 未指定文件打开模式
out.open("testfile"); // 模式隐含设置为输出和截断
out.close(); // 关闭 out,以便我们将其用于其他文件
out.open("testfile1", std::ofstream::app); // 模式为输出和追加,所有写操作都在文件末尾进行
out.close();
string 流
使用 istringstream
#include <string>
#include <sstream>
struct PersonInfo {
std::string name;
std::vector<std::string> phones;
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
std::vector<PersonInfo> person;
std::string line, word;
while (getline(std::cin, line)) {
PersonInfo info;
std::istringstream record(line);
record >> info.name;
while (record >> word) {
info.phones.push_back(word);
}
person.push_back(info);
}
return a.exec();
}
使用 ostringstream
#include <string>
#include <sstream>
struct PersonInfo {
std::string name;
std::vector<std::string> phones;
};
bool valid(const std::string &str)
{
return isdigit(str[0]);
}
std::string format(const std::string &str)
{
return str.substr(0,3) + "-" + str.substr(3,3) + "-" + str.substr(6);
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
std::vector<PersonInfo> person;
std::string line, word;
while (getline(std::cin, line)) {
PersonInfo info;
std::istringstream record(line);
record >> info.name;
while (record >> word) {
info.phones.push_back(word);
}
person.push_back(info);
}
for (const auto &entry : person) {
std::ostringstream formatted, badNums;
for (const auto &phone : entry.phones) {
if (valid(phone)) {
formatted << " " << format(phone);
} else {
badNums << " " << phone;
}
}
if (badNums.str().empty())
std::cout << entry.name << formatted.str() << std::endl;
else
std::cerr << "input error: " << entry.name
<< " invalid numbers " << badNums.str() << std::endl;
}
return a.exec();
}