C++のIO库
IO库
部分IO库设施:
istream:输入流类型,提供输入操作。ostream:输出流类型,提供输出操作。cin:istream对象,从标准输入读取数据。cout:ostream对象,向标准输出写入数据。cerr:ostream对象,向标准错误写入数据。>>运算符:从istream对象读取输入数据。<<运算符:向ostream对象写入输出数据。getline函数:从istream对象读取一行数据,写入string对象。
IO类(The IO Classes)
头文件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
宽字符版本的IO类型和函数的名字以w开始,如wcin、wcout和wcerr分别对应cin、cout和cerr。它们与其对应的普通char版本都定义在同一个头文件中,如头文件fstream定义了ifstream和wifstream类型。
可以将派生类的对象当作其基类的对象使用。
IO象无拷贝或赋值(No Copy or Assign for IO Objects)
不能拷贝或对IO对象赋值。
1 | ofstream out1, out2; |
这些代码中出现的错误主要是由于 C++ 的语法和对象语义导致的。让我们逐个解释这些错误:
out1 = out2; // error: cannot assign stream objects- 错误原因:C++ 中的流对象(如
ofstream)是不可拷贝的。这是因为拷贝流对象可能会导致不确定的行为,比如如果两个对象同时尝试关闭同一个文件句柄可能会出错。 - 解决方法:如果需要类似行为,可以考虑使用指针或引用,或者使用移动语义(如果支持的话)。
- 错误原因:C++ 中的流对象(如
ofstream print(ofstream); // error: can't initialize the ofstream parameter错误原因:在函数声明或定义中,参数的类型必须是确定的。在这里,
ofstream并不是类型,而应该是一个参数名。解决方法:应该提供一个参数名来标识参数的用途,比如:
1
ofstream print(ofstream& outStream); // 正确:接受一个 ofstream 引用作为参数
out2 = print(out2); // error: cannot copy stream objects- 错误原因:与第一条相同,流对象不能被拷贝。
- 解决方法:如果想要修改
out2对象的状态,可以传递引用或指针给函数,并在函数内部修改对象的状态。
由于IO对象不能拷贝,因此不能将函数形参或返回类型定义为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。
条件状态(Condition States)
IO库条件状态:
| 状态 | 含义 |
|---|---|
strm::iostate |
流的条件状态 |
strm::badbit |
流已崩溃 |
strm::failbit |
一个IO操作失败 |
strm::badbit |
流已崩溃 |
s.eof() |
若流s的eofbit置位,返回true |
s.fail() |
若流s的failbit或badbit置位,返回true |
s.bad() |
若流s的badbit置位,返回true |
s.good() |
若流s处于有效状态,返回true |
s.clear() |
将流s的所有条件状态复位并将流置为有效 |
s.clear(flags) |
将流s的条件状态置为flags |
s.rdstate() |
返回流的条件状态 |
badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法继续使用了。在发生可恢复错误后,failbit会被置位,如期望读取数值却读出一个字符。如果到达文件结束位置,eofbit和failbit都会被置位。如果流未发生错误,则goodbit的值为0。如果badbit、failbit和eofbit任何一个被置位,检测流状态的条件都会失败。
1 | while (cin >> word) |
good函数在所有错误均未置位时返回true。而bad、fail和eof函数在对应错误位被置位时返回true。此外,在badbit被置位时,fail函数也会返回true。因此应该使用good或fail函数确定流的总体状态,eof和bad只能检测特定错误。
流对象的rdstate成员返回一个iostate值,表示流的当前状态。setstate成员用于将指定条件置位(叠加原始流状态)。clear成员的无参版本清除所有错误标志;含参版本接受一个iostate值,用于设置流的新状态(覆盖原始流状态)。
1 | // remember the current state of cin |
管理输出缓冲(Managing the Output Buffer)
每个输出流都管理一个缓冲区,用于保存程序读写的数据。导致缓冲刷新(即数据真正写入输出设备或文件)的原因有很多:
- 程序正常结束。
- 缓冲区已满。
- 使用操纵符(如
endl)显式刷新缓冲区。 - 在每个输出操作之后,可以用
unitbuf操纵符设置流的内部状态,从而清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。 - 一个输出流可以被关联到另一个流。这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。默认情况下,
cin和cerr都关联到cout,因此,读cin或写cerr都会刷新cout的缓冲区。
flush操纵符刷新缓冲区,但不输出任何额外字符。ends向缓冲区插入一个空字符,然后刷新缓冲区。
1 | cout << "hi!" << endl; // writes hi and a newline, then flushes the buffer |
cout << "hi!" << endl;- 输出字符串 "hi!" 并且在末尾添加一个换行符。
- 会刷新输出缓冲区,这意味着输出会立即显示在控制台上。
cout << "hi!" << flush;- 输出字符串 "hi!",但不会添加换行符。
- 会刷新输出缓冲区,这样输出会立即显示在控制台上。
cout << "hi!" << ends;- 输出字符串 "hi!" 并在末尾添加一个空字符('\0')。
- 会刷新输出缓冲区,这样输出会立即显示在控制台上。
总结来说,它们都能达到即时输出的效果,但 endl
会在输出末尾添加一个换行符,而 flush 和 ends
则不会。
如果想在每次输出操作后都刷新缓冲区,可以使用unitbuf操纵符。它令流在接下来的每次写操作后都进行一次flush操作。而nounitbuf操纵符则使流恢复使用正常的缓冲区刷新机制。
1 | cout << unitbuf; // all writes will be flushed immediately |
如果程序异常终止,输出缓冲区不会被刷新。
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将cout和cin关联在一起,因此下面的语句会导致cout的缓冲区被刷新:
1 | cin >> ival; |
交互式系统通常应该关联输入流和输出流。这意味着包括用户提示信息在内的所有输出,都会在读操作之前被打印出来。
使用tie函数可以关联两个流。它有两个重载版本:无参版本返回指向输出流的指针。如果本对象已关联到一个输出流,则返回的就是指向这个流的指针,否则返回空指针。tie的第二个版本接受一个指向ostream的指针,将本对象关联到此ostream。
每个流同时最多关联一个流,但多个流可以同时关联同一个ostream。向tie传递空指针可以解开流的关联。
文件输入输出(File Input and Output)
头文件fstream定义了三个类型来支持文件IO:ifstream从给定文件读取数据,ofstream向指定文件写入数据,fstream可以同时读写指定文件。
| 操作 | 含义 |
|---|---|
fstream fstrm(s) |
打开s文件,打开模式取决于fstream |
fstream fstrm(s, mode) |
以mode模式打开s文件 |
fstrm.close() |
关闭文件 |
fstrm.is_open() |
如果文件成功打开并尚未关闭,返回true |
使用文件流对象(Using File Stream Objects)
每个文件流类型都定义了open函数,它完成一些系统操作,定位指定文件,并视情况打开为读或写模式。
创建文件流对象时,如果提供了文件名(可选),open会被自动调用。
1 | ifstream in(ifile); // construct an ifstream and open the given file |
这两行代码涉及到文件流对象的构造和打开文件的操作。
ifstream in(ifile);- 这行代码创建了一个输入文件流对象
in,并且尝试打开名为ifile的文件以供读取。 - 在这里,
ifile是一个表示文件路径或文件名的字符串变量或常量。文件被打开后,你就可以使用in对象来从该文件中读取数据。
- 这行代码创建了一个输入文件流对象
ofstream out;- 这行代码创建了一个输出文件流对象
out,但是并没有与任何文件相关联。 - 在这种情况下,
out对象是一个未关联的输出流,你可以通过它进行输出操作,但是输出将不会被写入到任何文件中,而是会留在内存中或者被送到标准输出(通常是终端窗口)。
- 这行代码创建了一个输出文件流对象
在C++11中,文件流对象的文件名可以是string对象或C风格字符数组。旧版本的标准库只支持C风格字符数组。
在要求使用基类对象的地方,可以用继承类型的对象代替。因此一个接受iostream类型引用或指针参数的函数,可以用对应的fstream类型来调用。
可以先定义空文件流对象,再调用open函数将其与指定文件关联。如果open调用失败,failbit会被置位。
对一个已经打开的文件流调用open会失败,并导致failbit被置位。随后试图使用文件流的操作都会失败。如果想将文件流关联到另一个文件,必须先调用close关闭当前文件,再调用clear重置流的条件状态(close不会重置流的条件状态)。
当fstream对象被销毁时,close会自动被调用。
文件模式(File Modes)
每个流都有一个关联的文件模式,用来指出如何使用文件。
| 模式 | 含义 |
|---|---|
in |
以读方式打开 |
out |
以写方式打开 |
app |
每次写操作前定位到文件末尾 |
ate |
打开文件后立即定位到文件末尾 |
trunc |
截断文件 |
binary |
以二进制方式读写 |
- 只能对
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模式打开。
默认情况下,打开ofstream对象时,文件内容会被丢弃,阻止文件清空的方法是同时指定app或in模式。
流对象每次打开文件时都可以改变其文件模式。
1 | ofstream out; // no file mode is set |
这段代码涉及到文件流对象的创建、打开、关闭以及文件模式的设置。
ofstream out;- 这行代码创建了一个未关联到任何文件的输出文件流对象
out。
- 这行代码创建了一个未关联到任何文件的输出文件流对象
out.open("scratchpad");- 这行代码打开了名为 "scratchpad" 的文件,并且使用默认的模式,即输出模式(out),如果文件不存在,则创建该文件;如果文件已存在,则将其截断为零长度。
out.close();- 这行代码关闭了之前打开的文件,这样可以释放资源,并且将
out对象重新设置为没有关联到任何文件。
- 这行代码关闭了之前打开的文件,这样可以释放资源,并且将
out.open("precious", ofstream::app);- 这行代码打开了名为 "precious" 的文件,并且指定了附加模式(app),表示在文件末尾追加内容。如果文件不存在,则创建该文件。
out.close();- 这行代码再次关闭了打开的文件,并且释放资源。
总结:
- 在第一次调用
open()时,文件模式没有显式设置,因此默认为输出模式和截断模式。 - 在第二次调用
open()时,通过指定ofstream::app,将模式设置为输出和追加,即新内容会追加到文件末尾。 - 在每次打开文件后,都调用了
close()来关闭文件并释放资源,这样可以确保在打开另一个文件之前关闭当前文件,避免资源泄露。
string流(string
Streams)
头文件sstream定义了三个类型来支持内存IO:istringstream从string读取数据,ostringstream向string写入数据,stringstream可以同时读写string的数据。
| 操作 | 含义 |
|---|---|
sstream strm(s) |
strm保存string s的拷贝 |
strm.str() |
返回strm中的string |
strm.str(s) |
将string s拷贝至strm |
使用istringstream(Using
an istringstream)
1 | // members are public by default |
这段代码是一个简单的程序,用于从标准输入读取人员信息,包括姓名和电话号码,并将其存储在一个
vector 中。
首先,定义了一个结构体
PersonInfo,其中包含了两个成员变量:name:用于存储人员姓名的字符串。phones:用于存储人员电话号码的字符串向量。
然后定义了两个字符串变量
line和word,分别用于从输入流中读取一行和一个单词。接着定义了一个
vector,名为people,用于存储所有输入记录的PersonInfo结构体。使用
while循环,不断地从标准输入中读取一行内容,直到getline(cin, line)返回假值(即遇到文件末尾或者出现错误)为止。在每次迭代中,创建一个
PersonInfo结构体对象info,用于存储当前记录的数据。使用
istringstream对象record将当前读取的行绑定到输入流上,以便于从中读取数据。使用
record >> info.name读取行中的第一个单词(即姓名),并将其存储到info.name成员变量中。使用内层的
while循环,不断从record中读取单词,并将其存储到info.phones中,直到record >> word返回假值(即当前行已读取完毕)为止。将完整的
PersonInfo结构体对象info添加到people向量中,以保存该记录的完整信息。
最终,people
向量中存储了所有输入记录的完整信息,每个记录都包含了姓名和一个电话号码的向量。
使用ostringstream(Using
ostringstreams)
1 | for (const auto &entry : people) |
这段代码是一个循环,用于遍历存储在 people
向量中的每个人员信息,并对其电话号码进行格式化和验证。
for (const auto &entry : people)- 这是一个基于范围的循环,用于遍历
people向量中的每个元素(每个人员信息),其中entry是循环迭代过程中当前元素的引用。
- 这是一个基于范围的循环,用于遍历
ostringstream formatted, badNums;- 在每次循环迭代开始时,创建了两个
ostringstream对象formatted和badNums,分别用于存储格式化后的电话号码和无效的电话号码。
- 在每次循环迭代开始时,创建了两个
for (const auto &nums : entry.phones)- 这是一个嵌套的基于范围的循环,用于遍历当前人员信息中的电话号码(存储在
entry.phones中)。
- 这是一个嵌套的基于范围的循环,用于遍历当前人员信息中的电话号码(存储在
if (!valid(nums))- 检查当前电话号码是否有效,
valid(nums)函数用于判断电话号码是否有效。
- 检查当前电话号码是否有效,
badNums << " " << nums;- 如果电话号码无效,则将其附加到
badNums对象中,以便后续打印出现错误的号码。
- 如果电话号码无效,则将其附加到
formatted << " " << format(nums);- 如果电话号码有效,则将其格式化后的结果附加到
formatted对象中,以便后续打印格式化后的号码。
- 如果电话号码有效,则将其格式化后的结果附加到
if (badNums.str().empty())- 检查
badNums对象中是否有无效的电话号码。如果没有,则表示所有电话号码都是有效的。
- 检查
os << entry.name << " " << formatted.str() << endl;- 打印当前人员信息的姓名和格式化后的电话号码到标准输出流
os中。
- 打印当前人员信息的姓名和格式化后的电话号码到标准输出流
else- 如果存在无效的电话号码,则输出错误消息到标准错误流
cerr中,指出哪些号码无效。
- 如果存在无效的电话号码,则输出错误消息到标准错误流





