IO库
1 IO类
头文件 | 类型 |
---|---|
iostream | istream,wistream 从流读取数据 ostream,wostream 向流写入数据 iostream,wiostream 读写流 |
fstream | ifstream,wifstream 从文件读取数据 ofstream,wofstream 向文件写入数据 fstream,wfstream 读写文件 |
sstream | istringstream,wistringstream 从 string 读取数据 ostringstream,wostringstream 向 string 写入数据 ostringstream,wstringstream 读写 striing |
注: w 开头的是宽字符版本。
IO类型的关系
类型 ifstream 和 istringstream 都继承自 istream。因此,我们可以像使用 istream 对象一样来使用 ifstream 和 istringstream 对象。类似的,类型 ofstream 和 ostringstream 都继承自 ostream。因此,我们是如何使用 cout 的,就可以同样地使用这些类型的对象。
1.1 IO 对象无拷贝或赋值
我们不能拷贝或对IO对象赋值
ofstream out1,out2;
out1 = out2; // 错误,不能对流对象赋值
ofstream print(ofstream); // 错误,不能初始化ofstream参数
out2 = print(out); // 错误,不能拷贝流对象
由于不能拷贝 IO 对象,因此我们也不能将形参或返回类型设置为流类型。进行 IO 操作的函数通常以引用方式传递和返回流。读写一个 IO 对象会改变其状态,因此传递和返回的引用不能是 const 的。
1.2 条件状态
IO 操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,以及超出了应用程序可以修正的范围。下表列出了 IO 类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态。
操作 | 解释 |
---|---|
strm::iostate | 提供表达条件状态的完整功能 |
strm::badbit | 指出流已奔溃 |
strm::failbit | 指出一个 IO 操作失败了 |
strm::eofbit | 指出流达到了文件结束 |
strm::goobit | 指出流未处于错误状态,此值保证为0 |
s.eof() | 若流 s 的 eofbit 置位,返回 true |
s.fail() | 若流 s 的 failbit 或 badbit 置位,返回 true |
s.good() | 若流 s 的 badbit 置位,返回 true |
s.clear() | 将流 s 中所有条件状态复位,将流的状态设置为有效,返回 void |
s.clear(flags) | 根据给定的 flags(类型为 strm::iostate) 标志位,将流 s 中对应条件状态位复位。 |
s.setstate(flags) | 根据给定的 flags(类型为 strm::iostate) 标志位,将流 s 中对应条件状态位置。 |
s.rastate() | 返回流 s 的但钱条件状态,返回值类型为 strm::iostate |
一个流一旦发生错误,其后续的IO错误都会失败。只有一个流处于无错状态,我们才可以从它读取数据,写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态最简单的方法就是将它作为一个条件使用:
while(cin >> word)
// ok:读操作成功
查询流的状态
将流作为条件使用,只能告诉我们流是否有效,而无法具体告诉我们发生了什么。有时我们也需要知道流为什么失败。例如,在键入文件结束标识符后我们的应对措施,可能与遇到一个 IO 设备错误的处理方式是不同的。
IO 库定义了一个与机器无关的 iostate 类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用,IO库定义了4个 iostate 类型的 constexpr 值,表示特定的位模式。这些值用来表示特定类型的 IO 条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
- badbit 表示系统级错误,如不可恢复的读写错误。通常情况下,一旦 badbit 被置位,流就无法使用了。
- 在发生可恢复错误后,failbit 被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
- 如果到达文件结束位置,eofbit 和 failbit 都会被置位。
- goodbit 的值为0,表示流未发生错误。
如果 badbit、failbit 和 eofbit 任一个被置位,则检测流状态的条件会失败。
管理条件状态
流对象的 rdstate 返回一个 iostate 值,对应流当前的状态。setstate 操作将给定条件位置位,表示发生了对应错误。clear 成员是一个重载的成员:它有两个版本,一个接受 iostate 参数,一个不接受。
不接受参数版本清除(复位)所有错误标志位。
//记住cin的当前状态
auto old_state = cin.rdstate(); // 记住cin的当前状态
cin.clear(); // 使cin有效
process_input(cin); // 使用cin
cin.setstate(old_state); // 将cin置为原有状态
带参数版本接受一个 iostate 值,表示流的新状态。为了复位单一的条件状态位,我们首先使用 rdstate 读取当前条件状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将 failbit 和 badbit 复位,但保持 eofbit 不变:
// 复位 failbit 和 badbit,保持其他位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
1.3 管理输出缓冲区
每个输出流都管理一个缓冲区,用来保存程序读写的数据。执行代码时,文本串可能立刻打印出来,但也有可能被操作系统暂存在缓冲区中,随后再打印。
有了缓冲机制,操作系统就可以将程序的多个输出操作合成单一的系统级写操作。因为设备的写操作可能很耗时,允许操作系统将多个输出操作组合成单一的设备写操作可以带来很大的性能提升。
导致缓冲刷新(即,数据真正写到设备或文件)的原因有很多:
- 程序正常结束,作为 main 函数的 return 操作的一部分,缓冲刷新被执行。
- 缓冲区满,需要刷新缓冲,之后的数据才能继续写入缓冲区。
- 我们可以使用操纵符如 endl,来显式刷新缓冲区。
- 在每个输出操作后,我们都可以通过用操纵符 unitbuf 来设置流的内部状态,来清空缓冲区。在默认情况下,对 cerr 是设置 unitbuf 的,因此写到 cerr 的内容都是立即刷新的。
- 一个输出流可能被关联到另外一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,在默认情况下,cin 和 cerr 都关联到 cout。因此,读 cin 或写 cerr 都会导致 cout 的缓冲区被刷新。
刷新输出缓冲区
cout << "hi!" << endl; // 输出一个hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush; // 输出hi,然后刷新缓冲区,不添加任何额外字符
cout << "hi!" << ends; // 输出hi和一个空字符,然后刷新缓冲区
unitbuf 操纵符
如果想在每次输出操作后都刷新缓冲区,可以使用 unitbuf 操纵符。它告诉流在接下来的每次写操作后都进行一个 flush 操作。而 nounitbuf 操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:
cout << unitbuf; // 所有输出操作后都立即刷新缓冲区
// 任何输出都立即刷新无缓冲
cout << nounitbuf; // 回到正常的缓冲方式
注: 如果程序崩溃,输出缓冲区不会被刷新。
关联输入和输出流
当一个输入流被关联到一个输出流,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标注库将 cout 和 cin 关联在一起,当执行 cin 时,会导致 cout 的缓冲区被刷新。
注: 交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提升信息,都会在读操作之前被打印出来。
tie 有两个重载版本,一个版本不带参,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果未关联到输出流,则返回空指针。tie 的第二个版本接受一个指向 ostream 的指针,将自己关联到此 ostream,即 x.tie(&o) 将流 x 关联到输出流 o,返回 x 之前关联的 ostream 指针。
我们既可以将一个 istream 对象关联到另一个 ostream,也可以将一个 ostream 关联到另一个 ostream:
cin.tie(&cout); // 仅仅用来展示,标准库已经将 cin 和 cout 关联
// old_tie 指向当前关联到的 cin 流(如果有)
ostream *old_tie = cin.tie(nullptr); // cin 关联到 nullptr(不再与其他流关联)
cin.ite(&cerr); // 关联到 cerr,读取 cin 会刷新 cerr 而不是 cout 了
cin.tie(old_tie); // 重建 cin 和 cout 的正常关联
这段代码中,为了将一个 给定的流关联到一个输出流,我们将新流的指针传递给了 tie。为了彻底解开流的关联,我们传递了一个空指针。
每个流同时最多关联到一个流,但多个流可以同时关联到同一个 ostream。
2 文件输入输出
除了继承自 iostream 类型的行为外,fstream 中的定义类型还增加了一些新的成员来管理与流关联的文件。在下表中列出了这些操作,我们可以对 fstream、ifstream 和 ofstream 对象调用这些操作,但不能对其他 IO类型调用这些操作。
操作 | 解释 |
---|---|
fstream fstrm | 创建一个未绑定的文件流 |
fstream fstrm(s) | 创建一个 fstrm,并以默认方式打开名为 s 的文件。 |
fstream fstrm(s, mode) | 与前一个构造函数类型,按指定 mode 打开文件 |
fstrm.open(s) | 打开名为 s 的文件,并将文件与 fstrm 绑定,返回 viod |
fstrm.close() | 关闭与 fstrm 绑定的文件,返回 void |
fstrm.is_open() | 返回一个 bool 值,指出与 fstrm 关联的文件是否成功打开且尚未关闭 |
注: s 可以是 string 类型,或者是一个指向 C 风格字符串的指针,这些构造函数都是 explicit 的。默认的 mode 依赖于 fstream 的类型。
2.1 使用文件流对象
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为 open 的成员函数,它完成了一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
用 fstram 代替 iostream&
基类的指针和引用可以指向派生类对象,但是无法使用不存在于基类只存在于派生类的元素。
成员函数 open 和 close
创建文件流对象时,我们可以提供文件名(可选)。如果提供了 一个文件名,则 open 会自动被调用。如果我们定义一个空文件流对象,可以随后调用open来将它与文件夹关联起来:
ifstream in(ifile); // 构筑一个 ifstream 并打开给定文件
ofstream out; // 输出文件流未与任何文件关联
out.open(ifile + ".copy"); // 打开指定文件
如果调用open失败,failbit 会被置位。因为调用 open 可能失败,进行 open 是否成功的检验通常是一个好习惯。
if(out) // 检查open是否成功,成功我们就可以使用文件了
一旦一个文件流已经打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用 open 会失败,并回到是 failbit 被置位。随后试图使用文件流的操作都会失败。为了将文件六关联到另一个文件,必须关闭(close)已经关联的文件。
注:当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。换句话说当一个 fstream 对象被销毁时,close 会自动被调用。
2.2 文件模式
模式 | 解释 |
---|---|
in | 以读方式打开 |
out | 以写方式打开 |
app | 每次写操作前均定位到文件末尾 |
ate | 打开文件后立即定位到文件末尾 |
trunc | 截断文件 |
binary | 以二进制方式进行 IO |
指定文件模式有如下限制:
- 只可以对 ofstream 或 fstream 对象设定 out 模式(写)
- 只可以对 ifstream 或 fstream 对象设定 in 模式(读)
- 只有当 out 被设定时才可设定 trunc 模式
- 只要 trunc 模式没被设定,就可设定 app 模式。在 app 模式下,即使没有显示设定 out 模式,文件也总是以写入模式被打开。
- 默认情况下,即使我们没有指定 trunc,以 out 模式打开的文件也会被截断。
- ate 和 binary 模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
每个文件流类型都定义了一个默认的文件模式,当未指定文件模式时,就使用此默认模式。
以 out 模式打开文件会丢弃已有数据
默认情况下当我们打开一个 ofstream 时文件的内容会被丢弃。
// 下面3条语句,file1都会被截断
ofstream out("file1"); // 隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out); // 隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
// 如果要保留文件内容,必须显式指定app模式
ofstream out("file2", ofstream::app); // 隐含位输出模式
ofstream out("file2", ofstream::out | ofstream::app);
注1: 保留被 ofstream 打开的文件中已有数据的唯一方法时显式指定 app 或 in 模式。
注2: 每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是是隐式地设置。当程序未指定模式时,就是用默认值。
3 string 流
除了继承自 iostream 类型的行为外,sstream 中的定义类型还增加了一些新的成员来管理与流关联的文件。在下表中列出了这些操作,可以对 stringstream 对象调用这些操作,但不能对其他 IO类型调用这些操作。
操作 | 解释 |
---|---|
sstream strm | 创建一个未绑定的 stringstream 对象 |
sstream strm(s) | 保存 string s 的一个拷贝,构造函数是 explicit 的 |
fstrm.str() | 返回 strm 所保存到 string 的拷贝 |
fstrm.str(s) | 将 string s 拷贝到 strm 中,返回 void |
3.1 使用 istringstream
当我们的某些工作是对整行文本进行处理而其他一些工作是处理行内的单个单词时,通常可以使用 istringstream。
假设文件中每条记录都以一个人名开始,后面跟随一个或多个电话号码,输入的文件可能是这样的。
john 123456 877889
tom 897673864 729832789
jess 34673467 67286372 2323678678
我们在每一个循环中处理输入数据,每个循环步 读取一条记录提取出一个人名和若干电话号码:
// 成员默认为公有
struct PersonInfo {
string name;
vector<string> phones;
};
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末尾
}
3.2 使用 ostringstream
当我们逐步构造输出,希望最后一起打印时,ostringstream 很有用。
例如,我们不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作,但是可以先将输出内容 “写入” 到一个内存ostringstream 中。
for (const auto &entry : people) { // 对 people 中每一项
ostringstream formatted, badNums; // 每个循环步创建的对象
for (const auto &nums : entry.phones) { // 每个电话
if (!valid(nums)) {
badNums << " " << nums;
} else {
// 将格式化的字符串写入 formatted
formatted << " " << nums;
}
}
if (badNums.str().empty()) // 没有错误
cout << entry.name << " " << formatted.str() << endl;
else
cerr << "input error:" << entry.name << "invalid number" << badNums.str() << endl;
}
4 IO 库再探 (第 17 章 标准库特殊设施)
此处我们将继续介绍三个更特殊的 IO 库特性:格式控制、未格式化 IO 和随机访问。
4.1 格式化输入与输出
标准库定义了一组操纵符来修改流的格式状态。一个操纵符是一个函数或一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象。类似输入和输出运算符,操纵符也能返回它所处理的流对象,因此我们可以在一条语句中组合操纵符和数据。
endl 就是一个操纵符,endl 不是一个普通值,而是一个操作:它输出一个换行符并刷新缓冲区。
很多操纵符改变格式状态
操纵符用于两大类输出控制:控制数值的输出形式以及控制补白的数量和位置。大多数改变格式状态的操纵符都是设置/复原成对的:一个操纵符用来将格式状态设置为一个新值,而另一个用来将其复原,恢复为正常的默认格式。
注: 当操纵符改变流的格式状态时,通常改变后的状态对所有后续 IO 都生效,通常最好在不再需要特殊格式时尽快将流恢复到默认状态。
定义在 iostream 种的操纵符
操作符 | 解释 |
---|---|
boolalpha * noboolalpha | 将 true 和 false 输出为字符串 将 true 和 false 输出为 1 和 0 |
showbase * noshowbase | 对整型值输出表示进制的前缀 不生成表示进制的前缀 |
showpoint * noshowpoint | 对浮点值总是显示小数点 只有当浮点值包含小数部分时才显示小数点 |
showpos * noshowpos | 对非负数显示 + 对非负数不显示 + |
uppercase * nouppercase | 在十六进制值中打印 0X,在科学计数法中打印 E 在十六进制值中打印 0x,在科学计数法中打印 e |
* dec hex oct | 整型显示为十进制 整型显示为十六进制 整型显示为八进制 |
left * right internal | 在值的右侧添加填充字符,左对齐输出 在值的左侧添加填充字符,右对齐输出 在符号和值之间添加填充字符,控制负数符号的位置,它左对齐符号,右对齐值,用空格填满所有中间空间 |
fixed scientific hexfloat defaultfolat | 浮点值显示定点十进制 浮点值显示为科学计数法 浮点值显示为十六进制(C++11 特性) 重置浮点数格式为十进制(C++11 特性) |
unitbuf * nounitbuf | 每次输出操作后都刷新缓冲区 恢复正常缓冲区刷新方式 |
* skipws noskipws | 输入运算符跳过空白符 输入运算符不跳过空白符 |
flush ends endl | 刷新 ostream 缓冲区 插入空字符,然后刷新 ostream 缓冲区 插入换行,然后刷新 ostream 缓冲区 |
注: * 表示默认流状态。
控制布尔值的格式
操纵符改变对象格式状态的一个例子是 boolalpha 操纵符。默认情况下,bool 值打印为 1 或 0。我们可以通过对流使用 boolalpha 操纵符来覆盖这种格式:
cout << "default bool values:" << true << " " << false
<< "\nalpha bool values:" << boolalpha
<< true << " " << false << endl;
执行这段程序会得到下面的结果
default bool vaules: 1 0
alpha bool values: true false
一旦向 cout 写入了 boolalpha,我们就改变了 cout 打印 bool 值的方式。后续打印 bool 值的操作就会打印 true 和 false 而非 1 或 0。
为了取消 cout 格式状态的改变,我们可以使用 noboolalpha。
指定整型值的进制
默认情况下,整形值的输入输出使用十进制。我们可以使用操纵符 hex、oct 和 dec 将其改为十六进制、八进制或是改回十进制:
cout << "deafult:" << 20 << " " << 1024 << endl;
cout << "octal:" << oct << 20 << " " << 1024 << endl;
cout << "hex:" << hex << 20 << " " << 1024 << endl;
cout << "decimal:" << dec << 20 << " " << 1024 << endl;
注: 操纵符 hex、oct 和 dec 只影响整形运算对象,浮点值的表示形式不受影响。
在输出中指出进制
默认情况下,当我们打印出数值时,没有可见的线索指出使用的是几进制。如果需要打印出八进制或十六进制,应该使用 showbase 操纵符(操纵符 noshowbase 恢复 cout 状态)。它遵循与真舒畅两种指定禁止相同的规范:
- 前导 0x 或 0X 表示十六进制。默认情况下,十六进制值会以小写打印,我们可以铜鼓哦使用 uppercase 操纵符来输出大写的 X 并将十六进制数字以大写输出。
- 前导 0 表示八进制。
- 无前导字符串表示十进制。
控制浮点数格式
我们可以控制浮点数输出三个格式:
- 以多高精度(多少个数字)打印浮点值
- 数值是打印为十六进制、定点十进制还是科学计数法形式
- 对于没有小数部分的浮点是否打印小数点
默认情况下,
- 浮点数按六位数字精度打印;
- 如果浮点数没有小数部分,则不打印小数点;
- 根据浮点数的值选择打印成顶点十进制或者科学计数法形式。标准库会选择一种可读性更好的格式:非常大和非常小的值打印为科学计数法形式,其他直达因为顶点十进制形式。
指定打印精度
我们可以通过调用 IO 对象的 precision 成员或使用 setprecision 操作符来改变精度。precision 成员是重载的,一个版本接受 int 值,将精度设置为此值并返回旧值。另一个版本不接受参数,返回当前精度值。setprecision 操作符接受一个参数,用来设置精度。
注: 操作符 setprecision 和其他接受参数的操作符都定义在头文件 iomanip 中。
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
cout.precision(12);
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
cout << setprecision(3);
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
结果如下:
Precision: 6, Value: 1.41421
Precision: 12, Value: 1.41421356237
Precision: 3, Value: 1.41
指定浮点数计数法
除非你要控制浮点数的表示形式(如,按列打印数据或打印表示金额或百分比的数据),否则由标准库选择计数法是最好的方式。
输出补白
当按列打印数据时,我们常常需要非常精细地控制数据格式。标准库提供了一些操纵符帮助我们完成所需的控制:
- setw 指定下一个数字或字符串值的最小空间。
- left 表示左对齐输出。
- right 表示右对齐输出,默认格式。
- internal 控制负数的符号的位置,左对齐符号,右对齐值用空格填满所有中间空间。
- setfill 允许指定一个字符代替默认的空格来补白输出。
注: setw 类似 endl,不改变输出流的内部状态。它只决定下一个输出的大小。
定义在 iomanip 中的操纵符
操作符 | 解释 |
---|---|
setfill(ch) | 用 ch 填充空白 |
setprecission(n) | 将浮点精度设置为 n |
setw(w) | 读或写值的宽度为 w 个字符 |
setbase(b) | 将整数输出为 b 进制 |
4.2 未格式化的输入/输出操作
格式化 IO 操作,即,输入和输出运算符根据读取或写入的数据类型来格式化它们。输入运算符忽略空白符,输出运算符应用补白、精度等规则。
标准库还提供了一组低层操作,支持未格式化 IO。这些操作允许我们将流当作一个无解释的字节序列来处理。
单字节操作
有几个未格式化操作每次一个字节的处理流,它们会读取而不是忽略空白符:
char ch;
while(cin.get(ch))
cout.put(ch);
此程序保留输入中的空白符,其输出与输入完全相同。
单字节低层 IO 操作
操作 | 解释 |
---|---|
is.get(ch) | 从 istream is 读取下一个字节存入字符 ch 中,返回 is |
os.put(ch) | 将字符 ch 输出到 ostream os。返回 os |
is.get() | 将 is 的下一个字节作为 int 返回 |
is.putback(ch) | 将字符 ch 返回 is。返回 is |
is.unget() | 将 is 向后移动一个字节,返回 is |
is.peek() | 将下一个字节作为 int 返回,但不从流中删除它 |
将字符放回输入流
有时我们需要读取一个字符才能知道还没准备好处理它。这种情况下,我们希望将字符放回流中。标准库提供了三种方法退回字符:
- peek 返回输入流中下一个字符的副本,但不会将它从流中删除,peek 返回的值仍然留在流中。
- unget 使得输入流向后移动,从而最后读取的值又回到流中。即使我们不知道最后从流中读取什么值,仍然可以调用 unget。
- putback 是更特殊版本的 unget:它退回从流中读取的最后一个值,但它接受一个参数,此参数必须与最后读取的值相同。
一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值。即,标准库不保证在中间不进行读取操作的情况下能连续调用 putback 或 unget。
从输入操作返回 int
函数 peek 和 无参的 get 版本都以 int 类型从输入流返回一个字符,而不是返回一个 char。
这些函数返回一个 int 的原因是:可以返回尾文件标记。我们使用 char 范围中的每个值来表示一个真实字符,因此,取值范围中没有额外的值可以用来表示文件尾。
返回 int 的函数将它们要返回的字符先转换为 unsigned char,然后再将结果提升到 int。因此,即使字符集中由字符映射到负值,这些操作返回的 int 也是正值。而标准库使用负值表示文件尾,这样就可以保证与任何合法字符的值都不同。头文件 cstdio 定义了一个名为 EOF 的 const,我们可以用它来检测从 get 返回的值是否是文件尾,而不必记忆表示文件尾的实际数值。
int ch; // 使用一个 int,而不是一个 char 来保存 get() 的返回值
while( (ch = cin.get()) != EOF)
cout.put(ch);
多字节操作
一些未格式化 IO 操作一次处理大块数据。如果速度是要考虑的重点问题的话,这些操作是很重要的,但是这些操作也容易出问题。热别是,这些操作要求我们自己分配并管理用来保存和提取数据的字符数组。
多字节低层 IO 操作
操作 | 解释 |
---|---|
is.get(sink, size, delim) | 从 is 中读取最多 size 个字节,并保存到字符数组中,字符数组的起始地址由 sink 给出。读取过程直至遇到字符 delim 或读取 size 个字节或遇到文件尾时停止。如果遇到 delim,将其留在输入流中,不读取出来存入 sink |
is.getline(sink, size, delim) | 与接受三个参数的 get 版本类似,但会读取并丢弃 delim |
is.read(sink, size) | 读取最多 size 个字节,存入字符数组 sink 中。返回 is |
is.gcount() | 返回上一个未格式化读取操作从 is 读取的字节数 |
os.write(source, size) | 将数组 source 中 size 个字节写入到 os。返回 os |
is.ignore(size, delim) | 读取并忽略最多 size 个字符,包括 delim;与其他未格式化函数不同,ignore 有默认参数:size 默认为 1,delim 默认为文件尾 |
get 和 getline 函数接收相同的参数,它们的行为类似但不相同。两个函数中,sink 都是一个 char 数组,用来保存数据。两个函数都一直读取数据,直到下面条件一直发生:
- 已读取了 size - 1 个字符
- 遇到了文件尾
- 遇到了分隔符
两个函数的差别就是处理分隔符的方式;get 将分隔符留作 istream 中下一个字符,而 getline 则读取并丢弃分隔符。无论哪个函数都不会将分隔符保存在 sink 中。
注: 一个常见的错误是想从流中删除分隔符,但却忘了做。
确定读取了多少个字符
某些操作从输入读取未知个数的字节。我们可以调用 gcount 来确定最后一个未格式化输入操作读取了多少个字符。应该在任何后续未格式化输入操作之前调用 gcount。特别是,将字符退回流的单字节操作也输入未格式化输入操作。如果在调用 gcount 之前使用了 peek、unget 或 putback,则 gcount 的返回值是 0。
4.3 流随机访问
各种流类型通常都支持对流中数据的随机访问,我们可以重定位流,使之跳过一些数据,首先读取最后一行,然后读取第一行。标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们位置。
注: 随机 IO 本质上是依赖于系统的,为了理解如何使用这些特性,必须查询系统文档。
大多数系统中,绑定到 cin、cout、cerr 和 clog 的流不支持随机访问。
seek 和 tell 函数
标准库实际上定义了两对 seek 和 tell 函数,一对用于输入流,一对用于输出流。
函数 | 解释 |
---|---|
tellg() tellp() | 返回到一个输入流(tellg)或输出流(tellp)标记的当前位置 |
seekg(pos) seekp(pos) | 在一个输入流或输出流中将标记重定位到给定的绝对地址。pos 通常是前一个 tellg 和 tellp 返回的值 |
seekp(off, from) seekg(off, from) | 在一个输入流或输出流中将标记定位到 from 之前或之后 off 个字符,from 可以是下列值之一:beg(偏移量相对流开始的位置)、cur(偏移量相对流当前位置) 和 end(偏移量相对流结尾位置) |
只有一个标记
由于只有单一的标记,因此只要我们在读写操作间切换,就必须进行 seek 操作来重定位标记。
重定位标记
// 将标记移动到一个固定位置
seekg(new_position); // 将读标记移动到指定的 pos_type 类型的位置
seekp(new_position); // 将写标记移动到指定的 pos_type 类型的位置
// 移动到给定七十点之前或之后的指定偏移位置
seekg(offset, from); // 将读标记移动到距 from 偏移量为 offset 的位置
seekp(offset, from); // 将写标记移动到距 from 偏移量为 offset 的位置
访问标记
函数 tellg 和 tellp 返回一个 pos_type 值,表示流的当前位置。tell 函数通常用来记住一个位置,以便稍后再定位回来:
// 记住当前写位置
ostringstream writeStr; // 输出 stringstream
ostringstream::pos_type mark = writeStr.tellp();
// ...
if(cancelEntry)
// 回到刚才记住的位置
writeStr.seekp(mark);
读写同一个文件
int main() {
// 以读写方式打开文件,并定位到文件尾
fstream inOut("copyOut", fstream::ate | fstream::in | fstream::out);
if(!inOut) {
cerr << "Unable to open file!" << endl;
return EXIT_FAILURE;
}
// inOut 以 ate 模式打开,因此一开始就定义到其文件尾
auto end_mark = inOut.tellg(); // 记住原文件尾位置
inOut.seekg(0, fstream::beg); // 重定位到文件开始
size_t cnt = 0; // 字节数累加器
string line; // 保存输入中的每行
// 继续读取的条件:还未遇到错误且还在读取源文件
while( inOut && inOut.tellg() != end_mark && getline(inOut, line)) { // 且还可获取一行输入
cnt += line.size() + 1; // 加 1 表示换行符
auto mark = inOut.tellg(); // 记住读取位置
inOut.seekp(0, fstream::end); // 将写标记移动到文件为
inOut << cnt; // 输出累计的长度
// 如果不是最后一行,打印一个分隔符
if(mark != end_mark)
inOut << " ";
inOut.seekg(mark); // 恢复读位置
}
inOut.seekp(0, ofstream::end); // 定位到文件尾
inOut << "\n"; // 再文件尾输出一个换行符
return 0;
}