第27章 使用流进行输入和输出
在本书第 1章,您使用了 std::cout在屏幕上显示Hello World;实际上,从该章起您就一直在使用流。现在该给予C++的这部分应有的关注,从实用的角度介绍流。
在本章中,您将学习:
• 什么是流,如何使用它们;
• 如何使用流来读写文件;
• 有用的C++流操作。
27.1 流的概述
假设您要开发一个程序,它从磁盘读取数据、将数据显示到屏幕上、从键盘读取用户输入以及将数据存储到磁盘中。在这种情况下,倘若不管数据来自或前往什么设备或位置,都能以相同的方式处理读写操作,那该有多好!这正是C++流提供的功能。
C++流是读写(输入和输出)逻辑的通用实现,让您能够统一的模式读写数据。不管是磁盘或键盘读取数据,还是将输入写入显示器或磁盘,这些模式都相同。您只需使用合适的流类,类的实现将负责处理与设备和操作系统相关的细节。
再来看一下您编写的第一个程序(程序清单1.1)中相关的代码行:
std:cout 是 ostream 类的一个对象,用于输出到控制台。要使用 std::cout,需要包含提供它的头文件<iostream>,这个头文件还提供了std::cin,让您能够从流中读取数据。
那么,我说流让您能够以一致的方式访问不同的设备时,是什么意思呢?如果要将Hello World写入文本文件,可将同样的语法用于文件流对象fsHW:
正如您看到的,选择正确的流类后,将Hello World写入文件与将其显示到屏幕上并没有太大的不同。
用于写入流时,运算符<<被称为流插入运算符,可将其用于写入屏幕、文件等。用于将流中的数据写入变量时,运算符>>被称为流提取运算符,可将其用于从键盘、文件等读取输入。
接下来,本章将从实用的角度探讨流。
27.2 重要的C++流类和流对象
C++提供了一组标准类和头文件,可帮助您执行重要而常见的输入/输出操作。表27.1列出了您将经常使用的类。
表27.1 std命名空间中常用的C++流类
cout、cin和cerr分别是流类ostream、istream和ostream的全局对象。由于是全局对象,它们在main()开始之前就已初始化。
使用流类时,可指定为您执行特定操作的控制符(manipulator)。std::endl就是一个这样的控制符,您一直在使用它来插入换行符:
表27.2列出了其他几个控制符和标志。
表27.2 std命名空间中常用于流的控制符
27.3 使用std::cout将指定格式的数据写入控制台
std::cout用于写入到标准输出流,可能是本书前面使用得最多的流。下面更详细地介绍它,并使用一些控制符来改变数据的对齐和显示方式。
可以让cout以十六进制或八进制方式显示整数。程序清单27.1演示了如何使用cout以各种格式显示输入的数字。
程序清单27.1 使用cout和<iomanip>控制符以十进制、十六进制和八进制格式显示整数
输出:
分析:
这个代码示例使用了表27.2所示的控制符,以修改cout显示用户输入的整数Input的方式。注意到第10和11行使用了控制符oct和hex。第14行使用了setiosflags()让cout以十六进制方式(并使用大写字母)显示该数字,其结果是cout将253显示为OXFD。第18行使用了resetiosflags(),其效果是再次使用cout显示该整数时,将显示为十进制。要将显示整数时使用的基数改为十进制,也可使用下面这种方式:
对于诸如Pi等数字,可指定cout显示它们时使用的精度(小数点后面的位数),还可指定以定点表示法或科学表示法显示它们。程序清单27.2演示了如何设置这些格式。
程序清单27.2 使用cout以定点表示法和科学表示法显示Pi和圆面积
输出:
分析:
输出表明,第7行和第10行分别将精度设置为7和10后,显示的Pi值不同。另外,控制符scientific导致计算得到的圆面积被显示为 6.2731491429e + 002。
可使用setw()控制符来设置字段宽度,插入到流中的内容将在指定宽度内右对齐。在这种情况下,还可使用setfill()指定使用什么字符来填充空白区域,如程序清单27.3所示。
程序清单27.3 使用控制符setw()设置字段宽度,并使用setfill()指定填充字符
输出:
分析:
第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.4中,最有趣的部分是,您使用指数表示法输入Pi的值时,cin也将其读取到了double变量Pi中。注意到可以使用一行代码将输入读取到三个字符变量中,如第15行所示。
27.4.2 使用std::cin:get将输入读取到char数组中
cin让您能够将输入直接写入int变量,也可将输入直接写入char数组(C风格字符串):
写入C风格字符串缓冲区时,务必不要超越缓冲区的边界,以免导致程序崩溃或带来安全隐患,这至关重要。因此,将输入读取到char数组(C风格字符串)时,下面是一种更好的方法:
这种将文本插入到char数组(C风格字符串)的方式更安全,程序清单27.5演示了这一点。
程序清单27.5 插入到C风格字符串中时不超越其边界
输出:
分析:
从输出可知,只将用户输入的前 9 个字符读取到了 C 风格字符串中,这是因为第 8 行使用的是cin:get。处理C风格字符串时,这是最安全的方式。
尽可能不要使用C风格字符串和char数组;只要可能,就应使用std::string而不是char*。
27.4.3 使用std::cin将输入读取到std::string中
cin多才多艺,甚至可使用它将用户输入的字符串直接读取到std::string中:
程序清单27.6演示了如何使用cin将输入读取到std::string。
程序清单27.6 使用cin将文本插入到std::string中
输出:
分析:
输出表明,并未按程序设计的那样显示整个姓名。我在第8行使用了cin,希望Name存储整个姓名,而不仅仅是名字。为什么会这样呢?显然是由于cin遇到空白后停止插入。
要读取整行输入(包括空白),需要使用getline():
程序清单27.7演示了如何结合使用getline()和cin。
程序清单27.7 使用getline()和cin读取整行用户输入
输出:
分析:
第8行的getline()确保不跳过空白字符,现在输出包含整行用户输入。
27.5 使用std::fstream处理文件
C++提供了std::fstream,旨在以独立于平台的方式访问文件。std::fstream从std::ofstream那里继承了写入文件的功能,并从std::ifstream那里继承了读取文件的功能。
换句话说,std::fstream提供了读写文件的功能。
要使用std::fstream类或其基类,需要包含头文件<fstream>:
27.5.1 使用open()和close()打开和关闭文件
要使用fstream、ofstream或ifstream类,需要使用方法open()打开文件:
open()接受两个参数:第一个是要打开的文件的路径和名称(如果没有提供路径,将假定为应用程序的当前目录设置),第二个是文件的打开模式。在上述代码中,指定了模式 ios_base::trunc(即便指定的文件存在,也重新创建它)、ios_base::in(可读取文件)和ios_base::out(可写入文件)。
注意到在上述代码中使用了is_open(),它检测open()是否成功。
别忘了关闭文件流以保存其内容,这至关重要。
还有另一种打开文件流的方式,那就是使用构造函数:
如果只想打开文件进行写入,可使用如下代码:
如果只想打开文件进行读取,可使用如下代码:
无论是使用构造函数还是成员方法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新建一个文本文件并向其中写入文本
输出:
分析:
第7行以ios_base::out模式(即只写模式)打开文件。第9行检查open()是否成功,然后使用插入运算符<<写入该文件流,如第13和14行所示。最后,第17行关闭文件流。
程序清单27.8表明,写入文件的方式与使用cout写入到标准输出(控制台)的方式相同。这表明,C++流让您能够以类似的方式处理不同的设备:使用cout将文本显示到屏幕的方式与使用ofstream写入文件的方式相同。
要读取文件,可使用fstream或ifstream,并使用标志ios_base::in打开它。程序清单27.9演示了如何读取程序清单27.8创建的文件HelloFile.txt。
程序清单27.9 从程序清单27.8创建的文件HelloFile.txt中读取文本
输出:
鉴于程序清单27.9读取程序清单27.9创建的文本文件HelloFile.txt,因此您需要将该文件移到该项目的工作目录,或者将该程序清单合并到前一个程序清单中。
分析:
与往常一样,您使用is_open()检查第8行调用open()是否成功。请注意,这里没有使用提取运算符>>将文件内容直接读取到第18行使用cout显示的string,而是使用getline()从文件流中读取输入,这与程序清单27.7使用它来读取用户输入的方式完全相同:每次读取一行。
写入二进制文件的流程与前面介绍的流程差别不大,重要的是在打开文件时使用 ios_base::binary标志。通常使用ofstream::write和ifstream::read来读写二进制文件,如程序清单27.10所示。
程序清单27.10 将一个结构写入二进制文件并使用该文件的内容创建一个结构
输出:
分析:
第 22~31 行创建了结构 Human 的一个实例(该结构包含属性 Name、Age 和 BOD),并使用ofstream 将其持久化到磁盘中的二进制文件 MyBinary.bin 中。接下来,第 33~34 使用另一个类型为ifstream的流对象读取这些信息。输出的Name等属性是从二进制文件中读取的。该示例还演示了如何使用ifstream::read和ofstream::write来读写文件。注意到第29行使用了reinterpret_cast,它让编译器将结构解释为char*。第38行使用C风格类型转换方式,这与第29行的类型转换方式等价。
如果不是处于解释的目的,我将把结构Human及其属性持久化到一个XML文件中。XML是一种基于文本和标记的存储格式,在持久化信息方面提供了灵活性和可扩展性。
发布这个程序后,如果您对其进行升级,给结构Human添加了新属性(如NumChildren),则需要考虑新版本使用的 ifstream::read,确保它能够正确地读取旧版本创建的二进制数据。
27.6 使用std::stringstream对字符串进行转换
假设您有一个字符串,它包含字符串值45,如何将其转换为整型值45呢?如何将整型值45转换为字符串45呢?C++提供的stringstream类是最有用的工具之一,让您能够执行众多的转换操作。
要使用std::stringstream类,需要包含头文件<sstream>:
程序清单27.11演示了一些简单的stringstream操作。
程序清单27.11 使用std::stringstream在整型和字符串之间进行转换
输出:
分析:
该程序让用户输入一个整型值,并使用运算符<<将其插入到一个stringstream对象中,如第12行所示;然后,您使用提取运算符将这个整数转换为string,如第14行所示。接下来,您将存储在strInput中的字符串转换为整数,并将其存储到Copy中。
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的答案。在继续学习下一章前,请务必弄懂这些答案。
1.在只需写入文件的情况下,应使用哪种流?
2.如何使用cin从输入流中获取一整行?
3.在需要将std::string对象写入文件时,应使用ios_base::binary模式吗?
4.使用open()打开流后,为何还要使用is_open()进行检查?
1.查错:找出下述代码中的错误:
2.查错:找出下述代码中的错误: