第四章 文件处理【信息学奥赛】

4.1  简介 

        存储在变量和数组中的数据是临时的,这些数据在程序运行结束后都会消失。文件用来永久地保存大量的数据。计算机把文件存储在二级存储设备中(特别是磁盘存储设备)。本章要讨论怎样用C++程序建立、更新和处理数据文件(包括顺序存储文件和随机访问文件)。我们要比较格式化与“原始数据”文件处理。后面将介绍从string而不是从文件输入和输出数据。

4.2  文件和流

        C++语言把每一个文件都看成一个有序的字节流,每一个文件或者以文件结束符(end-of-file marker)结束,或者在特定的字节号处结束(结束文件的特定的字节号记录在由系统维护和管理的数据结构中)。当打开一个文件时,该文件就和某个流关联起来。第11章曾介绍过cin、cout、cerr和clog这4个对象会自动生成。与这些对象相关联的流提供程序与特定文件或设备之间的通信通道。例如.cin对象(标准输入流对象)使程序能从键盘输入数据,cout对象(标准输出流对象)使程序能向屏幕输出数据,cerr和clog对象(标准错误流对象)使程序能向屏幕输出错误消息。

        要在C++中进行文件处理,就要包括头文件<iostream.h>和<fstream.h>。<fstream.h>头文件包括流类ifstream(从文件输入)、ofstream(向文件输出)和fstream(从文件输入,输出)的定义。生成这些流类的对象即可打开文件。这些流类分别从istream、ostream和iostream类派生(即继承它们的功能)。这样,第2章“C++输入,输出流”中介绍的成员函数、运算符和流操纵算子也可用于文件流。

4.3  建立并写入文件

        因为C++把文件看着是无结构的字节流,所以记录等等的说法在C++文件中是不存在的。为此,程序员必须提供满足特定应用程序要求的文件结构。下例说明了程序员是怎样给文件强加一个记录结构。先列出程序,然后再分析细节。

        图4.4中的程序建立了一个简单的访问文件,该文件可用在应收账目管理系统中跟踪公司借贷客户的欠款数目。程序能够获取每一个客户的账号、客户名和对客户的结算额。一个客户的数据就构成了该客户的记录。账号在应用程序中用作记录关键字,文件按账号顺序建立和维护。范例程序假定用户是按账号顺序键人记录的(为了让用户按任意顺序键入记录,完善的应收账目管理系统应该具备排序能力)。然后把键入的记录保存并写入文件。

// 建立文件
#include <iostream.h>

#include <fstream.h>

#include <stdlib.h>

using namespace std;

int main() {
	// ofstream构造函数打开文件
	ofstream fout( "clients.dat");

	if ( !fout ) { // 是否能打开文件

		cerr << "无法打开文件" << endl;

		exit( 1 ); // prototype in stdlib.h

	}
	cout << "输入帐户、名称和余额.\n"
	     << "输入文件后缀以结束输入。\n? ";

	int account;

	char name[ 30 ];

	float balance;

	while (cin >> account >> name >> balance ) {

		fout << account << ' ' << name << ' ' << balance << '\n';

		cout << "? ";

	}

	return 0;	// ofstream析构函数关闭文件
}

输出结果:

输入帐户、名称和余额.

输入文件后缀以结束输入。

? 100 张三 24.98

? 200 李四 345.67

? 300 王五 0

? 400 赵六 -42.16

? 500 钱七 224.62

? ^z

图 4.4 建立文件

        现在我们来研究这个程序。前面曾介绍过,文件通过建立ifstream、ofstream或fstream流类的对象而打开。图4.4中,要打开文件以便输出,因此生成ofstream对象。向对象构造函数传入两个参数——文件名和文件打开方式。对于ostream对象,文件打开方式可以是ios::out(将数据输出到文件)或ios::app(将数据添加到文件末尾,而不修改文件中现有的数据)。现有文件用ios::out打开时会截尾,即文件中的所有数据均删除。如果指定文件还不存在,则用该文件名生成这个文件。下列声明(第10行):

        ofstream fout (“clients.dat”);

生成ofstream对象fout,与打开输出的文件clients.dat相关联。参数"clients.dat"和ios::out传入ofstream构造函数,该函数打开文件,从而建立与文件的通信线路。默认情况下,打开ofstream对象以便输出,因此下列语句:

        ofstream fout (”clients.dat”);

也可以打开clients.dat进行输出。图4.5列出了文件打开方式。

也可以生成ofstream对象而不打开特定文件,可以在后面再将文件与对象相连接。例如,下列声明:

        ofstream fout;

生成以ofstram对象fout。ofstream成员函数open打开文件并将其与现有ofstream对象相连接,如下所示:

        fout open(“clients.dat”);

------------------------------------------------------------------------------------------

  文件打开方式            说明

------------------------------------------------------------------------------------------

  ios::app              将所有输出写入文件末尾

  ios::ate               打开文件以便输出,井移到文件末尾(通常用于添加数据)数据可以写入

                            文件中的任何地方

  ios::in                 打开文件以便输入

  ios::out               打开文件以便输出

  ios::trunc            删除文件现有内容(是ios::out的默认操作)

  ios::nocreate      如果文件不存在,则文件打开失败

  ios::noreplace    如果文件存在,则文件打开失败

------------------------------------------------------------------------------------------

图4. 5 文件打开方式

生成ofstream对象并准备打开时,程序测试打开操作是否成功。下列if结构中的操作(第12行到第15行):

    if ( ! fout ) {

        cerr << "无法打开文件" << 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 << "输入帐户、名称和余额.\n"

                << "输入文件后缀以结束输入。\n? ";

图4. 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行:

    fout<< account << ' ' << name<< ' ' << balance << '\n';

用流插人运算符<<和程序开头与文件相关联的fout对象将一组数据写入文件”clients.dat"。

可以用读取文件的程序取得这些数据(见4.5节)。注意图4.4中生成的文件是文本文件,可以用任何文本编辑器读取。

输人文件结束符后,main终止,使得fout对象删除,从而调用其析构函数,关闭文件

clients.dat。程序员可以用成员函数close显式关闭ofstream对象,如下所示:

    fout.close();

4.4  读取文件中的数据

为了在需要的时候能够检索要处理的数据,数据要存储在文件中。上一节演示丁怎样建立一个顺序访问的文件。这一节要讨论按顺序读取文件中的数据。

图4.7中的程序读取文件"clients.dat"(图4.4中的程序建立)中的记录,并打印出了记录的内容。通过建立ifstream类对象打开文件以便输入。向对象传入的两个参数是文件名和文件打开方式。下列声明:

        ifstream  fin ( "clients.dat");

生成ifstream对象fin,并将其与打开以便输入的文件clients.dat相关联。括号中的参数传入ifstream构造函数,打开文件并建立与文件的通信线路。

打开ifstream类对象默认为进行输入,因此下列语句:

        ifstream fin ( "Clients.dat" );

可以打开clients.dat以便输入。和ofstream对象一样,ifstream对象也可以生成而不打开特定文件,然后再将对象与文件相连接。

程序用fin条件确定文件是否打开成功,然后再从文件中读取数据。下列语句:

        while (fin >> account >> name >> balance )

从文件中读取一组值(即记录)。第一次执行完该条语句后,account的值为100,name的值为"John",balance的值为24.98。每次执行程序中的该条语句时,函数都读取文件中的另一条记录,并把新的值赋给account、name和balance。记录用函数outputLine显示,该函数用参数化流操纵算子将数据格式化之后再显示。到达文件末尾时,while结构中的输入序列返回0(通常返回fin流),ifstream析构函数将文件关闭,程序终止。

// Reading and printing a sequential file
#include <iostreamoh>

#include <fstream.h>

#include <iomanip.h>

#include <stdlib.h>

void outputLine( int, const char *, double );

int main() {
	// ifstream构造函数打开文件
	ifstream fin ( "clients.dat" );

	if { ! fin ) {

		cerr << "无法打开文件\n";

		exit( 1 );

	}

	int account;

	char name[ 30 ] ;

	double balance;

	cout << setiosflags( ios::left ) << setw( 10 ) << "账户"

	     << setw( 13 ) << "名称" << "余额\n";

	while ( fin >> account >> name >> balance )

		outputLine( account, name, balance );

	return 0;// ifstream destructor closes the file

	}

	void outputLine( int acct, const char *name, double bal )

	{

		cout << setiosflags( ios::left ) << setw( 10 ) << acct

		     << setw( 13 ) << name << setw( 7 ) << setprecision( 2 )

		     << resetiosflags( ios::left )

		     << setiosflags( ios::fixed | ios::showpoint )

		     << bal << '\n';

	}
	return 0;
}

输出结果:

账户       名称         余额

i00        Jones        24.98

200        Doe         345.67

300        White         0.00

400        Stone       -42.16

500        Rich        224.62

图4.7 读取并打印一个顺序文件

        为了按顺序检索文件中的数据,程序通常要从文件的起始位置开始读取数据,然后连续地读取所有的数据,直到找到所需要的数据为止。程序执行中可能需要按顺序从文件开始位置处理文件中的数据好几次。istreatrl类和ostream类都提供成员函数,使程序把“文件位置指针”(file position pointer,指示读写操作所在的下一个字节号)重新定位。这些成员函数是istream类的seekg(“seekget”)和ostream类的seekp(“seek put”)。每个istream对象有个get指针,表示文件中下一个输入

相距的字节数,每个ostream对象有一个put指针,表示文件中下一个输出相距的字节数。下列语句:

    fin.seekg( 0 );

将文件位置指针移到文件开头(位置0),连接fin。seekg的参数通常为long类型的整数。

第二个参数可以指定寻找方向,ios::beg(默认)相对于流的开头定位,ios::cur相对于流当前位置定位,ios::end相对于流结尾定位。文件位置指针是个整数值,指定文件中离文件开头的相对位置(也称为离文件开头的偏移量)。下面是一些get文件位置指针的例子:

    // 定位到fileObject的第n个字节

    // 假设 ios::beg

    fileObject.seekg( n );

    // 在fileObject中向前放置n个字节

    fileObject.seekg( n, ios::cur );

    // 从fileObject结尾向后放置y字节

    fileObject.seekg( y, ios::end );

    // fileObject末尾的位置

    fileObject.seekg( o, ios::end );

        ostream成员函数seekp也可以进行类似的操作。成员函数tellg和tellp分别返回get和put指针的当前位置。下列语句将get文件位置指针值赋给long类型的变量location。

    location = filObject.tellg();

4.5  更新访问文件

4. 4 节介绍了格式化和写入访问文件的数据修改时会有破坏文件中其他数据的危险。例如,如果要把名字"White"改为"Worthinglon",则不是简单地重定义旧的名字。White的记录是以如下形式写人文件中的:

    300 White 0.00

如果用新的名字从文件中相同的起始位置重写该记录,记录的格式就成为:

    300 Worthington 0.00

        因为新的记录长度大于原始记录的长度,所以从“Worthington"的第二个“0”之后的字符将重定义文件中的下一条顺序记录。出现该问题的原因在于:在使用流插入运算符<<和流读取运算符>>的格式化输人,输出模型中,域的大小是不定的,因而记录的大小也是不定的。例如,7、14、-117、2047和27383都是int类型的值,虽然它们的内部存储占用相同的字节数,但是将它们以格式化文本打印到屏幕上或存储在磁盘上时要占用不同大小的域。因此,格式化输入,输出模型通常不用来更新已有的记录。

        也可以修改上述名字,但比较危险。比如,在300 Whlte 0.00之前的记录要复制到一个新的文件中,然后写入新的记录并把300 White 0.00之后的记录复制到新文件中。这种方法要求在更新一条记录时处理文件中的每一条记录。如果文件中一次要更新许多记录,则可以用这种方法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值