汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分...

21
C++ I/O 揭秘 本章内容 流的含义 输入输出数据的时候如何使用流 标准库中提供的标准流 一个程序的基本任务是接受输入和产生输出。一个不能产生任何类型输出的程序不会太有用。 所有的语言都提供了某种 I/O 机制,这种机制既有可能内建在语言中,也有可能提供操作系统特定 API 。一个好的 I/O 系统应该兼具灵活性和易用性。灵活的 I/O 系统支持通过不同设备的输入和 输出,例如文件和用户控制台。还支持读写不同类型的数据。I/O 很容易出错,因为来自用户的数 据可能是不正确的,或者底层的文件系统或其他数据源有可能无法访问。因此,一个好的 I/O 系统 还应该能够处理错误条件。 如果您已经熟悉了 C 语言,那么您肯定使用过 printf() scanf()。作为 I/O 机制, printf() scanf() 确实很灵活。通过转义代码和变量占位符,这些函数可以定制为读取特定格式的数据,或输出格式 化代码允许的任何值,此类值局限于整数/ 字符值、浮点值和字符串。然而,printf() scanf()在优秀 I/O 系统的其他指标表现落后。这些函数不能很好地处理错误,处理自定义数据类型不够灵活,而 且最糟的是,在 C++这样的面向对象语言中,它们根本不是面向对象的! C++通过一种称为流(stream) 的机制提供了更为精良的输入输出方法。流是一种灵活且面向对象 I/O 方法。本章介绍如何将流用于数据输出和输入。您还要学习如何通过流机制从不同的来源读 取数据,以及向不同的目标写出数据,例如用户控制台、文件甚至字符串。本章将讲解最常用的 I/O 特性。 15.1 使用流 需要花一些功夫才能习惯流的隐喻。初看上去,流似乎比传统的 C 风格 I/O( 例如 printf()) 要复杂。 事实上,流初看上去更复杂的原因是相比于 printf(),流背后的隐喻更深刻。不过不必担心,了解了 15

Upload: others

Post on 03-Nov-2020

10 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

C++ I/O 揭秘

本章内容

● 流的含义

● 输入输出数据的时候如何使用流

● 标准库中提供的标准流

一个程序的基本任务是接受输入和产生输出。一个不能产生任何类型输出的程序不会太有用。

所有的语言都提供了某种 I/O 机制,这种机制既有可能内建在语言中,也有可能提供操作系统特定

的 API。一个好的 I/O 系统应该兼具灵活性和易用性。灵活的 I/O 系统支持通过不同设备的输入和

输出,例如文件和用户控制台。还支持读写不同类型的数据。I/O 很容易出错,因为来自用户的数

据可能是不正确的,或者底层的文件系统或其他数据源有可能无法访问。因此,一个好的 I/O 系统

还应该能够处理错误条件。

如果您已经熟悉了 C 语言,那么您肯定使用过 printf()和 scanf()。作为 I/O机制,printf()和 scanf()

确实很灵活。通过转义代码和变量占位符,这些函数可以定制为读取特定格式的数据,或输出格式

化代码允许的任何值,此类值局限于整数/字符值、浮点值和字符串。然而,printf()和 scanf()在优秀

I/O 系统的其他指标表现落后。这些函数不能很好地处理错误,处理自定义数据类型不够灵活,而

且最糟的是,在 C++这样的面向对象语言中,它们根本不是面向对象的!

C++通过一种称为流(stream)的机制提供了更为精良的输入输出方法。流是一种灵活且面向对象

的 I/O 方法。本章介绍如何将流用于数据输出和输入。您还要学习如何通过流机制从不同的来源读

取数据,以及向不同的目标写出数据,例如用户控制台、文件甚至字符串。本章将讲解最常用的 I/O

特性。

15.1 使用流

需要花一些功夫才能习惯流的隐喻。初看上去,流似乎比传统的 C 风格 I/O(例如 printf())要复杂。

事实上,流初看上去更复杂的原因是相比于 printf(),流背后的隐喻更深刻。不过不必担心,了解了

15

第 章

Page 2: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

478

一些示例之后,您再也不想用旧式的 I/O了。

15.1.1 流的含义

第 1 章将 cout 流比喻为与数据对应的洗衣滑槽。把一些变量丢到流中,这些变量就会写到用户

屏幕上,即控制台(console)上。更一般地,所有的流都可以看做是数据滑槽。流之间的区别体现在

方向以及关联的来源和目的地。例如,您已经熟悉的 cout 流是一个输出流,因此这个流的方向是“流

出”。这个流将数据写入控制台,因此这个流关联的目的地是“控制台”。还有一个称为 cin 的标准

流,这个流接受来自用户的输入。这个流的方向为“流入”,关联的来源为“控制台”。cout 和 cin

都是 C++在 std 名称空间中预定义的流实例。表 15-1 简要地描述了所有预定义的流。后面一节会解

释缓冲流和非缓冲流之间的区别。

表 15-1

流 说 明

cin 输入流,从“输入控制台”中读取数据

cout 缓冲的输出流,向“输出控制台”写入数据

cerr 非缓冲的输出流,向“错误控制台”写入数据,“错误控制台”通常等同于“输出

控制台”

clog cerr 的缓冲版本

注意,图形用户界面应用程序通常没有一个控制台,即,如果向 cout 写入一些数据,用户无法

看到。如果您在编写一个库,那么绝对不要假定存在 cout、cin、cerr 和 clog,因为不可能知道您编

写的库会应用在控制台应用程序还是 GUI 应用程序。

有关流的另一个要点是流不仅包含数据,还包含一个称为当前位置(current position)的数据。当

前位置指的是流将要进行下一次读或写操作的位置。

15.1.2 流的来源和目标

流这个概念可以应用于任何接受数据或产生数据的对象。因此可以编写基于流的网络类,还可

以编写 MIDI 设备的流式访问。在 C++中,流可以使用 3 个公共的来源和目标。

您已经看到了很多用户(或控制台)流的例子。控制台输入流允许程序在运行时从用户那里获得

输入,因而使得程序具有了交互性。控制台输出流向用户提供反馈和输出结果。

顾名思义,文件流能够从文件系统中读取数据并向文件系统写入数据。文件输入流适用于读取

配置数据、读取保存的文件以及批处理基于文件的数据等任务。文件输出流适用于保存状态数据和

所有输入流都有一个关联的来源。所有输出流都有一个关联的目标。

Page 3: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

479

提供输出等任务。文件流包含了 C 语言输出函数 fprintf()、fwrite()和 fputs()的功能,还包含了 C 语

言输入函数 fscanf()、fread()和 fgets()的功能。

字符串流是将流隐喻应用于字符串类型的例子。使用字符串流的时候,可以像处理其他任何流

一样处理字符数据。就字符串流的大部分功能而言,只不过是为 string 类提供的很多方法能够完成

的功能提供了便利的语法。然而,使用流式语法为优化提供了机会,而且比直接使用 string 类方便

得多。字符串流包含了 sprintf()和 sprintf_s()的功能,以及很多 C 语言字符串格式化函数的功能。

本节主要讲解控制台流(cin 和 cout)。本章后面会列举文件流和字符串流的例子。其他类型的流,

例如打印机输出和网络 I/O等往往和平台相关,因此本书中没有讨论这些流。

15.1.3 流式输出

第 1 章介绍了流式输出,本书中几乎每一章都使用了流式输出。本节首先简单回顾一些基本概

念,然后介绍一些更高级的内容。

1. 输出基本概念

输出流定义在<ostream>头文件中。大部分程序员都会在程序中包含<iostream>头文件,这个头

文件又包含输入流和输出流的头文件。<iostream>头文件还声明了标准控制台输出流 cout。

使用输出流的最简单方法是使用<<运算符。通过<<可以输出 C++的基本类型,例如 int、指针、

double 和字符。此外,C++的 string 类也兼容<<,C 风格的字符串也能正确输出。下面列举一些使

用<<的示例:

int i = 7;

cout << i << endl;

char ch = 'a';

cout << ch << endl;

string myString = "Marni is adorable.";

cout << myString << endl;

代码取自 OutputBasics\OutputBasics.cpp

输出如下所示:

7

a

Marni is adorable.

cout 流是写入到控制台的内建流,控制台也称为标准输出(standard output)。可将<<的使用串联

起来,从而输出多个数据段。这是因为<<运算符返回一个流的引用,因此可以立即对同一个流再次

应用<<运算符。例如:

int j = 11;

cout << "On a scale of 1 to cute, Marni ranks " << j << "!" << endl;

代码取自 OutputBasics\OutputBasics.cpp

Page 4: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

480

输出如下所示:

On a scale of 1 to cute, Marni ranks 11!

C++流可以正确地解析 C 风格的转义字符,例如包含\n 的字符串,但是完成\n 功能更好的方法

是使用内建的 endl 机制。下面的例子使用了 endl,endl 在 std 名称空间中定义,表示行结束符,并

且刷新输出缓冲区。通过一行代码可以输出多行文本。

cout << "Line 1" << endl << "Line 2" << endl << "Line 3" << endl;

代码取自 OutputBasics\OutputBasics.cpp

输出如下所示:

Line 1

Line 2

Line 3

2. 输出流的方法

毫无疑问,<<运算符是输出流最有用的部分。然而,还需要了解一些额外功能。如果看一下

<ostream>头文件,会发现很多重载<<运算符定义的代码行。还可以看到一些有用的公共方法。

put()和 write()

put()和 write()是原始的输出方法。这两个方法接受的不是定义了输出行为的对象或变量,put()

接受一个单独的字符,write()接受一个字符数组。传给这些方法的数据按照原本的形式输出,没有

任何特殊的格式化和处理操作。例如,下面的函数接受一个 C 风格的字符串,并且将这个字符串输

出到控制台,这个函数没有使用<<运算符:

void rawWrite(const char* data, int dataSize)

{

cout.write(data, dataSize);

}

代码取自 Write\Write.cpp

下面这个函数通过 put()方法,将 C 风格字符串的给定索引输出到控制台:

void rawPutChar(const char* data, int charIndex)

{

cout.put(data[charIndex]);

}

代码取自 Put\Put.cpp

flush()

向输出流写入数据的时候,流不一定会将数据立即写入目标。大部分输出流都会进行缓冲,也

就是积累数据,而不是立即将得到的数据写出去。当满足以下条件之一的时候,流进行刷新操作

(flush),即将积累的数据写出:

Page 5: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

481

● 到达某个标记的时候,例如 endl 标记。

● 流离开作用域被析构的时候。

● 要求从对应的输入流输入数据的时候(即要求从 cin 输入的时候,cout 会刷新)。在有关文件

流的小节中,您将学习如何建立这种连接。

● 流缓冲满的时候。

● 显式地要求流刷新缓冲的时候。

显式要求流刷新缓冲的方法是调用流的 flush()方法,如下代码所示:

cout << "abc";

cout.flush(); // abc is written to the console.

cout << "def";

cout << endl; // def is written to the console.

代码取自 Flush\fl ush.cpp

3. 处理输出错误

输出错误可能会在多种情况下出现。有可能试图打开一个不存在的文件。有可能因为磁盘错误

导致写入操作失败,例如磁盘已满。到目前为止,您所读到的使用了流的代码都没有考虑这些可能

性,主要是为了代码简洁。然而,处理任何可能发生的错误是非常重要的。

当一个流处于正常的可用状态时,称这个流是“好的”。调用流的 good()方法可以判断这个流当

前是否处于好的状态。

if (cout.good()) {

cout << "All good" << endl;

}

通过 good()可以方便地获得流的基本验证信息,但是不能提供流不可用的原因。还有一个 bad()

方法提供了稍多信息。如果 bad()返回 true,意味着发生了致命错误(相对于非致命错误,例如遇到文

件结尾)。另一个方法 fail()在最近一次操作失败的时候返回 true,表示下一次操作也会失败。例如,

对输出流调用 flush()之后,可以调用 fail()确保流仍然可用。

cout.flush();

if (cout.fail()) {

cerr << "Unable to flush to standard out" << endl;

}

还可以要求流在发生故障的时候抛出异常。然后可以编写一个 catch 处理程序来捕捉

ios_base::failure 异常,然后对这个异常调用 what()方法获得错误的描述信息,调用 code()方法获得错

误代码。不过,是否能获得有用信息取决于编译器:

cout.exceptions(ios::failbit | ios::badbit | ios::eofbit);

try {

cout << "Hello World." << endl;

不是所有的输出流都有缓冲。例如,cerr 流就不会对输出缓冲。

Page 6: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

482

} catch (const ios_base::failure& ex) {

cerr << "Caught exception: " << ex.what()

<< ", error code = " << ex.code() << endl;

}

代码取自 Exceptions\Exceptions.cpp

通过 clear()方法重置流的错误状态:

cout.clear();

控制台输出流的错误检查不如文件输入输出流的错误检查频繁。这里讨论的方法也适用于其他

类型的流,后面讨论每一种类型的时候都会回顾这些方法。

4. 输出操作算子

流有一项独特的特性,那就是放入数据滑槽的内容并非仅限于数据。C++流还能识别操作算子

(manipulator),操作算子是能够修改流行为的对象,而不是(或者额外提供)流能够操作的数据。

您已经看到了一个操作算子:endl。endl 操作算子封装了数据和行为。endl 算子要求流输出一

个行结束序列,并且刷新缓冲。下面列出了其他有用的操作算子,大部分定义在<ios>和<iomanip>

标准头文件中。列表后面的例子展示了如何使用这些操作算子:

● boolalpha和noboolalpha:要求流将 bool值输出为 true和 false(boolalpha)或 1和0(noboolalpha)。

默认行为是 noboolalpha。

● hex、oct 和 dec:分别以十六进制、八进制和十进制输出数字。

● setprecision:设置输出小数时的小数位数。这是一个参数化的操作算子(也就是说这个操作

算子接受一个参数)。

● setw:设置输出数值数据的字段宽度。这是一个参数化的操作算子。

● setfill:当数字宽度小于指定宽度的时候,设置用于填充的字符。这是一个参数化的操作

算子。

● showpoint 和 noshowpoint:对于不带小数部分的浮点数,强制流总是显示或总是不显示小

数点。

● [C++11] put_money:向流写入一个格式化的货币值。

● [C++11] put_time:向流写入一个格式化的时间值。

下面的例子通过这些操作算子自定义输出。这个例子还使用了第 14 章讨论的 locale概念。

// Boolean values

bool myBool = true;

cout << "This is the default: " << myBool << endl;

cout << "This should be true: " << boolalpha << myBool << endl;

cout << "This should be 1: " << noboolalpha << myBool << endl;

// Simulate "%6d" with streams

int i = 123;

printf("This should be '123': %6d\n", i);

cout << "This should be '123': " << setw(6) << i << endl;

// Simulate "%06d" with streams

printf("This should be '000123': %06d\n", i);

Page 7: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

483

cout << "This should be '000123': " << setfill('0') << setw(6) << i << endl;

// Fill with *

cout << "This should be '***123': " << setfill('*') << setw(6) << i << endl;

// Reset fill character

cout << setfill(' ');

// Floating point values

double dbl = 1.452;

double dbl2 = 5;

cout << "This should be ' 5': " << setw(2) << noshowpoint << dbl2 << endl;

cout << "This should be @@1.452: " << setw(7) << setfill('@') << dbl << endl;

// Format numbers according to your location

cout.imbue(locale(""));

cout << "This is 1234567 formatted according to your location: " << 1234567 << endl;

// C++11 put_money:

cout << "This should be a money amount of 1200, "

<< "formatted according to your location: "

<< put_money("120000") << endl;

// C++11 put_time:

time_t tt;

time(&tt);

tm t;

localtime_s(&t, &tt);

cout << "This should be the current date and time "

<< "formatted according to your location: "

<< put_time(&t, "%c") << endl;

代码取自 Manipulator\Manipulator.cpp

如果您不关心操作算子的概念,通常也能应付过去。流通过 precision()这类方法提供了大部分

相同的功能。例如,以如下这行代码为例:

cout << "This should be '1.2346': " << setprecision(5) << 1.234567 << endl;

这一行代码可以转换为方法调用:

cout.precision(5);

cout << "This should be '1.2346': " << 1.23456789 << endl;

代码取自 Manipulator\Manipulator.cpp

更详细信息请参阅 Wrox 网站上的标准库参考资源。

15.1.4 流式输入

输入流为结构化数据和非结构化数据的读入提供了简单方法。本节以 cin 为例讨论了输入技术,

cin 即控制台输入流。

Page 8: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

484

1. 输入基本概念

通过输入流,可以采用两种简单方法来读取数据。第一种方法类似于<<运算符,向输出流输出

数据。读入数据对应的运算符是>>。通过>>从输入流读入数据的时候,通过代码中提供的变量保存

接受的值。例如,以下程序从用户那里读入一个单词,并将这个单词保存在一个字符串中。然后这

个字符串又被输出到控制台:

string userInput;

cin >> userInput;

cout << "User input was " << userInput << endl;

代码取自 Input\string.cpp

默认情况下,>>运算符根据空白符对输入值标志化。例如,如果一个用户运行以上程序并且键

入 hello there作为输入,那么只有第一个空白字符(在这个例子中为空格符)之前的字符才会被捕捉在

userInput 变量中。输出如下所示:

User input was hello

在输入中包含空白字符的一种方法是使用 get(),本章后面会讨论这个方法。

>>运算符可以用于不同的变量类型,就像<<运算符一样。例如,如果要读取一个整数,那么只

有变量类型的区别:

int userInput;

cin >> userInput;

cout << "User input was " << userInput << endl;

代码取自 Input\int.cpp

通过输入流可以读入多个值,而且可以根据需要混合和匹配类型。例如,下面这个函数摘选自

一个餐馆预订系统,这个函数要求用户输入姓以及聚会就餐的人数:

void getReservationData()

{

string guestName;

int partySize;

cout << "Name and number of guests: ";

cin >> guestName >> partySize;

cout << "Thank you, " << guestName << "." << endl;

if (partySize > 10) {

cout << "An extra gratuity will apply." << endl;

}

}

代码取自 Input\getReservationData.cpp

注意,>>运算符会根据空白字符符号化,因此 getReservationData()函数不允许输入带有空白字

符的姓名。一个解决方法是使用本章后面讲解的 unget()方法。注意,尽管这里使用 cout 的时候没有

通过 endl 或 flush()显式地刷新缓存区,但是仍然可以将文本写入控制台,因为这里使用的 cin 立即

Page 9: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

485

刷新了 cout 缓存区;cin 和 cout 通过这种方式连接在一起。

2. 输入方法

与输出流一样,输入流也提供了一些方法,通过这些方法可以获得比普通>>运算符更底层的

访问。

get()

get()方法允许从流中读入原始输入数据。get()最简单的版本返回流中的下一个字符,还有其他

版本一次读入多个字符。get()常用于避免>>运算符的自动标志化。例如,下面这个函数从输入流中

读入一个由多个词构成的名字,直到读到流尾。

string readName(istream& inStream)

{

string name;

while (inStream.good()) {

int next = inStream.get();

if (next == EOF)

break;

name += next;// Implicitly convert to a char and append.

}

return name;

}

代码取自 Get\Get.cpp

在这个 readName()函数中,有一些有趣的发现:

● 这个函数的参数是一个对 istream 的非 const 引用,而不是一个 const 引用。从一个流读入数

据的方法会改变实际的流(主要改变当前位置),因为这些方法都不是 const 方法。因此,不

能对 const 引用调用这些方法。

● get()的返回值保存在一个 int 中,而不是一个 char 中。因为 get()会返回一些特殊的非字符的

值,例如 EOF(文件尾),因此使用 int。当 next 被追加到一个 string 的时候,被隐式地转换

为一个 char;如果被追加到一个 wstring,则会被转换为一个 wchar_t。

readName()有一点奇怪,因为可以采用两种方式跳出循环。一种方式是流进入“不好的”状态,

另一种方式是达到流尾。另一种从流中读入数据的更常用的方法是使用另一个版本的 get(),这个版

本接受一个字符的引用,并且返回一个流的引用。这种模式利用了一个事实:在条件环境中对一个

输入流求值的时候,只有当输入流可以用于下一次读取的时候才会返回 true。如果遇到错误或者到

达文件尾都会使得流求值为 false。第 18 章讲解了实现这个特性所需要的转换操作的底层细节。同

一个函数的下面这个版本稍微简洁一些:

如果分不清<<和>>的作用,只要联想箭头的方向指向它们的目标即可。在输出流

中,<<指向流本身,因为数据被发送至流。在输入流中,>>指向变量,因为数据被

保存。

Page 10: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

486

string readName(istream& inStream)

{

string name;

char next;

while (inStream.get(next)) {

name += next;

}

return name;

}

代码取自 Get\Get.cpp

unget()

对于大多数场合来说,理解输入流的正确方式是将输入流理解为一个单方向的滑槽。数据丢入

滑槽,然后进入变量。unget()方法打破了这个模型,允许将数据塞回滑槽。

调用 unget()导致流回退一个位置,将前一个读入的字符放回到流中。通过调用 fail()方法可以查

看 unget()是否成功。例如,如果当前位置就是流的起始位置,那么 unget()会失败。

本章前面出现的 getReservationData()函数不允许输入一个带有空白字符的名字。下面的代码使

用了 unget(),允许名字中出现空白字符。这段代码逐字符读入,并检查字符是否为数字。如果字符

不是数字,则将字符添加到 guestName。如果字符是数字,则通过 unget()将这个字符放回到流中,

循环停止,然后通过>>运算符输入一个整数 partySize。后面的“输入操作算子”小节将讨论 noskipws

的意义。

void getReservationData()

{

string guestName;

int partySize = 0;

// Read letters until we find a non-letter

char ch;

cin >> noskipws;

while (cin >> ch) {

if (isdigit(ch)) {

cin.unget();

if (cin.fail())

cout << "unget() failed" << endl;

break;

}

guestName += ch;

}

// Read partysize

cin >> partySize;

cout << "Thank you '" << guestName

<< "', party of " << partySize << endl;

if (partySize > 10) {

cout << "An extra gratuity will apply." << endl;

}

}

代码取自 Unget\Unget.cpp

Page 11: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

487

putback()

putback()和 unget()一样,允许在输入中流反向移动一个字符。区别在于 putback()方法将从参数

接收的字符放回流中:

char ch1;

cin >> ch1;

cin.putback(ch1);

// ch1 will be the next character read off the stream.

peek()

通过 peek()方法可预览调用 get()返回的下一个值。再拿滑槽的隐喻打比方,可以想象为查看一

下滑槽,但是不把值取出来。

peek()非常适合于在取出一个之前需要预先查看这个值的场合。例如下面的代码实现了 get-

ReservationData()函数,允许名字中出现空白字符,但使用的是 peek()而不是 unget():

void getReservationData()

{

string guestName;

int partySize = 0;

// Read letters until we find a non-letter

char ch;

cin >> noskipws;

while (true) {

// 'peek' at next character

ch = cin.peek();

if (!cin.good())

break;

if (isdigit(ch)) {

// next character will be a digit, so stop the loop

break;

}

// next character will be a non-digit, so read it

cin >> ch;

guestName += ch;

}

// Read partysize

cin >> partySize;

cout << "Thank you '" << guestName

<< "', party of " << partySize << endl;

if (partySize > 10) {

cout << "An extra gratuity will apply." << endl;

}

}

代码取自 Peek\Peek.cpp

getline()

从输入流中获得一行数据是一个非常常见的需求,所以有一个方法能完成这个任务。getline()

方法用一行数据填充字符缓冲区,数据量最多至指定大小。指定的大小中包括\0 字符。因此,下面

的代码最多从 cin 中读取 kBufferSize-1 个字符,或者读到行尾为止:

Page 12: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

488

char buffer[kBufferSize];

cin.getline(buffer, kBufferSize);

代码取自 Getline\Getline.cpp

调用 getline()的时候,这个方法从输入流中读取一行,读到行尾为止。不过,行尾字符不会出

现在字符串中。注意,行尾序列和平台相关。例如,行尾序列可以是\r\n、\n 或\n\r。

有一个版本的 get()执行的操作和 getline()一样,区别在于 get()把换行序列留在输入流中。

还有一个用于 C++ string 的名为 getline()的函数。这个函数定义在<string>头文件和 std 名称空间

中。这个函数接受一个流引用,一个 string 引用以及一个可选的分隔符作为参数:

string myString;

std::getline(cin, myString);

代码取自 Getline\Getline.cpp

3. 处理输入错误

输入流提供了一些方法用于检测异常情形。大部分和输入流有关的错误条件都发生在无数据可

读的时候。例如,可能到达了流尾(称为文件尾,即使不是文件流)。查询输入流状态的最常见方法

是在条件语句中访问输入流。例如,只要 cin 保持在“好的”状态,下面的循环持续进行:

while (cin) { ... }

同时可以输入数据:

while (cin >> ch) { ... }

还可以调用 good()方法,就像输出流那样。还有一个名为 eof()的方法,如果流到达尾部的时候

返回 true。

您还应该养成一个读取数据后就检查流状态的习惯,这样可以从异常输入中恢复。

下面的程序展示了从流中读取数据并处理错误的常用模式。这个程序从标准输入中读取数字,

到达文件结尾的时候显示这些数字的总和。注意在命令行环境中,需要用户键入一个特殊的字符表

示文件结束。在 Unix 和 Linux 中,这个特殊的字符是 Control+D,在 Windows 中为 Control+Z。具

体的字符与操作系统相关,因此您还需要了解操作系统要求的字符:

cout << "Enter numbers on separate lines to add. "

<< "Use Control+D to finish (Control+Z in Windows)." << endl;

int sum = 0;

if (!cin.good()) {

cerr << "Standard input is in a bad state!" << endl;

return 1;

}

int number;

while (true) {

cin >> number;

if (cin.good()) {

sum += number;

} else if (cin.eof()) {

Page 13: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

489

break; // Reached end of file

} else {

// Error!

cin.clear(); // Clear the error state.

string badToken;

cin >> badToken; // Consume the bad input.

cerr << "WARNING: Bad input encountered: " << badToken << endl;

}

}

cout << "The sum is " << sum << endl;

代码取自 ErrorCheck\ErrorCheck.cpp

4. 输入操作算子

下面的列表中列出了内建的输入操作算子,输入操作算子可以发送到输入流中以便自定义数据

读入的行为。

● boolalpha 和 noboolalpha:如果使用了 boolalpha,字符串 false 会解释为布尔值 false;其他

任何字符串都会被解释为布尔值 true。如果设置了 noboolalpha,0 会被解释为 false,其他任

何值都被解释为 true。默认行为是 noboolalpha。

● hex、oct 和 dec:分别以十六进制、八进制和十进制读入数字。

● skipws 和 noskipws:告诉输入流在标记化的时候跳过空白字符,或者读入空白字符作为

标记。

● ws:一个简便的操作算子,表示跳过流中当前位置的一串空白字符。

● [C++11] get_money:从流中读入一个货币值。

● [C++11] get_time:从流中读入一个格式化的时间值。

输入支持 locale。例如,下面的代码将 cin 的 locale 设置为系统 locale。第 14 章讨论了 locale:

cin.imbue(locale(""));

int i;

cin >> i;

如果系统 locale 为 U.S. English,那么输入 1,000 会被解析为 1000。如果系统 locale 为 Dutch

Belgium,那么输入 1.000 会被解析为 1000。

15.1.5 对象的输入输出

如果不是基本类型,也可以通过<<运算符输出一个 C++字符串。在 C++中,对象可以描述其输

出和输入的方式。这是通过重载<<和>>运算符完成的,重载的运算符可以理解新的类型或类。

为什么要重载这些运算符?如果您已经熟悉了 C 语言中的 printf()函数,那么您应该知道 printf()

在这方面并不灵活。尽管 printf()知道多种数据类型,但是无法让其知道更多的知识。例如,考虑下

面这个简单的类:

class Muffin

{

public:

string getDescription() const;

Page 14: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

490

void setDescription(const string& inDesc);

int getSize() const;

void setSize(int inSize);

bool getHasChocolateChips() const;

void setHasChocolateChips(bool inChips);

protected:

string mDesc;

int mSize;

bool mHasChocolateChips;

};

string Muffin::getDescription() const { return mDesc; }

void Muffin::setDescription(const string& inDesc) { mDesc = inDesc; }

int Muffin::getSize() const { return mSize; }

void Muffin::setSize(int inSize) { mSize = inSize; }

bool Muffin::getHasChocolateChips() const { return mHasChocolateChips; }

void Muffin::setHasChocolateChips(bool inChips) { mHasChocolateChips = inChips; }

代码取自 Muffin\Muffi n.cpp

为了通过 printf()输出 Muffin 类的对象,如果能将其指定为参数,然后再用%m 这样的占位符就

好了。

printf("Muffin output: %m\n", myMuffin); // BUG! printf doesn't understand Muffin.

遗憾的是,printf()函数完全不了解 Muffin 类型,因此无法输出 Muffin 类型的对象。最糟糕的情

况是,由于 printf()函数的声明方式,这样的代码会导致运行时错误,而不是一个编译时错误(不过一

个好的编译器会给出一个警告消息)。

如果要使用 printf(),最多在 Muffin 类中添加一个新的 output()方法。

class Muffin

{

public:

string getDescription() const;

void setDescription(const string& inDesc);

int getSize() const;

void setSize(int inSize);

bool getHasChocolateChips() const;

void setHasChocolateChips(bool inChips);

void output();

protected:

string mDesc;

int mSize;

bool mHasChocolateChips;

};

// Other method implementations omitted for brevity

void Muffin::output()

{

printf("%s, Size is %d, %s\n", getDescription().c_str(), getSize(),

(getHasChocolateChips() ? "has chips" : "no chips"));

}

代码取自 Muffin\Muffin.cpp

Page 15: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

491

不过,使用这种机制非常笨拙。如果要在另一行文本的中间输出一个 Muffin,那么需要将这一

行分解为两个调用,在两个调用之间插入一个 Muffin::output()调用,如下所示:

printf("The muffin is ");

myMuffin.output();

printf(" -- yummy!\n");

通过重载<<运算符,Muffin 的输出就像输出一个 string 一样简单——只要将其作为<<的参数即

可。第 18 章讲解了运算符<<和>>的重载。

15.2 字符串流

可以通过字符串流将流语义用于 string。通过这种方式,可得到一个内存内的流(in memory

stream),通过这个流表示文本数据。例如,在一个 GUI 应用程序中,可能需要用流来构建文本数据,

但是不想将文本输出到控制台或文件中,而是想要作为结果显示在 GUI 元素中,例如消息框和编辑

框。另一个例子是,假如想要将一个字符串流作为参数传给不同函数,同时想要维护当前的读位置,

这样每一个函数都可以处理流的下一部分。字符串流也非常适于解析文本,因为流内建了标记化的

功能。

ostringstream 类用于将数据写入 string,istringstream 用于将数据从一个 string 中读出。这两个类

都定义在<sstream>头文件中。由于 ostringstream 和 istringstream 分别继承了来自 ostream 和 istream

的同样功能,因此这些类的使用也非常类似。

下面的程序从用户那里请求单词,然后将这些单词输入到一个 ostringstream 中,通过制表符将

单词分开。在程序的最后,整个流通过 str()方法转换为一个 string 对象,并写入控制台。可以通过

输入标记“done”来停止标记的输入,或通过按下 Control+D(Unix)或 Control+Z(Windows)来关闭输

入流。

cout << "Enter tokens. Control+D (Unix) or Control+Z (Windows) to end" << endl;

ostringstream outStream;

while (cin) {

string nextToken;

cout << "Next token: ";

cin >> nextToken;

if (nextToken == "done")

break;

outStream << nextToken << "\t";

}

cout << "The end result is: " << outStream.str();

代码取自 StringStream\StringStream.cpp

从一个字符串流中读入数据非常类似。下面的函数创建一个 Muffin 对象,并通过从字符串输入

流中读入的数据填充这个对象(参见此前的例子)。流数据格式固定,因此这个函数可以轻松地将数

据值转换为对 Muffin 类的设置方法的调用:

Page 16: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

492

Muffin createMuffin(istringstream& inStream)

{

Muffin muffin;

// Assume data is properly formatted:

// Description size chips

string description;

int size;

bool hasChips;

// Read all three values. Note that chips is represented

// by the strings "true" and "false"

inStream >> description >> size >> boolalpha >> hasChips;

muffin.setSize(size);

muffin.setDescription(description);

muffin.setHasChocolateChips(hasChips);

return muffin;

}

代码取自 Muffin\Muffin.cpp

相比于标准 C++ string,字符串流最主要的好处是除了数据之外,这个对象还知道从哪里进行

下一次读或写操作,这个位置也称为当前位置。根据特定的字符串流的实现,可能还会有性能优势。

例如,如果需要将大量字符串串联在一起,使用字符串流的效率可能比反复调用 string 对象的+=运

算符的效率更高。

15.3 文件流

文件本身非常符合流的抽象,因为文件读写的时候除了数据之外还涉及读写的位置。在 C++中,

ofstream 和 ifstream 类提供了文件的输出和输入功能。这两个类在<fstream>头文件中定义。

在处理文件系统的时候,错误情形的检测和处理非常重要。您正在处理的文件可能在一个刚刚

下线的网络存储中,或者您可能写入一个已满磁盘上的文件。也许您试图打开一个用户没有访问权

限的文件。可以通过前面描述的标准错误处理机制检测到错误情形。

输出文件流和其他输出流的一个主要区别在于:文件流的构造函数可以接受文件名以及打开文

件的模式作为参数。默认模式是写文件,ios_base::out,这个模式从文件开头写文件,改写任何已有

的数据。在文件流构造函数的第二个参数指定常量 ios_base::app,还可按追加模式打开输出文件流。

表 15-2 列出了可供使用的不同常量:

将一个对象转换为一个“扁平”类型(例如 string)的过程通常称为编组(marshall)。

将对象保存至磁盘或通过网络发送的时候,编组操作非常有用。

Page 17: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

493

表 15-2

常 量 说 明

ios_base::app 打开文件,在每一次写操作之前,移到文件末尾

ios_base::ate 打开文件,打开之后立即移到文件末尾

ios_base::binary 以二进制模式执行输入和输出操作(相对于文本模式)

ios_base::in 打开文件作为输入

ios_base::out 打开文件作为输出

ios_base::trunc 打开文件,并截断任何已有数据

下面的程序打开文件 test,并输出程序的参数。ifstream 和 ofstream 的析构函数会自动关闭底层

文件,因此不需要显式调用 close():

int main(int argc, char* argv[])

{

ofstream outFile("test");

if (!outFile.good()) {

cerr << "Error while opening output file!" << endl;

return -1;

}

outFile << "There were " << argc << " arguments to this program." << endl;

outFile << "They are: " << endl;

for (int i = 0; i < argc; i++) {

outFile << argv[i] << endl;

}

return 0;

}

代码取自 FileStream\FileStream1.cpp

15.3.1 通过 seek()和 tell()在文件中转移

所有的输入和输出流都有 seek()和 tell()方法,但是在文件流的上下文之外很少有意义。

seek()方法允许在输入或输出流中移动到任意位置。seek()有好几种形式。输入流中的 seek()方法

实际上称为 seekg()(g 表示 get 的意思),输出流中 seek()的版本称为 seekp()(p 表示 put 的意思)。您可

能想知道为什么同时存在 seekg()和 seekp()方法,而不是一个 seek()方法。原因是有的流既可以输入

又可以输出,例如文件流。在这种情况中,流需要记住一个读位置和一个独立的写位置。另外还有

本章后面讨论的双向 I/O。

seekg()和 seekp()有两个重载。一个重载接受一个参数:绝对位置,这个重载定位到这个绝对位

置。另一个重载接受一个偏移量和一个位置,这个重载定位到给定位置相对偏移量的位置。位置的

Page 18: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

494

类型为 ios_base::streampos,偏移量的类型为 ios_base::streamoff,这两个类型都以字节计数。预定义

的三个位置如表 15-3 所示。

表 15-3

位 置 说 明

ios_base::beg 表示流的开头

ios_base::end 表示流的结尾

ios_base::cur 表示流的当前位置

例如,要定位到输出流的一个绝对位置,可以使用接受一个参数的 seekp()版本,如下例所示,

这个例子通过 ios_base::beg 常量定位到流的开头位置:

outStream.seekp(ios_base::beg);

在输入流中定位完全一样,只不过用的是 seekg()方法:

inStream.seekg(ios_base::beg);

接受两个参数的版本定位到流中的相对位置。第一个参数表示要移动的位置数,第二个参数表

示起始点。要相对文件起始位置移动,使用 ios_base::beg 常量。要相对文件末尾位置移动,使用

ios_base::end 常量。要相对文件当前位置移动,使用 ios_base::cur 常量。例如,下面这行代码从流起

始位置移动到第二个字节。注意,整数被隐式地转换为 ios_base::streampos 和 ios_base::streamoff

类型:

outStream.seekp(2, ios_base::beg);

下面这个例子转移到输入流中倒数第 3 个字节:

inStream.seekg(-3, ios_base::end);

可以通过 tell()方法查询流的当前位置,这个方法返回一个表示当前位置的 ios_base::streampos

值。利用这个结果,可在进行 seek()之前记住当前标记的位置,还可以查询是否在某个特定的位置。

和 seek()一样,输入流和输出流也有不同版本的 tell()。输入流使用的是 tellg(),输出流使用的是 tellp()。

下面的代码检查输入流的当前位置,并判断是否在起始位置:

ios_base::streampos curPos = inStream.tellg();

if (ios_base::beg == curPos) {

cout << "We're at the beginning." << endl;

}

下面是一个整合了所有内容的示例程序。这个程序写入一个名为 test.out 的文件,并且执行以下

测试:

(1) 将字符串 12345 输出至文件。

Page 19: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

495

(2) 验证标记在流中的位置 5。

(3) 转移到输出流的位置 2。

(4) 在位置 2 输出 0,并关闭输出流。

(5) 在文件 test.out 文件上打开输入流。

(6) 将第一个标记以整数的形式读入。

(7) 确认这个值是否为 12045。

ofstream fout("test.out");

if (!fout) {

cerr << "Error opening test.out for writing" << endl;

return 1;

}

// 1. Output the string "12345".

fout << "12345";

// 2. Verify that the marker is at position 5.

ios_base::streampos curPos = fout.tellp();

if (5 == curPos) {

cout << "Test passed: Currently at position 5" << endl;

} else {

cout << "Test failed: Not at position 5" << endl;

}

// 3. Move to position 2 in the stream.

fout.seekp(2, ios_base::beg);

// 4. Output a 0 in position 2 and close the stream.

fout << 0;

fout.close();

// 5. Open an input stream on test.out.

ifstream fin("test.out");

if (!fin) {

cerr << "Error opening test.out for reading" << endl;

return 1;

}

// 6. Read the first token as an integer.

int testVal;

fin >> testVal;

// 7. Confirm that the value is 12045.

const int expected = 12045;

if (testVal == expected) {

cout << "Test passed: Value is " << expected << endl;

} else {

cout << "Test failed: Value is not " << expected

<< " (it was " << testVal << ")" << endl;

}

代码取自 FileStream\FileStream2.cpp

15.3.2 将流连接在一起

任何输入和输出流之间都可以建立连接,从而实现“访问时刷新”的行为。换句话说,当从输

入流请求数据的时候,连接的输出流会自动刷新。这种行为可用于所有流,但是对于互相可能存在

依赖关系的文件流来说特别有用。

Page 20: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第Ⅱ部分 专业的 C++编码方法

496

通过 tie()方法完成流的连接。要将输出流连接至一个输入流,对输入流调用 tie()方法,并且传

入输出流的地址。要解除连接,传入 nullptr。

下面的程序将一个文件的输入流连接至一个完全不同的文件的输出流。您可以连接至同一个文

件的输出流,但是双向 I/O(详见稍后的描述)可能是实现同时读写同一个文件的更优雅的方式。

ifstream inFile("input.txt");

ofstream outFile("output.txt");

// Set up a link between inFile and outFile.

inFile.tie(&outFile);

// Output some text to outFile. Normally, this would

// not flush because std::endl was not sent.

outFile << "Hello there!";

// outFile has NOT been flushed.

// Read some text from inFile. This will trigger flush()

// on outFile.

string nextToken;

inFile >> nextToken;

// outFile HAS been flushed.

代码取自 tie\tie.cpp

在 ostream 基类上定义 flush()方法,因此可将一个输出流连接至另一个输出流:

outFile.tie(&anotherOutputFile);

这种关系意味着:每次写入一个文件的时候,发送给另一个文件的缓冲数据会被写入。可以通

过这种机制保持两个相关文件的同步。

这种流连接的一个例子是 cout 和 cin 之间的连接。每当从 cin 输入数据的时候,cout 都会自动

刷新。

15.4 双向 I/O

目前,本章已经讨论了输入和输出流,讨论的时候把这两个类当做独立但又关联的类。事实上,

有一种流可以同时执行输入和输出。双向流可以同时以输入流和输出流的方式操作。

双向流是 iostream 的子类,而 iostream 是 istream 和 ostream 的子类,因此这是一个多重继承的

实例。跟直觉一样,双向流同时支持>>运算符和<<运算符,还支持输入流和输出流的方法。

fstream 类提供了双向文件流。fstream 特别适合于需要替换文件中数据的应用程序,因为可以通

过读取文件找到正确的位置,然后立即切换为写入文件。例如,假设一个程序保存一个 ID 号和电

话号码之间的映射列表。这个程序可能使用以下格式的数据文件:

123 408-555-0394

124 415-555-3422

164 585-555-3490

100 650-555-3434

一个合理的方案是当这个程序打开文件的时候读取整个数据文件,然后在程序结束的时候,将

所有的变化重新写入这个文件。然而,如果数据集巨大,可能无法把所有数据都保存在内存中。如

Page 21: 汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃images.china-pub.com/ebook3680001-3685000/3684434/ch15.pdf第Ⅱ部分 专业的C++编码方法 478 一些示例之后,您再也不想用旧式的I/O了。

第 15 章 C++ I/O 揭秘

497

果使用 iostream,则不需要这样。您可以轻松扫描文件找到记录,然后以追加模式打开输出文件来

添加新记录。如果要修改已有记录,可以使用双向流,例如下面的函数替换指定 ID的电话号码:

bool changeNumberForID(const string& inFileName, int inID,

const string& inNewNumber)

{

fstream ioData(inFileName.c_str());

if (!ioData) {

cerr << "Error while opening file " << inFileName << endl;

return false;

}

// Loop until the end of file

while (ioData.good()) {

int id;

string number;

// Read the next ID.

ioData >> id;

// Check to see if the current record is the one being changed.

if (id == inID) {

// Seek to the current read position

ioData.seekp(ioData.tellg());

// Output a space, then the new number.

ioData << " " << inNewNumber;

break;

}

// Read the current number to advance the stream.

ioData >> number;

}

return true;

}

代码取自 Bidirectional\Bidirectional.cpp

当然,只有在数据大小固定的时候这种方法才能正常工作。当以上程序从读取切换到写入的时

候,输出数据会改写文件中的其他数据。为了保持文件的格式,并且避免写入下一条记录,数据必

须相同大小。

还可以通过 stringstream 类双向访问字符串流。

15.5 本章小结

流为输入和输出提供了一种灵活且面向对象的方式。本章中最重要的内容是流的概念,这个概

念甚至比流的使用还要重要。有一些操作系统可能有自己的文件访问和 I/O 工具,但是掌握了流和

以流的方式工作的库的知识是使用任何类型现代 I/O系统的关键。

双向流用不同的指针保存读位置和写位置。在读取和写入之间切换的时候,需要

定位到正确的位置。