- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
6.8 简单文件输入/输出
有时候,通过键盘输入并非最好的选择。例如,假设您编写了一个股票分析程序,并下载了一个文件,其中包含 1000 种股票的价格。在这种情况下,让程序直接读取文件,而不是手工输入文件中所有的值,将方便得多。同样,让程序将输出写入到文件将更为方便,这样可得到有关结果的永久性记录。
幸运的是,C++使得将读取键盘输入和在屏幕上显示输出(统称为控制台输入/输出)的技巧用于文件输入/输出(文件 I/O)非常简单。第 17 章将更详细地讨论这些主题,这里只介绍简单的文本文件 I/O。
6.8.1 文本 I/O 和文本文件
这里再介绍一下文本 I/O 的概念。使用 cin 进行输入时,程序将输入视为一系列的字节,其中每个字节都被解释为字符编码。不管目标数据类型是什么,输入一开始都是字符数据——文本数据。然后,cin 对象负责将文本转换为其他类型。为说明这是如何完成的,来看一些处理同一个输入行的代码。
假设有如下示例输入行:
来看一下使用不同数据类型的变量来存储时,cin 是如何处理该输入行的。首先,来看使用 char 数据类型的情况:
输入行中的第一个字符被赋给 ch。在这里,第一个字符是数字 3,其字符编码(二进制)被存储在变量 ch 中。输入和目标变量都是字符,因此不需要进行转换。注意,这里存储的数值 3,而是字符 3 的编码。执行上述输入语句后,输入队列中的下一个字符为字符 8,下一个输入操作将对其进行处理。
接下来看看 int 类型:
在这种情况下,cin 将不断读取,直到遇到非数字字符。也就是说,它将读取 3 和 8,这样句点将成为输入队列中的下一个字符。cin 通过计算发现,这两个字符对应数值 38,因此将 38 的二进制编码复制到变量 n 中。
接下来看看 double 类型:
在这种情况下,cin 将不断读取,直到遇到第一个不属于浮点数的字符。也就是说,cin 读取 3、8、句点和 5,使得空格成为输入队列中的下一个字符。cin 通过计算发现,这四个字符对应于数值 38.5,因此将 38.5 的二进制编码(浮点格式)复制到变量 x 中。
接下来看看 char 数组的情况:
在这种情况下,cin 将不断读取,直到遇到空白字符。也就是说,它读取 3、8、句点和 5,使得空格成为输入队列中的下一个字符。然后,cin 将这 4 个字符的字符编码存储到数组 word 中,并在末尾加上一个空字符。这里不需要进行任何转换。
最后,来看一下另一种使用 char 数组来存储输入的情况:
在这种情况下,cin 将不断读取,直到遇到换行符(示例输入行少于 50 个字符)。所有字符都将被存储到数组 word 中,并在末尾加上一个空字符。换行符被丢弃,输入队列中的下一个字符是下一行中的第一个字符。这里不需要进行任何转换。
对于输入,将执行相反的转换。即整数被转换为数字字符序列,浮点数被转换为数字字符和其他字符组成的字符序列(如 284.53 或−1.58E+06)。字符数据不需要做任何转换。
这里的要点是,输入一开始为文本。因此,控制台输入的文件版本是文本文件,即每个字节都存储了一个字符编码的文件。并非所有的文件都是文本文件,例如,数据库和电子表格以数值格式(即二进制整数或浮点格式)来存储数值数据。另外,字处理文件中可能包含文本信息,但也可能包含用于描述格式、字体、打印机等的非文本数据。
本章讨论的文件 I/O 相当于控制台 I/O,因此仅适用于文本文件。要创建文本文件,用于提供输入,可使用文本编译器,如 DOS 中的 EDIT、Windows 中的“记事本”和 UNIX/Linux 系统中的 vi 或 emacs。也可以使用字处理程序来创建,但必须将文件保存为文本格式。IDE 中的源代码编辑器生成的也是文本文件,事实上,源代码文件就属于文本文件。同样,可以使用文本编辑器来查看通过文本输出创建的文件。
6.8.2 写入到文本文件中
对于文件输入,C++使用类似于 cout 的东西。下面来复习一些有关将 cout 用于控制台输出的基本事实,为文件输出做准备。
- 必须包含头文件 iostream。
- 头文件 iostream 定义了一个用处理输出的 ostream 类。
- 头文件 iostream 声明了一个名为 cout 的 ostream 变量(对象)。
- 必须指明名称空间 std;例如,为引用元素 cout 和 endl,必须使用编译指令 using 或前缀 std::。
- 可以结合使用 cout 和运算符<<来显示各种类型的数据。
文件输出与此极其相似。
- 必须包含头文件 fstream。
- 头文件 fstream 定义了一个用于处理输出的 ofstream 类。
- 需要声明一个或多个 ofstream 变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则。
- 必须指明名称空间 std;例如,为引用元素 ofstream,必须使用编译指令 using 或前缀 std::。
- 需要将 ofstream 对象与文件关联起来。为此,方法之一是使用 open( ) 方法。
- 使用完文件后,应使用方法 close( ) 将其关闭。
- 可结合使用 ofstream 对象和运算符<<来输出各种类型的数据。
注意,虽然头文件 iostream 提供了一个预先定义好的名为 cout 的 ostream 对象,但您必须声明自己的 ofstream 对象,为其命名,并将其同文件关联起来。下面演示了如何声明这种对象:
下面演示了如何将这种对象与特定的文件关联起来:
注意,方法 open( ) 接受一个 C-风格字符串作为参数,这可以是一个字面字符串,也可以是存储在数组中的字符串。
下面演示了如何使用这种对象:
重要的是,声明一个 ofstream 对象并将其同文件关联起来后,便可以像使用 cout 那样使用它。所有可用于 cout 的操作和方法(如<<、endl 和 setf( ))都可用于 ofstream 对象(如前述示例中的 outFile 和 fout)。
总之,使用文件输出的主要步骤如下。
1.包含头文件 fstream。
2.创建一个 ofstream 对象。
3.将该 ofstream 对象同一个文件关联起来。
4.就像使用 cout 那样使用该 ofstream 对象。
程序清单 6.15 中的程序演示了这种方法。它要求用户输入信息,然后将信息显示到屏幕上,再将这些信息写入到文件中。读者可以使用文本编辑器来查看该输出文件的内容。
程序清单 6.15 outfile.cpp
该程序的最后一部分与 cout 部分相同,只是将 cout 替换为 outFile 而已。下面是该程序的运行情况:
屏幕输出是使用 cout 的结果。如果您查看该程序的可执行文件所在的目录,将看到一个名为 carinfo.txt 的新文件(根据编译器的配置,该文件也可能位于其他文件夹),其中包含使用 outFile 生成的输出。如果使用文本编辑器打开该文件,将发现其内容如下:
正如读者看到的,outFile 将 cout 显示到屏幕上的内容写入到了文件 carinfo.txt 中。
程序说明
在程序清单 6.15 的程序中,声明一个 ofstream 对象后,便可以使用方法 open( ) 将该对象特定文件关联起来:
程序使用完该文件后,应该将其关闭:
注意,方法 close( ) 不需要使用文件名作为参数,这是因为 outFile 已经同特定的文件关联起来。如果您忘记关闭文件,程序正常终止时将自动关闭它。
outFile 可使用 cout 可使用的任何方法。它不但能够使用运算符<<,还可以使用各种格式化方法,如 setf( ) 和 precision( )。这些方法只影响调用它们的对象。例如,对于不同的对象,可以提供不同的值:
读者需要记住的重点是,创建好 ofstream 对象(如 outFile)后,便可以像使用 cout 那样使用它。
回到 open( ) 方法:
在这里,该程序运行之前,文件 carinfo.txt 并不存在。在这种情况下,方法 open( ) 将新建一个名为 carinfo.txt 的文件。如果在此运行该程序,文件 carinfo.txt 将存在,此时情况将如何呢?默认情况下,open( ) 将首先截断该文件,即将其长度截短到零——丢其原有的内容,然后将新的输出加入到该文件中。第 17 章将介绍如何修改这种默认行为。
警告:
打开已有的文件,以接受输出时,默认将它其长度截短为零,因此原来的内容将丢失。
打开文件用于接受输入时可能失败。例如,指定的文件可能已经存在,但禁止对其进行访问。因此细心的程序员将检查打开文件的操作是否成功,这将在下一个例子中介绍。
6.8.3 读取文本文件
接下来介绍文本文件输入,它是基于控制台输入的。控制台输入涉及多个方面,下面首先总结这些方面。
- 必须包含头文件 iostream。
- 头文件 iostream 定义了一个用处理输入的 istream 类。
- 头文件 iostream 声明了一个名为 cin 的 istream 变量(对象)。
- 必须指明名称空间 std;例如,为引用元素 cin,必须使用编译指令 using 或前缀 std::。
- 可以结合使用 cin 和运算符>>来读取各种类型的数据。
- 可以使用 cin 和 get( ) 方法来读取一个字符,使用 cin 和 getline( ) 来读取一行字符。
- 可以结合使用 cin 和 eof( )、fail( ) 方法来判断输入是否成功。
- 对象 cin 本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值 true,否则被转换为 false。
文件输出与此极其相似:
- 必须包含头文件 fstream。
- 头文件 fstream 定义了一个用于处理输入的 ifstream 类。
- 需要声明一个或多个 ifstream 变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则。
- 必须指明名称空间 std;例如,为引用元素 ifstream,必须使用编译指令 using 或前缀 std::。
- 需要将 ifstream 对象与文件关联起来。为此,方法之一是使用 open( ) 方法。
- 使用完文件后,应使用 close( ) 方法将其关闭。
- 可结合使用 ifstream 对象和运算符>>来读取各种类型的数据。
- 可以使用 ifstream 对象和 get( ) 方法来读取一个字符,使用 ifstream 对象和 getline( ) 来读取一行字符。
- 可以结合使用 ifstream 和 eof( )、fail( ) 等方法来判断输入是否成功。
- ifstream 对象本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值 true,否则被转换为 false。
注意,虽然头文件 iostream 提供了一个预先定义好的名为 cin 的 istream 对象,但您必须声明自己的 ifstream 对象,为其命名,并将其同文件关联起来。下面演示了如何声明这种对象:
下面演示了如何将这种对象与特定的文件关联起来:
注意,方法 open( ) 接受一个 C-风格字符串作为参数,这可以是一个字面字符串,也可以是存储在数组中的字符串。
下面演示了如何使用这种对象:
重要的是,声明一个 ifstream 对象并将其同文件关联起来后,便可以像使用 cin 那样使用它。所有可用于 cin 的操作和方法都可用于 ifstream 对象(如前述示例中的 inFile 和 fin)。
如果试图打开一个不存在的文件用于输入,情况将如何呢?这种错误将导致后面使用 ifstream 对象进行输入时失败。检查文件是否被成功打开的首先方法是使用方法 is_open( ),为此,可以使用类似于下面的代码:
如果文件被成功地打开,方法 is_open( ) 将返回 true;因此如果文件没有被打开,表达式!inFile.isopen( ) 将为 true。函数 exit( ) 的原型是在头文件 cstdlib 中定义的,在该头文件中,还定义了一个用于同操作系统通信的参数值 EXIT_FAILURE。函数 exit( ) 终止程序。
方法 is_open( ) 是 C++中相对较新的内容。如果读者的编译器不支持它,可使用较老的方法 good( ) 来代替。正如第 17 章将讨论的,方法 good( ) 在检查可能存在的问题方面,没有 is_open( ) 那么广泛。
程序清单 6.16 中的程序打开用户指定的文件,读取其中的数字,然后指出文件中包含多少个值以及它们的和与平均值。正确地设计输入循环至关重要,详细请参阅后面的“程序说明”。注意,通过使用了 if 语句,该程序受益匪浅。
程序清单 6.16 sumafile.cpp
要运行程序清单 6.16 中的程序,首先必须创建一个包含数字的文本文件。为此,可以使用文本编辑器(如用于编写源代码的文本编辑器)。假设该文件名为 scores.txt,包含的内容如下:
程序还必须能够找到这个文件。通常,除非在输入的文件名中包含路径,否则程序将在可执行文件所属的文件夹中查找。
警告:
Windows 文本文件的每行都以回车字符和换行符结尾;通常情况下,C++在读取文件时将这两个字符转换为换行符,并在写入文件时执行相反的转换。有些文本编辑器(如 Metrowerks CodeWarrior IDE 编辑器),不会自动在最后一行末尾加上换行符。因此,如果读者使用的是这种编辑器,请在输入最后的文本后按下回车键,然后再保存文件。
下面是该程序的运行情况:
程序说明
该程序没有使用硬编码文件名,而是将用户提供的文件名存储到字符数组 filename 中,然后将该数组用作 open( ) 的参数:
正如本章前面讨论的,检查文件是否被成功打开至关重要。下面是一些可能出问题的地方:指定的文件可能不存在;文件可能位于另一个目录(文件夹)中;访问可能被拒绝;用户可能输错了文件名或省略了文件扩展名。很多初学者花了大量的时间检查文件读取循环的哪里出了问题后,最终却发现问题在于程序没有打开文件。检查文件是否被成功打开可避免将这种将精力放在错误地方的情况发生。
读者需要特别注意的是文件读取循环的正确设计。读取文件时,有几点需要检查。首先,程序读取文件时不应超过 EOF。如果最后一次读取数据时遇到 EOF,方法 eof( ) 将返回 true。其次,程序可能遇到类型不匹配的情况。例如,程序清单 6.16 期望文件中只包含数字。如果最后一次读取操作中发生了类型不匹配的情况,方法 fail( ) 将返回 true(如果遇到了 EOF,该方法也将返回 true)。最后,可能出现意外的问题,如文件受损或硬件故障。如果最后一次读取文件时发生了这样的问题,方法 bad( ) 将返回 true。不要分别检查这些情况,一种更简单的方法是使用 good( ) 方法,该方法在没有发生任何错误时返回 true:
然后,如果愿意,可以使用其他方法来确定循环终止的真正原因:
这些代码紧跟在循环的后面,用于判断循环为何终止。由于 eof( ) 只能判断是否到达 EOF,而 fail( ) 可用于检查 EOF 和类型不匹配,因此上述代码首先判断是否到达 EOF。这样,如果执行到了 else if 测试,便可排除 EOF,因此,如果 fail( ) 返回 true,便可断定导致循环终止的原因是类型不匹配。
方法 good( ) 指出最后一次读取输入的操作是否成功,这一点至关重要。这意味着应该在执行读取输入的操作后,立刻应用这种测试。为此,一种标准方法是,在循环之前(首次执行循环测试前)放置一条输入语句,并在循环的末尾(下次执行循环测试之前)放置另一条输入语句:
鉴于以下事实,可以对上述代码进行精简:表达式 inFile >> value 的结果为 inFile,而在需要一个 bool 值的情况下,inFile 的结果为 inFile.good( ),即 true 或 false。
因此,可以将两条输入语句用一条用作循环测试的输入语句代替。也就是说,可以将上述循环结构替换为如下循环结构:
这种设计仍然遵循了在测试之前进行读取的规则,因为要计算表达式 inFile >> value 的值,程序必须首先试图将一个数字读取到 value 中。
至此,读者对文件 I/O 有了初步的认识。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论