第27章 使用流进行输入和输出

    在本书第 1章,您使用了 std::cout在屏幕上显示Hello World;实际上,从该章起您就一直在使用流。现在该给予C++的这部分应有的关注,从实用的角度介绍流。

    在本章中,您将学习:

    • 什么是流,如何使用它们;

    • 如何使用流来读写文件;

    • 有用的C++流操作。

    27.1 流的概述

    假设您要开发一个程序,它从磁盘读取数据、将数据显示到屏幕上、从键盘读取用户输入以及将数据存储到磁盘中。在这种情况下,倘若不管数据来自或前往什么设备或位置,都能以相同的方式处理读写操作,那该有多好!这正是C++流提供的功能。

    C++流是读写(输入和输出)逻辑的通用实现,让您能够统一的模式读写数据。不管是磁盘或键盘读取数据,还是将输入写入显示器或磁盘,这些模式都相同。您只需使用合适的流类,类的实现将负责处理与设备和操作系统相关的细节。

    再来看一下您编写的第一个程序(程序清单1.1)中相关的代码行:

    第27章 使用流进行输入和输出 - 图1

    std:cout 是 ostream 类的一个对象,用于输出到控制台。要使用 std::cout,需要包含提供它的头文件<iostream>,这个头文件还提供了std::cin,让您能够从流中读取数据。

    那么,我说流让您能够以一致的方式访问不同的设备时,是什么意思呢?如果要将Hello World写入文本文件,可将同样的语法用于文件流对象fsHW:

    第27章 使用流进行输入和输出 - 图2

    正如您看到的,选择正确的流类后,将Hello World写入文件与将其显示到屏幕上并没有太大的不同。

    第27章 使用流进行输入和输出 - 图3用于写入流时,运算符<<被称为流插入运算符,可将其用于写入屏幕、文件等。用于将流中的数据写入变量时,运算符>>被称为流提取运算符,可将其用于从键盘、文件等读取输入。

    接下来,本章将从实用的角度探讨流。

    27.2 重要的C++流类和流对象

    C++提供了一组标准类和头文件,可帮助您执行重要而常见的输入/输出操作。表27.1列出了您将经常使用的类。

    表27.1 std命名空间中常用的C++流类

    第27章 使用流进行输入和输出 - 图4

    第27章 使用流进行输入和输出 - 图5cout、cin和cerr分别是流类ostream、istream和ostream的全局对象。由于是全局对象,它们在main()开始之前就已初始化。

    使用流类时,可指定为您执行特定操作的控制符(manipulator)。std::endl就是一个这样的控制符,您一直在使用它来插入换行符:

    第27章 使用流进行输入和输出 - 图6

    表27.2列出了其他几个控制符和标志。

    表27.2 std命名空间中常用于流的控制符

    第27章 使用流进行输入和输出 - 图7

    27.3 使用std::cout将指定格式的数据写入控制台

    std::cout用于写入到标准输出流,可能是本书前面使用得最多的流。下面更详细地介绍它,并使用一些控制符来改变数据的对齐和显示方式。

    27.3.1 使用std::cout修改数字的显示格式

    可以让cout以十六进制或八进制方式显示整数。程序清单27.1演示了如何使用cout以各种格式显示输入的数字。

    程序清单27.1 使用cout和<iomanip>控制符以十进制、十六进制和八进制格式显示整数

    第27章 使用流进行输入和输出 - 图8

    输出:

    第27章 使用流进行输入和输出 - 图9

    分析:

    这个代码示例使用了表27.2所示的控制符,以修改cout显示用户输入的整数Input的方式。注意到第10和11行使用了控制符oct和hex。第14行使用了setiosflags()让cout以十六进制方式(并使用大写字母)显示该数字,其结果是cout将253显示为OXFD。第18行使用了resetiosflags(),其效果是再次使用cout显示该整数时,将显示为十进制。要将显示整数时使用的基数改为十进制,也可使用下面这种方式:

    第27章 使用流进行输入和输出 - 图10

    对于诸如Pi等数字,可指定cout显示它们时使用的精度(小数点后面的位数),还可指定以定点表示法或科学表示法显示它们。程序清单27.2演示了如何设置这些格式。

    程序清单27.2 使用cout以定点表示法和科学表示法显示Pi和圆面积

    第27章 使用流进行输入和输出 - 图11

    输出:

    第27章 使用流进行输入和输出 - 图12

    分析:

    输出表明,第7行和第10行分别将精度设置为7和10后,显示的Pi值不同。另外,控制符scientific导致计算得到的圆面积被显示为 6.2731491429e + 002。

    27.3.2 使用std::cout对齐文本和设置字段宽度

    可使用setw()控制符来设置字段宽度,插入到流中的内容将在指定宽度内右对齐。在这种情况下,还可使用setfill()指定使用什么字符来填充空白区域,如程序清单27.3所示。

    程序清单27.3 使用控制符setw()设置字段宽度,并使用setfill()指定填充字符

    第27章 使用流进行输入和输出 - 图13

    输出:

    第27章 使用流进行输入和输出 - 图14

    分析:

    第8行使用了setw(35),而第11行使用了setw(35)和setfill(‘*’),输出说明了这样做的效果。从输出可知,第11行导致使用setfill()指定的星号来填充文本前的空白区域。

    27.4 使用std::cin进行输入

    std::cin多才多艺,让您能够将输入读取到基本类型(如int、double以及C风格字符串char*)变量中。您还可使用getline()从键盘读取一行输入。

    27.4.1 使用std::cin将输入读取到基本类型变量中

    使用cin可将标准输入读取到int、double和char变量中,程序清单27.4演示了如何读取用户输入的简单数据类型。

    程序清单27.4 使用cin将输入读取到int变量中,将使用科学表示法的浮点数读取到double变量中,将三个字符分别读取到char变量中

    第27章 使用流进行输入和输出 - 图15

    输出:

    第27章 使用流进行输入和输出 - 图16

    分析:

    在程序清单27.4中,最有趣的部分是,您使用指数表示法输入Pi的值时,cin也将其读取到了double变量Pi中。注意到可以使用一行代码将输入读取到三个字符变量中,如第15行所示。

    27.4.2 使用std::cin:get将输入读取到char数组中

    cin让您能够将输入直接写入int变量,也可将输入直接写入char数组(C风格字符串):

    第27章 使用流进行输入和输出 - 图17

    写入C风格字符串缓冲区时,务必不要超越缓冲区的边界,以免导致程序崩溃或带来安全隐患,这至关重要。因此,将输入读取到char数组(C风格字符串)时,下面是一种更好的方法:

    第27章 使用流进行输入和输出 - 图18

    这种将文本插入到char数组(C风格字符串)的方式更安全,程序清单27.5演示了这一点。

    程序清单27.5 插入到C风格字符串中时不超越其边界

    第27章 使用流进行输入和输出 - 图19

    输出:

    第27章 使用流进行输入和输出 - 图20

    分析:

    从输出可知,只将用户输入的前 9 个字符读取到了 C 风格字符串中,这是因为第 8 行使用的是cin:get。处理C风格字符串时,这是最安全的方式。

    第27章 使用流进行输入和输出 - 图21尽可能不要使用C风格字符串和char数组;只要可能,就应使用std::string而不是char*。

    27.4.3 使用std::cin将输入读取到std::string中

    cin多才多艺,甚至可使用它将用户输入的字符串直接读取到std::string中:

    第27章 使用流进行输入和输出 - 图22

    程序清单27.6演示了如何使用cin将输入读取到std::string。

    程序清单27.6 使用cin将文本插入到std::string中

    第27章 使用流进行输入和输出 - 图23

    输出:

    第27章 使用流进行输入和输出 - 图24

    分析:

    输出表明,并未按程序设计的那样显示整个姓名。我在第8行使用了cin,希望Name存储整个姓名,而不仅仅是名字。为什么会这样呢?显然是由于cin遇到空白后停止插入。

    要读取整行输入(包括空白),需要使用getline():

    第27章 使用流进行输入和输出 - 图25

    程序清单27.7演示了如何结合使用getline()和cin。

    程序清单27.7 使用getline()和cin读取整行用户输入

    第27章 使用流进行输入和输出 - 图26

    输出:

    第27章 使用流进行输入和输出 - 图27

    分析:

    第8行的getline()确保不跳过空白字符,现在输出包含整行用户输入。

    27.5 使用std::fstream处理文件

    C++提供了std::fstream,旨在以独立于平台的方式访问文件。std::fstream从std::ofstream那里继承了写入文件的功能,并从std::ifstream那里继承了读取文件的功能。

    换句话说,std::fstream提供了读写文件的功能。

    第27章 使用流进行输入和输出 - 图28要使用std::fstream类或其基类,需要包含头文件<fstream>:

    第27章 使用流进行输入和输出 - 图29

    27.5.1 使用open()和close()打开和关闭文件

    要使用fstream、ofstream或ifstream类,需要使用方法open()打开文件:

    第27章 使用流进行输入和输出 - 图30

    open()接受两个参数:第一个是要打开的文件的路径和名称(如果没有提供路径,将假定为应用程序的当前目录设置),第二个是文件的打开模式。在上述代码中,指定了模式 ios_base::trunc(即便指定的文件存在,也重新创建它)、ios_base::in(可读取文件)和ios_base::out(可写入文件)。

    注意到在上述代码中使用了is_open(),它检测open()是否成功。

    第27章 使用流进行输入和输出 - 图31别忘了关闭文件流以保存其内容,这至关重要。

    还有另一种打开文件流的方式,那就是使用构造函数:

    第27章 使用流进行输入和输出 - 图32

    如果只想打开文件进行写入,可使用如下代码:

    第27章 使用流进行输入和输出 - 图33

    如果只想打开文件进行读取,可使用如下代码:

    第27章 使用流进行输入和输出 - 图34

    第27章 使用流进行输入和输出 - 图35无论是使用构造函数还是成员方法open()来打开文件流,都建议您在使用文件流对象前,使用open()检查文件打开操作是否成功。

    可在下述各种模式下打开文件流。

    • ios_base::app:附加到现有文件末尾,而不是覆盖它。

    • ios_base::ate:切换到文件末尾,但可在文件的任何地方写入数据。

    • ios_base::trunc:导致现有文件被覆盖,这是默认设置。

    • ios_base::binary:创建二进制文件(默认为文本文件)。

    • ios_base::in:以只读方式打开文件。

    • ios_base::out:以只写方式打开文件。

    27.5.2 使用open()创建文本文件并使用运算符<<写入文本

    有打开的文件流后,便可使用运算符<<向其中写入文本,如程序清单27.8所示。

    程序清单27.8 使用ofstream新建一个文本文件并向其中写入文本

    第27章 使用流进行输入和输出 - 图36

    输出:

    第27章 使用流进行输入和输出 - 图37

    分析:

    第7行以ios_base::out模式(即只写模式)打开文件。第9行检查open()是否成功,然后使用插入运算符<<写入该文件流,如第13和14行所示。最后,第17行关闭文件流。

    第27章 使用流进行输入和输出 - 图38程序清单27.8表明,写入文件的方式与使用cout写入到标准输出(控制台)的方式相同。这表明,C++流让您能够以类似的方式处理不同的设备:使用cout将文本显示到屏幕的方式与使用ofstream写入文件的方式相同。

    27.5.3 使用open()和运算符>>读取文本文件

    要读取文件,可使用fstream或ifstream,并使用标志ios_base::in打开它。程序清单27.9演示了如何读取程序清单27.8创建的文件HelloFile.txt。

    程序清单27.9 从程序清单27.8创建的文件HelloFile.txt中读取文本

    第27章 使用流进行输入和输出 - 图39

    输出:

    第27章 使用流进行输入和输出 - 图40

    第27章 使用流进行输入和输出 - 图41鉴于程序清单27.9读取程序清单27.9创建的文本文件HelloFile.txt,因此您需要将该文件移到该项目的工作目录,或者将该程序清单合并到前一个程序清单中。

    分析:

    与往常一样,您使用is_open()检查第8行调用open()是否成功。请注意,这里没有使用提取运算符>>将文件内容直接读取到第18行使用cout显示的string,而是使用getline()从文件流中读取输入,这与程序清单27.7使用它来读取用户输入的方式完全相同:每次读取一行。

    27.5.4 读写二进制文件

    写入二进制文件的流程与前面介绍的流程差别不大,重要的是在打开文件时使用 ios_base::binary标志。通常使用ofstream::write和ifstream::read来读写二进制文件,如程序清单27.10所示。

    程序清单27.10 将一个结构写入二进制文件并使用该文件的内容创建一个结构

    第27章 使用流进行输入和输出 - 图42

    输出:

    第27章 使用流进行输入和输出 - 图43

    分析:

    第 22~31 行创建了结构 Human 的一个实例(该结构包含属性 Name、Age 和 BOD),并使用ofstream 将其持久化到磁盘中的二进制文件 MyBinary.bin 中。接下来,第 33~34 使用另一个类型为ifstream的流对象读取这些信息。输出的Name等属性是从二进制文件中读取的。该示例还演示了如何使用ifstream::read和ofstream::write来读写文件。注意到第29行使用了reinterpret_cast,它让编译器将结构解释为char*。第38行使用C风格类型转换方式,这与第29行的类型转换方式等价。

    第27章 使用流进行输入和输出 - 图44如果不是处于解释的目的,我将把结构Human及其属性持久化到一个XML文件中。XML是一种基于文本和标记的存储格式,在持久化信息方面提供了灵活性和可扩展性。

    发布这个程序后,如果您对其进行升级,给结构Human添加了新属性(如NumChildren),则需要考虑新版本使用的 ifstream::read,确保它能够正确地读取旧版本创建的二进制数据。

    27.6 使用std::stringstream对字符串进行转换

    假设您有一个字符串,它包含字符串值45,如何将其转换为整型值45呢?如何将整型值45转换为字符串45呢?C++提供的stringstream类是最有用的工具之一,让您能够执行众多的转换操作。

    第27章 使用流进行输入和输出 - 图45要使用std::stringstream类,需要包含头文件<sstream>:

    第27章 使用流进行输入和输出 - 图46

    程序清单27.11演示了一些简单的stringstream操作。

    程序清单27.11 使用std::stringstream在整型和字符串之间进行转换

    第27章 使用流进行输入和输出 - 图47

    输出:

    第27章 使用流进行输入和输出 - 图48

    分析:

    该程序让用户输入一个整型值,并使用运算符<<将其插入到一个stringstream对象中,如第12行所示;然后,您使用提取运算符将这个整数转换为string,如第14行所示。接下来,您将存储在strInput中的字符串转换为整数,并将其存储到Copy中。

    第27章 使用流进行输入和输出 - 图49

    27.7 总结

    本章从实用的角度介绍了流。您了解到,从本书开头起,您就一直在使用输入/输出流,如 cout和cin。现在,您知道了如何创建简单的文本文件以及如何读写这种文件。您了解到,stringstream可帮助您在简单类型(如整型)和字符串之间进行转换。

    27.8 问与答

    问:我发现,可使用fstream来读取和写入文件,那么什么情况下该使用ofstream和ifstream呢?

    答:如果您的代码或模块只需读取文件,就应使用ifstream;同样,如果您的代码或模块只需写入文件,就应使用 ofstream。在这两种情形下,都可使用 fstream,但为确保数据和代码的完整性,最好像使用const那样采取更严厉的策略,不过并非必须这样做。

    问:什么情况下应使用cin.get()?什么情况下应使用cin.getline()?

    答:cin.getline()确保您捕获用户输入的整行,包括空白在内;cin.get()帮助您以每次一个字符的方式捕获用户输入。

    问:什么情况下应使用stringstream?

    答:stringstream提供了一种方便的途径,让您能够在整型及其他简单类型和字符串之间进行转换,程序清单27.11演示了这一点。

    27.9 作业

    作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录D的答案。在继续学习下一章前,请务必弄懂这些答案。

    27.9.1 测验

    1.在只需写入文件的情况下,应使用哪种流?

    2.如何使用cin从输入流中获取一整行?

    3.在需要将std::string对象写入文件时,应使用ios_base::binary模式吗?

    4.使用open()打开流后,为何还要使用is_open()进行检查?

    27.9.2 练习

    1.查错:找出下述代码中的错误:

    第27章 使用流进行输入和输出 - 图50

    2.查错:找出下述代码中的错误:

    第27章 使用流进行输入和输出 - 图51