📄 14章 文件处理.txt
字号:
C++大学教程 第14章 文件处理
教学目标
●能够建立、读写和更新文件
●熟悉顺序访问文件的处理方式
●熟悉随机访问文件的处理方式
●指定高性能无格式的I/O操作
●了解格式化与“原始数据”文件处理的差别
●用随机访问文件处理建立事务处理程序
14.1 简介
存储在变量和数组中的数据是临时的,这些数据在程序运行结束后都会消失。文件用来永久地保存大量的数据。计算机把文件存储在二级存储设备中(特别是磁盘存储设备)。本章要讨论怎样用C++程序建立、更新和处理数据文件(包括顺序存储文件和随机访问文件)。我们要比较格式化与“原始数据”文件处理。第19章将介绍从string而不是从文件输入和输出数据。
14. 2 数据的层次
计算机处理的所有数据项最终都是0和1的组合。采用这种组合方式是因为它非常简单,并且能够经济地制造表示两种稳定状态的电子设备(一种状态代表1,另一种状态代表0)。计算机所完成的复杂功能仅仅涉及最基本的对0和1的操作。
0和1可以认为是计算机中的最小数据项,人们称之为“位”(bit)。bit是binary digit(二进制数字)的缩写,一个二进制数字是0和1的两个值之一。计算机电路完成各种简单的位操作,如确定某个位的值、设置某个位的值和反转某个位的值(0变为1,1变为0)等等。
程序员如果以底层位的形式处理数据会感到很麻烦,所以更喜欢用十进制数字(即0、1、2、3、4、5、6、7、8和9)、字母(即A~Z和a—z)和专门的符号(即$、@、%、&、*、(、)、-、+、”、:、?、/等等)处理数据。数字、字母和专门的符号称为“字符”(character)。能够在特定计算机上用来编写程序和代表数据项的所有字符的集合称为“字符集”(character set)。因为计算机只能处理1和0,所以计算机字符集中的每一个字符都是用称为“字节”(byte)的0、1序列表示的。目前最常见的是用8位构成一个字节。程序员以字符为单位建立程序和数据项,计算机按位模式操作
和处理这些字符。
就像字符是由位构成的,域(field)是由字符构成的。一个域就是一组有意义的字符。例如,一个仅仅包含大写字母和小写字母的域可用来表示某人的名字。
计算机处理的数据项构成了“数据的层次”(data hierarchy)。在这个结构中,数据项从位到字符再到域是越来越大、越来越复杂。
记录(即C语言中的结构)是由多个域构成(在C++中称为成员)。例如,在一张工资表中,为某个特定雇员建立的一条记录可能是由如下域组成的:
1. 雇员标识号
2. 名字
3.地址
4.每小时工资等级
5. 免税申请号
6. 年度收入
7. 联邦税收额等等
因此,一条记录是一组相关的域。在上面的例于中,每一个域都针对同一个雇员。当然,特定的公司会有许多雇员,所以要为每一个雇员建立一个工资表(记录)。一个文件就是一组相关的记录。某个公司的工资表文件通常包含为每一个雇员建立的记录。较小公司的工资表文件可能只包含22条记录,而大型公司的工资表文件可能要包含100 000条记录。一个机构建立成百上千个文件,而每一个文件又包含几百万甚至几十亿个字符信息,这是不值得奇怪的。图14.1反映了数据的层次。
为了检索文件中指定的记录,每个记录中至少要选出一个域作为“记录关键字”(record key)。记录关键字标识了属于某人或某个实体的记录。例如,在本节的工资表记录中,“雇员标识号”通常选作记录关键字。
文件中的记录有多种组织方式。最常见的组织方式是按记录关键字字段的顺序存储记录,接这种方式存储记录的文件称为“顺序文件”(sequential file)。在工资表文件中.记录通常按雇员标识号的顺序存储。在第一个雇员的记录中,该雇员的雇员标识号最小,其后的记录中包含的雇员标识号依次递增。
多数商业机构要用许多文件来存储数据。例如,公司里可能要有工资表文件、应收账目文件(列出客户的欠款)、应付账目文件(列出欠供应商的金额)、存货文件(列出经商的货物)和其他多种类型的文件。有时把一组相关的文件称为“数据库”(database)。为建立和管理数据库而设计的文件集合称为“数据库管理系统”(DBMS)。
14.3 文件和流
C++语言把每一个文件都看成一个有序的字节流(见图14.2),每一个文件或者以文件结束符(end-of-file marker)结束,或者在特定的字节号处结束(结束文件的特定的字节号记录在由系统维护和管理的数据结构中)。当打开一个文件时,该文件就和某个流关联起来。第11章曾介绍过cin、cout、cerr和clog这4个对象会自动生成。与这些对象相关联的流提供程序与特定文件或设备之间的通信通道。例如.cin对象(标准输入流对象)使程序能从键盘输入数据,cout对象(标准输出流对象)使程序能向屏幕输出数据,cerr和clog对象(标准错误流对象)使程序能向屏幕输出错误消息。
图14.2 C++把文件看成n个字节
要在C++中进行文件处理,就要包括头文件<iostream.h>和<fstream.h>。<fstream.h>头文件包括流类ifstream(从文件输入)、ofstream(向文件输出)和fstream(从文件输入,输出)的定义。生成这些流类的对象即可打开文件。这些流类分别从istream、ostream和iostream类派生(即继承它们的功能)。这样,第11章“C++输入,输出流”中介绍的成员函数、运算符和流操纵算子也可用于文件流。I/O类的继承关系见图14.3。
14.4 建立顺序访问文件
因为C++把文件看着是无结构的字节流,所以记录等等的说法在C++文件中是不存在的。为此,程序员必须提供满足特定应用程序要求的文件结构。下例说明了程序员是怎样给文件强加一个记录结构。先列出程序,然后再分析细节。
图14.4中的程序建立了一个简单的顺序访问文件,该文件可用在应收账目管理系统中跟踪公司借贷客户的欠款数目。程序能够获取每一个客户的账号、客户名和对客户的结算额。一个客户的数据就构成了该客户的记录。账号在应用程序中用作记录关键字,文件按账号顺序建立和维护。范例程序假定用户是按账号顺序键人记录的(为了让用户按任意顺序键入记录,完善的应收账目管理系统应该具备排序能力)。然后把键入的记录保存并写入文件。
1 // Fig. 14.4; fig14_04.cpp
2 // Create a sequential file
3 #include <iostream.h>
# #include <fstream.h>
5 #include <stdlib.h>
6
7 int main()
8{
9 // ofstream constructor opens file
10 ofstream outClientFile( "clients.dat", ios::out );
11
12 if ( !outClientFile ) { // overloaded ! operator
13 cerr << "File could not be opened" << endl;
14 exit( 1 ); // prototype in stdlib.h
15 }
16
17 cout << "Enter the account, name, and balance.\n"
18 << "Enter end-of-file to end input.\n? ";
19
20 int account;
21 char name[ 30 ];
22 float balance;
23
24 while (cin >> account >> name >> balance ) {
25 outClientFile << account << ' ' << name
26 << ' ' << balance << '\n';
27 cout << "? ";
28 }
29
30 return 0; // ofstream destructor closes file
31 }
输出结果:
Enter the account, name, and balance.
Enter end-of-file to end input.
? 100 Jones 24.98
? 200 Doe 345.67
? 300 White 0
? 400 Stone -42.16
? 500 Rich 224.62
? ^z
图 14.4 建立顺序文件
现在我们来研究这个程序。前面曾介绍过,文件通过建立ifstream、ofstream或fstream流类的对象而打开。图14.4中,要打开文件以便输出,因此生成ofstream对象。向对象构造函数传入两个参数——文件名和文件打开方式。对于ostream对象,文件打开方式可以是ios::out(将数据输出到文件)或ios::app(将数据添加到文件末尾,而不修改文件中现有的数据)。现有文件用ios::out打开时会截尾,即文件中的所有数据均删除。如果指定文件还不存在,则用该文件名生成这个文件。下列声明(第10行):
ofstream outClientFile(“clients.dat”.ios::out);
生成ofstream对象outClientfile,与打开输出的文件clients.dat相关联。参数"clients.dat"和ios::out传入ofstream构造函数,该函数打开文件,从而建立与文件的通信线路。默认情况下,打开ofstream对象以便输出,因此下列语句:
ofstream outClientFile (”clients.dat”);
也可以打开clients.dat进行输出。图14.5列出了文件打开方式。
常见编程错误14.1
打开一个用户想保留数据的现有文件进行输出(ios::out方式)。这种操作会删除文件的内容而不会给予警告。
常见编程错误14. 2
用错误的ofstream对象指明一个文件。
也可以生成ofstream对象而不打开特定文件,可以在后面再将文件与对象相连接。例如,下列声明:
ofstream outClientFile;
生成以ofstram对象outClientFile。ofstream成员函数open打开文件并将其与现有ofstream对象相连接,如下所示:
outClientFile open(”clients.dat”,ios::out);
------------------------------------------------------------------------------------------
文件打开方式 说明
------------------------------------------------------------------------------------------
ios::app 将所有输出写入文件末尾
ios::ate 打开文件以便输出,井移到文件末尾(通常用于添加数据)数据可以写入
文件中的任何地方
ios::in 打开文件以便输入
ios::out 打开文件以便输出
ios::trunc 删除文件现有内容(是ios::out的默认操作)
ios::nocreate 如果文件不存在,则文件打开失败
ios::noreplace 如果文件存在,则文件打开失败
------------------------------------------------------------------------------------------
图14. 5 文件打开方式
常见编程错误 14. 3
在引用文件之前忘记打开该文件。
生成ofstream对象并准备打开时,程序测试打开操作是否成功。下列if结构中的操作(第12行到第15行):
if ( !outClientFile ) {
cerr << "File could not be opened" << endl;
exit(1);
}
用重载的ios运算符成员函数operator!确定打开操作是否成功。如果open操作的流将failbit或badbit设置,则这个条件返回非0值(true)。可能的错误是试图打开读取不存在的文件、试图打开读取没有权限的文件或试图打开文件以便写人而磁盘空间不足。
如果条件表示打开操作不成功.则输出错误消息“File could not be opened",并调用函数exit结束程序,exit的参数返回到调用该程序的环境中,参数0表示程序正常终止.任何其他值表示程序因某个错误而终止。exit返回的值让调用环境(通常是操作系统)对错误做出相应的响应。
另一个重载的ios运算符成员函数operator void*将流变成指针,使其测试为0(空指针)或非0(任何其他指针值)。如果failbit或badbit(见第11章)对流进行设置,则返回0(false)。下列while首部的条件自动调用operator void*成员函数:
while (cin >> account >> name >> balance )
只要cin的failbit和badbit都没有设置,则条件保持true。输入文件结束符设置cin的failbit。operator void*函数可以测试输入对象的文件结束符,而不必对输入对象显式调用eof成员函数。
如果文件打开成功,则程序开始处理数据。下列语句(第17行和第18行)提示用户对每个记录输入不同域,或在数据输入完成时输入文件结束符:
cout << "Enter the account, name, and balance.\n"
<< "Enter EOF to and input.\n? ";
图14. 6列出了不同计算机系统中文件结束符的键盘组合。
---------------------------------------------------
计算机系统 组合键
---------------------------------------------------
UNIX系统 <ctrl>d
IBM PC及其兼容机 <ctrl>z
Macintosh <ctrl>d
VAX(VMS) <ctrl>z
---------------------------------------------------
图14.6各种流行的计算机系统中的文件结束组合键
下列语句(第24行):
while (cin >> account >> name >> balance )
输入每组数据并确定是否输人了文件结束符。输入文件结束符或不合法数据时,cin的流读取运算符>>返回0(通常这个流读取运算符>>返回cin),while结构终止。用户输入文件结束符告诉程序没有更多要处理的数据。当用户输入文件结束符组合键时,设置文件结束符。只要没有输入文件结束符,while结构就一直循环。
第25行和第26行:
outClientFile << account << ' ' << name
<< ' ' << balance << '\n';
用流插人运算符<<和程序开头与文件相关联的outClientFile对象将一组数据写入文件”clients.dat"。
可以用读取文件的程序取得这些数据(见14.5节)。注意图14.4中生成的文件是文本文件,可以用任何文本编辑器读取。
输人文件结束符后,main终止,使得outClientFile对象删除,从而调用其析构函数,关闭文件
clients.dat。程序员可以用成员函数close显式关闭ofstream对象,如下所示:
outClientFile.close();
性能提示14. 1
程序不再引用的文件应立即显式关闭.这样可以减少程序继续执行时占用的资源。这种方法还可以提高程序的清晰性。
在图14.4的执行范例中,用户输人了五条记录.然后键入了表示数据输入结束的文件结束符(IBM PC兼容机的屏幕上显示^z)。输出结果的对话窗口中没有说明这些记录究竟是怎样在文件中组织。为了验证文件的建立是成功的,下一节介绍了读取和打印该文件的程序。
14.5 读取顺序访问文件中的数据
为了在需要的时候能够检索要处理的数据,数据要存储在文件中。上一节演示丁怎样建立一个顺序访问的文件。这一节要讨论按顺序读取文件中的数据。
图14.7中的程序读取文件"clients.dat"(图14.4中的程序建立)中的记录,并打印出了记录的内容。通过建立ifstream类对象打开文件以便输入。向对象传入的两个参数是文件名和文件打开方式。下列声明:
ifstream inClientFile( "clients.dat", ios::in );
生成ifstream对象inClientFile,并将其与打开以便输入的文件clients.dat相关联。括号中的参数传入ifstream构造函数,打开文件并建立与文件的通信线路。
打开ifstream类对象默认为进行输入,因此下列语句:
ifstream inClientFile( "Clients.dat" );
可以打开clients.dat以便输入。和ofstream对象一样,ifstream对象也可以生成而不打开特定文件,然后再将对象与文件相连接。
编程技巧14. 1
如果文件内容不能修改,那么只能打开文件以便输入(用ios::in),避免不小心改动文件。这是最低权限原则的又一个例子。
程序用!inClientFile条件确定文件是否打开成功,然后再从文件中读取数据。下列语句:
while (inClientFile >> account >> name >> balance )
从文件中读取一组值(即记录)。第一次执行完该条语句后,account的值为100,name的值为"John",balance的值为24.98。每次执行程序中的该条语句时,函数都读取文件中的另一条记录,并把新的值赋给account、name和balance。记录用函数outputLine显示,该函数用参数化流操纵算子将数据格式化之后再显示。到达文件末尾时,while结构中的输入序列返回0(通常返回inClientFile流),ifstream析构函数将文件关闭,程序终止。
1 // Fig. 14.7: fig14_O7.cpp
2 // Reading and printing a sequential file
3 #include <iostreamoh>
4 #include <fstream.h>
5 #include <iomanip.h>
6 #include <stdlib.h>
7
8 void outputLine( int, const char *, double );
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -