
在我们的C++编程学习中,我们已经接触了很多输入输出(IO)库的功能。这些功能就像是程序与外界沟通的桥梁,让我们能够从用户那里获取信息,也能向用户展示结果。
C++标准库为我们提供了一套完整的输入输出系统。其中最基本的就是istream(输入流)和ostream(输出流)这两个类型。istream专门负责输入操作,就像是一个数据接收器;而ostream专门负责输出操作,就像是一个数据发送器。
在实际使用中,我们最常接触的是三个预定义的对象:cin、cout和cerr。cin是一个istream对象,它从标准输入(通常是键盘)读取数据。
cout是一个ostream对象,它向标准输出(通常是屏幕)写入数据。cerr也是一个ostream对象,但它专门用于输出错误信息,通常也是显示在屏幕上。
为了进行数据的读取和写入,我们使用了两个重要的操作符:>>和<<。>>操作符用于从输入流中读取数据,而<<操作符用于向输出流写入数据。此外,还有一个非常有用的函数getline,它能够从指定的输入流中读取一整行文本到字符串中。
到目前为止,我们使用的IO类型和对象主要处理字符(char)数据,并且默认连接到用户的控制台窗口。 但是,真实的程序不能仅仅局限于与控制台窗口进行交互。程序经常需要读取或写入命名文件,比如读取配置文件、保存用户数据等。 此外,使用IO操作来处理字符串中的字符也是非常方便的。还有一些应用程序可能需要读写需要宽字符支持的语言。
为了支持这些不同类型的IO处理,C++标准库除了我们已经使用的istream和ostream类型外,还定义了一系列IO类型。
这些类型定义在三个独立的头文件中:iostream定义了用于从流读取和向流写入的基本类型,fstream定义了用于读写命名文件的类型,sstream定义了用于读写内存中字符串的类型。
iostream头文件包含了istream和wistream(用于从流读取)、ostream和wostream(用于向流写入)、以及iostream和wiostream(用于读写流)。
fstream头文件包含了ifstream和wifstream(用于从文件读取)、ofstream和wofstream(用于向文件写入)、以及fstream和wfstream(用于读写文件)。
sstream头文件包含了istringstream和wistringstream(用于从字符串读取)、ostringstream和wostringstream(用于向字符串写入)、以及stringstream和wstringstream(用于读写字符串)。
宽字符支持
为了支持使用宽字符的语言,C++标准库定义了一套操作wchar_t数据的类型和对象。这些宽字符版本的名称都以字母"w"开头。
例如,wcin、wcout和wcerr分别是cin、cout和cerr对应的宽字符对象。
宽字符类型和对象定义在与普通字符类型相同的头文件中。例如,fstream头文件同时定义了ifstream和wifstream类型。
从概念上讲,无论是设备类型还是字符大小,都不会影响我们想要执行的IO操作。例如,无论我们是从控制台窗口、
磁盘文件还是字符串中读取数据,我们都希望使用>>操作符。同样,无论我们读取的字符是适合char类型还是需要wchar_t类型,我们都希望使用相同的操作符。
C++标准库通过继承机制让我们能够忽略这些不同类型流之间的差异。就像模板一样,我们可以在不了解继承工作原理细节的情况下使用通过继承相关的类。 继承让我们可以说某个特定类继承自另一个类。通常,我们可以像使用继承它的类的对象一样使用继承类的对象。之后我们会详细介绍这个概念。
ifstream和istringstream类型都继承自istream。因此,我们可以像使用istream对象一样使用ifstream或istringstream类型的对象。
我们可以用与使用cin相同的方式使用这些类型的对象。例如,我们可以在ifstream或istringstream对象上调用getline函数,
也可以使用>>从ifstream或istringstream中读取数据。同样,ofstream和ostringstream类型继承自ostream。因此,我们可以用与使用cout相同的方式使用这些类型的对象。
如果我们尝试以下操作,编译器会报错:
|ofstream outputFile1, outputFile2; outputFile1 = outputFile2; // 错误:不能赋值流对象 ofstream createFile(ofstream); // 错误:不能初始化ofstream参数 outputFile2 = createFile(outputFile2); // 错误:不能复制流对象
由于我们不能复制IO类型,我们不能有流类型的参数或返回类型。执行IO操作的函数通常通过引用传递和返回流。 读取或写入IO对象会改变其状态,所以引用不能是const类型。
在进行IO操作时,错误的发生是不可避免的。有些错误是可以恢复的,而有些错误发生在系统深处,超出了程序能够纠正的范围。 IO类定义了一些函数和标志,让我们能够访问和操作流的状态。考虑以下代码:
|int number; cin >> number;
如果我们在标准输入中输入"Hello"而不是数字,读取操作就会失败。输入操作符期望读取一个整数,但却得到了字符'H'。结果,cin会被置于错误状态。同样,如果我们输入文件结束符,cin也会处于错误状态。
一旦发生错误,对该流的后续IO操作都会失败。我们只能在流处于非错误状态时从中读取或向其写入数据。由于流可能处于错误状态,代码通常应该在尝试使用流之前检查流是否正常。
确定流对象状态的最简单方法是将该对象用作条件:
|string word; while (cin >> word) { // 读取操作成功时的处理逻辑 }
while条件检查从>>表达式返回的流的状态。如果该输入操作成功,状态保持有效,条件就会成功。
使用流作为条件只能告诉我们流是否有效,但不能告诉我们发生了什么。有时我们还需要知道流为什么无效。例如,我们在遇到文件结束符后要做的操作可能与遇到IO设备错误后要做的操作不同。
IO库定义了一个机器相关的整型iostate,用于传达关于流状态的信息。这个类型被用作位的集合,就像我们在位操作中使用的变量一样。
IO类定义了四个iostate类型的constexpr值,表示特定的位模式。这些值用于指示特定类型的IO条件,可以与位运算符一起使用来测试或设置多个标志。
badbit表示系统级故障,如不可恢复的读写错误。一旦设置了badbit,通常就不可能再使用该流。
failbit在可恢复错误后设置,例如在期望数值数据时读取了字符。通常可以纠正这些问题并继续使用流。到达文件末尾会同时设置eofbit和failbit。
goodbit保证值为0,表示流上没有失败。如果设置了badbit、failbit或eofbit中的任何一个,那么评估该流的条件就会失败。
库还定义了一组函数来查询这些标志的状态。good操作在没有设置任何错误位时返回true。bad、fail和eof操作在相应位开启时返回true。此外,如果设置了bad,fail也返回true。
因此,确定流整体状态的正确方法是使用good或fail。实际上,当我们使用流作为条件时执行的代码等同于调用!fail()。eof和bad操作只揭示是否发生了这些特定错误。
rdstate成员返回一个iostate值,对应于流的当前状态。setstate操作开启给定的条件位以指示发生了问题。clear成员是重载的:一个版本不接受参数,第二个版本接受一个iostate类型的参数。
不接受参数的clear版本关闭所有失败位。在clear()之后,调用good返回true。我们可以这样使用这些成员:
|// 记住cin的当前状态 auto oldState = cin.rdstate(); cin.clear(); // 使cin有效 processInput(cin); // 使用cin cin.setstate(oldState); // 现在将cin重置为其旧状态
接受参数的clear版本期望一个表示流新状态的iostate值。要关闭单个条件,我们使用rdstate成员和位运算符来产生所需的新状态。
例如,以下代码关闭failbit和badbit但保持eofbit不变:
|// 关闭failbit和badbit,但其他位保持不变 cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
每个输出流都管理一个缓冲区,用于保存程序读取和写入的数据。例如,当执行以下代码时:
|cout << "请输入一个值:";
字面字符串可能会立即打印,或者操作系统可能会将数据存储在缓冲区中以供稍后打印。 使用缓冲区允许操作系统将我们程序的几个输出操作组合成单个系统级写入。 由于向设备写入可能很耗时,让操作系统将几个输出操作组合成单个写入可以提供重要的性能提升。
有几个条件会导致缓冲区被刷新(即写入)到实际的输出设备或文件:
程序正常完成时,所有输出缓冲区都会作为main返回的一部分被刷新。
在某个不确定的时间,缓冲区可能会变满,在这种情况下,它会在写入下一个值之前被刷新。
我们可以使用endl等操纵符显式刷新缓冲区。
我们可以使用unitbuf操纵符设置流的内部状态,以便在每个输出操作后清空缓冲区。
默认情况下,unitbuf为cerr设置,因此对cerr的写入会立即刷新。
输出流可能绑定到另一个流。在这种情况下,每当绑定到它的流被读取或写入时,输出流就会被刷新。
默认情况下,cin和cerr都绑定到cout。因此,读取cin或写入cerr会刷新cout中的缓冲区。
我们的程序已经使用了endl操纵符,它结束当前行并刷新缓冲区。还有两个类似的操纵符:flush和ends。flush刷新流但不向输出添加任何字符;ends向缓冲区插入一个空字符然后刷新它:
|cout << "你好!" << endl; // 写入"你好!"和换行符,然后刷新缓冲区 cout << "你好!" << flush; // 写入"你好!",然后刷新缓冲区;不添加数据 cout << "你好!" << ends; // 写入"你好!"和空字符,然后刷新缓冲区
如果我们想在每次输出后都刷新,可以使用unitbuf操纵符。这个操纵符告诉流在每次后续写入后都进行刷新。nounitbuf操纵符将流恢复到使用正常的、系统管理的缓冲区刷新:
|cout << unitbuf; // 所有写入都将立即刷新 // 任何输出都立即刷新,不缓冲 cout << nounitbuf; // 返回到正常缓冲
注意:程序崩溃时缓冲区不会自动刷新
我们要特别注意一个常见的“坑”:当我们的程序异常终止(比如崩溃)时,输出缓冲区里的内容是不会自动刷新的。
也就是说,程序本来已经用cout等输出流写入了一些数据,但这些数据还在缓冲区里“排队”,并没有真正显示到屏幕上。
所以,在调试程序时,如果你发现某些输出语句好像“没执行”,其实很可能是因为程序还没来得及刷新缓冲区就崩溃了。
为此,我们在关键调试输出后可以加上endl或flush,确保缓冲区被及时刷新。这样可以避免因为输出没被刷新而误判程序流程,节省大量排查时间。
当输入流绑定到输出流时,任何尝试读取输入流的操作都会首先刷新与输出流关联的缓冲区。库将cout绑定到cin,所以语句:
|cin >> value;
会导致与cout关联的缓冲区被刷新。
交互式系统通常应该将其输入流绑定到其输出流。这样做意味着所有输出(可能包括对用户的提示)都会在尝试读取输入之前被写入。
tie有两个重载版本:一个版本不接受参数,返回指向当前绑定到此对象的输出流的指针(如果有的话)。如果流没有绑定,函数返回空指针。tie的第二个版本接受一个指向ostream的指针,并将自己绑定到该ostream。也就是说,x.tie(&o)将流x绑定到输出流o。
我们可以将istream或ostream对象绑定到另一个ostream:
|cin.tie(&cout); // 仅作说明:库为我们绑定cin和cout // oldTie指向当前绑定到cin的流(如果有的话) ostream *oldTie = cin.tie(nullptr); // cin不再绑定 // 将cin和cerr绑定;这不是好主意,因为cin应该绑定到cout cin.tie(&cerr); // 读取cin会刷新cerr,而不是cout cin.tie(oldTie); // 重新建立cin和cout之间的正常绑定
要将给定流绑定到新的输出流,我们向tie传递指向新流的指针。要完全解绑流,我们传递空指针。每个流最多可以同时绑定到一个流。但是,多个流可以将自己绑定到同一个ostream。
fstream头文件定义了三种类型来支持文件IO操作:ifstream用于从指定文件读取数据,ofstream用于向指定文件写入数据,fstream既可以读取也可以写入指定文件。
这些类型提供了与我们之前在cin和cout对象上使用的相同操作。特别是,我们可以使用IO操作符(<<和>>)来读写文件,可以使用getline函数来读取ifstream,并且前面介绍的所有内容都适用于这些类型。
除了从iostream类型继承的行为外,fstream中定义的类型还添加了成员来管理与流关联的文件。这些操作可以调用在fstream、ifstream或ofstream对象上,但不能调用在其他IO类型上。
当我们想要读取或写入文件时,我们定义一个文件流对象并将该对象与文件关联。每个文件流类都定义了一个名为open的成员函数,它执行定位给定文件并适当地打开它以进行读取或写入所需的系统特定操作。
当我们创建文件流时,我们可以(可选地)提供文件名。当我们提供文件名时,open会自动调用:
|ifstream inputFile("data.txt"); // 构造一个ifstream并打开指定文件 ofstream outputFile; // 输出文件流,尚未与任何文件关联
这段代码将inputFile定义为一个输入流,初始化为从字符串参数指定的文件读取。它将outputFile定义为一个输出流,尚未与文件关联。在新标准中,文件名可以是库字符串或C风格字符数组。库的早期版本只允许C风格字符数组。
我们可以在期望原始类型对象的地方使用继承类型对象。这意味着为接受iostream类型之一的引用(或指针)而编写的函数可以为相应的fstream(或sstream)类型调用。
也就是说,如果我们有一个接受ostream&的函数,我们可以传递一个ofstream对象来调用该函数,对于istream&和ifstream也是如此。
例如,我们可以使用前面的read和print函数来从命名文件读取和写入数字。在这个例子中,我们假设输入和输出文件的名称作为参数传递给main:
|ifstream inputFile(argv[1]); // 打开输入文件 ofstream outputFile(argv[2]); // 打开输出文件 int number; // 保存读取的数字 if (read(inputFile, number)) { // 读取第一个数字 int sum = number; // 保存数字总和 while(read(inputFile, number)) { // 读取剩余数字 sum += number; // 累加数字 }
这个简单的例子展示了如何从文件读取数字并计算它们的总和。重要的部分是调用read和print。我们可以将fstream对象传递给这些函数,即使这些函数的参数分别定义为istream&和ostream&。
当我们定义一个空的文件流对象时,我们可以随后通过调用open将该对象与文件关联:
|ifstream inputFile("data.txt"); // 构造一个ifstream并打开指定文件 ofstream outputFile; // 输出文件流,尚未与任何文件关联 outputFile.open("data.txt.copy"); // 打开指定文件
如果open调用失败,会设置failbit。因为open调用可能会失败,通常最好验证打开是否成功:
|if (outputFile) // 检查打开是否成功 // 打开成功,所以我们可以使用文件
这个条件类似于我们在cin上使用的条件。如果打开失败,这个条件将失败,我们不会尝试使用outputFile。
一旦文件流被打开,它就保持与指定文件的关联。实际上,在已经打开的文件流上调用open将失败并设置failbit。
随后尝试使用该文件流将失败。要将文件流与不同文件关联,我们必须首先关闭现有文件。一旦文件关闭,我们就可以打开新文件:
|inputFile.close(); // 关闭文件 inputFile.open("data2.txt"); // 打开另一个文件
如果open成功,那么open设置流的状态,使good()为true。
考虑一个main函数接受它应该处理的文件列表的程序。这样的程序可能有一个类似以下的循环:
|// 对于传递给程序的每个文件 for (auto p = argv + 1; p != argv + argc; ++p) { ifstream inputFile(*p); // 创建输入并打开文件 if (inputFile) { // 如果文件正常,"处理"这个文件 processFile(inputFile); } else cerr << "无法打开:" + string(*p); } // inputFile在每次迭代时超出作用域并被销毁
每次迭代都构造一个名为inputFile的新ifstream对象并打开它以读取给定文件。像往常一样,我们检查打开是否成功。
如果成功,我们将该文件传递给将读取和处理输入的函数。如果不成功,我们打印错误消息并继续。
因为inputFile定义在形成for循环体的块内,它在每次迭代时被创建和销毁。
当fstream对象超出作用域时,它绑定的文件会自动关闭。在下一次迭代中,inputFile被重新创建。
当fstream对象被销毁时,close会自动调用。
每个流都有一个关联的文件模式,表示文件的使用方式。我们可以在打开文件时提供文件模式——要么在调用open时,要么在从文件名初始化流时间接打开文件。
我们可以指定的模式有以下限制:
out模式打开的文件会被截断(即使没有指定trunc)。out打开的文件的现有内容,必须同时指定app(只能在文件末尾写入),或同时指定in(文件可读写)。每个文件流类型都定义一个默认文件模式,在我们没有另外指定模式时使用。与ifstream关联的文件以in模式打开;与ofstream关联的文件以out模式打开;与fstream关联的文件以in和out模式打开。
默认情况下,当我们打开ofstream时,文件的内容会被丢弃。防止ostream清空给定文件的唯一方法是指定app:
|// file1在这些情况下都被截断 ofstream outputFile("file1"); // out和trunc是隐式的 ofstream outputFile2("file1", ofstream::out); // trunc是隐式的 ofstream outputFile3("file1", ofstream::out | ofstream::trunc); // 要保留文件内容,我们必须明确指定app模式 ofstream appendFile("file2", ofstream::app); // out是隐式的
保留由ofstream打开的文件中现有数据的唯一方法是明确指定app或in模式。
给定流的文件模式可能在每次打开文件时改变:
|ofstream outputFile; // 没有设置文件模式 outputFile.open("temp.txt"); // 模式隐式为out和trunc outputFile.close(); // 关闭outputFile以便我们可以将其用于不同文件 outputFile.open("important.txt", ofstream::app); // 模式是out和app outputFile.close();
第一次调用open没有明确指定输出模式;这个文件隐式地以out模式打开。像往常一样,out意味着trunc。
因此,当前目录中名为temp.txt的文件将被截断。当我们打开名为important.txt的文件时,我们要求追加模式。文件中的任何数据都保留,所有写入都在文件末尾完成。
每当我们调用
open时,文件模式都会被设置,无论是明确地还是隐式地。每当没有指定模式时,就使用默认值。
sstream头文件定义了三种类型来支持内存中IO;这些类型从字符串读取或向字符串写入,就像字符串是IO流一样。
istringstream类型读取字符串,ostringstream写入字符串,stringstream读写字符串。
像fstream类型一样,sstream中定义的类型继承自我们在iostream头文件中使用的类型。
除了它们继承的操作外,sstream中定义的类型还添加了成员来管理与流关联的字符串。这些操作可以调用在stringstream对象上,但不能调用在其他IO类型上。
虽然fstream和sstream共享到iostream的接口,但它们没有其他相互关系。特别是,我们不能在stringstream上使用open和close,也不能在fstream上使用str。
当我们需要对整行进行一些工作,并对行内的单个单词进行其他工作时,istringstream经常被使用。
作为一个例子,假设我们有一个文件列出了学生及其成绩。每个学生可能有多个科目的成绩。我们的输入文件可能看起来像这样:
|张三 85 92 78 李四 90 88 王五 76 85 92 88
这个文件中的每条记录都以学生姓名开始,后面跟着一个或多个成绩。我们将首先定义一个简单的结构来表示我们的输入数据:
|// 成员默认是public的 struct StudentInfo { string name; vector<int> scores; };
StudentInfo类型的对象将有一个表示学生姓名的成员和一个保存不同数量成绩的向量。
我们的程序将读取数据文件并构建一个StudentInfo向量。向量中的每个元素都对应于文件中的一条记录。我们将在读取记录然后提取每个学生的姓名和成绩的循环中处理输入:
|string line, word; // 分别保存输入中的一行和一个单词 vector<StudentInfo> students; // 保存输入中的所有记录 // 一次读取一行输入,直到cin遇到文件结束(或其他错误) while (getline(cin, line)) { StudentInfo info; // 创建一个对象来保存这条记录的数据 istringstream record(line); // 将record绑定到我们刚读取的行 record >> info.name; // 读取姓名 while (record >> word) // 读取成绩 info.scores.push_back(stoi(word)); // 转换为整数并存储 students.push_back(info);
这里我们使用getline从标准输入读取整个记录。如果getline调用成功,那么line保存输入文件中的一条记录。
在while循环内,我们定义一个局部StudentInfo对象来保存当前记录的数据。接下来,我们将istringstream绑定到我们刚读取的行。
我们现在可以在该istringstream上使用输入操作符来读取当前记录中的每个元素。我们首先读取姓名,然后是一个while循环,它将读取该学生的成绩。
内部while循环在我们读取完line中的所有数据时结束。这个循环的工作方式类似于我们编写的其他读取cin的循环。
区别在于这个循环从字符串而不是从标准输入读取数据。当字符串被完全读取时,"文件结束"被发出信号,record上的下一个输入操作将失败。
我们通过将我们刚处理的StudentInfo附加到向量来结束外部while循环。外部while继续直到我们在cin上遇到文件结束。
当我们需要一点一点地构建输出但不想立即打印输出时,ostringstream很有用。例如,我们可能想要计算我们在前一个例子中读取的学生成绩的平均分。
如果所有成绩都有效(在0-100范围内),我们想要打印一个包含学生姓名和平均分的新文件。如果学生有任何无效成绩,我们不会将他们放在新文件中。相反,我们将写入一个包含学生姓名和其无效成绩列表的错误消息。
因为我们不想包含任何有无效成绩的学生的数据,我们不能在看到并验证所有他们的成绩之前产生输出。但是,我们可以将输出"写入"到内存中的ostringstream:
|for (const auto &student : students) { // 对于students中的每个学生 ostringstream validScores, invalidScores; // 在每次循环中创建的对象 int sum = 0; int count = 0; for (const auto &score : student.scores) { // 对于每个成绩 if (score < 0 || score > 100) { invalidScores << " " <<
在这个程序中,我们检查每个成绩是否在有效范围内(0-100)。程序的有趣部分是使用字符串流validScores和invalidScores。
我们使用正常的输出操作符(<<)向这些对象写入。但是,这些"写入"实际上是字符串操作。它们分别向validScores和invalidScores内的字符串添加字符。
现在让我们通过一些简单的练习题来巩固本章学到的知识。每道题都涵盖了输入输出流的核心概念,请先尝试独立完成,然后再查看答案。
编写一个程序,从用户输入读取一个整数n,然后读取n个数字,计算它们的平均值并输出。
|#include <iostream> using namespace std; int main() { int n; cout << "请输入数字的个数:"; cin >> n; double sum = 0; double num; cout << "请输入 " << n << " 个数字:" << endl; for (int
创建一个程序,从文件"input.txt"读取一行文本,将其转换为大写字母后写入文件"output.txt"。要求检查文件是否成功打开。
|#include <iostream> #include <fstream> #include <string> #include <cctype> // 用于toupper函数 using namespace std; int main() { // 打开输入文件 ifstream inFile("input.txt"); if (!inFile) { cout << "无法打开输入文件!" << endl; return 1; } // 打开输出文件
编写一个程序,读取一行包含多个数字的字符串(如"12 34 56 78"),使用istringstream解析这些数字,计算它们的总和。
|#include <iostream> #include <sstream> #include <string> using namespace std; int main() { string input; cout << "请输入一行数字(用空格分隔):"; getline(cin, input); // 读取整行 istringstream iss(input); // 创建字符串输入流 int num; int sum = 0; // 从字符串流中读取数字 while
创建一个程序,从用户输入读取学生姓名和成绩,检查成绩是否在0-100范围内。如果不在范围内,输出错误信息并继续读取下一个学生。
|#include <iostream> #include <string> using namespace std; int main() { string name; int score; cout << "请输入学生信息(输入'quit'退出):" << endl; while (true) { cout << "姓名:"; cin >> name; if (name == "quit"
编写一个程序,从文件"students.txt"读取学生姓名和成绩(每行格式:姓名 成绩),计算平均成绩,并将结果写入文件"result.txt"。
|#include <iostream> #include <fstream> #include <string> using namespace std; int main() { // 打开输入文件 ifstream inFile("students.txt"); if (!inFile) { cout << "无法打开输入文件!" << endl; return 1; } // 打开输出文件 ofstream outFile(
这道题展示了基本的输入输出操作。使用 cin 读取用户输入,使用 cout 输出结果。注意要使用 double 类型来存储数字和平均值,以确保小数计算的准确性。
文件操作需要包含 <fstream> 头文件。ifstream 用于读取文件,ofstream 用于写入文件。使用 ! 操作符检查文件是否成功打开。处理完文件后要记得关闭文件。
字符串流让我们可以像处理文件或标准输入一样处理字符串。istringstream 用于从字符串读取数据,ostringstream 用于向字符串写入数据。这在解析格式化的字符串时非常有用。
错误处理是程序健壮性的重要组成部分。通过检查输入的有效性,我们可以避免程序产生错误的结果或崩溃。使用 continue 可以跳过无效输入,继续处理下一个输入。
这个程序综合运用了文件读写、循环处理、数据计算和错误处理。它展示了如何从文件读取格式化数据,进行处理后写入另一个文件。注意检查文件是否成功打开,以及处理空文件的情况。