第8单元 类与对象(II)
本单元教学目标介绍类的继承与派生、虚函数和运算符重载等面向对象程序设计的基本概念,以及文件处理的基本方法。
学习要求深入理解面向对象程序设计方法的基本思想,包括封装、继承和多态及其在C++中的实现方法。
教学内容
C++中的类体现了面向对象技术所要求的抽象和封装机制,同时为继承提供了基础。面向对象技术中的抽象、封装、继承和多态性强有力地支持了对复杂的大型软件系统的构建、分析和维护,是现代软件工程的基础。在本单元中,我们介绍面向对象技术中的继承、重载和多态性等特性在C++中的实现。
8.1 继承
继承这一概念源于分类概念。首先请看如图8-1所示的分类树。
在图8-1中,最高层为一般化概念,其下面的每一层都比其上的各层更具体化。一旦在分类中定义了一个特征,则由该分类细分而成的下层类目均自动含有该特征。例如,一旦确定某物为红富士苹果,则可以确定它具有苹果的所有特性,当然是水果。这种层次结构也可用“IS-A”关系表达,即如某物为红富士苹果,则其是一个(is a)苹果,是一个水果。
在C++中,类的继承关系类似这种分类层次关系。如果一个类继承了另一个类的成员(包括数据成员和成员函数),则称前者基类(或父类),后者为其派生类(或子类),后者从前者派生。类的派生过程可以继续下去,即派生类又可作其他类的基类。
由某基类派生一个新类的形式为:
class <派生类名>:<访问权限> <基类名>
{
...,..
};
其中访问权限可以是关键字public或private之一。如果为public,称派生类从基类公有派生;如果为private,称派生类从基类私有派生。
公有派生时,基类成员的访问权限在派生类中保持不变,即原来基类中的私有成员在派生类中仍为私有成员;原来基类中的公有成员在派生类中仍为公有成员。这就意味着在派生类外可以访问其从基类继承下来的公有成员。然而,对基类而言,派生类也是其“外部”,因此在派生类中不能直接访问基类中的私有成员,也必须通过基类所提供的公共接口(成员函数)才可以访问基类中的私有成员。
私有派生时,基类中所有成员的访问权限在派生类中均为私有。即从派生类外部来看,其基类的所有成员均不可见。因此,为了对基类中的数据成员进行操作,在派生类中必须声明相应的公有成员函数。
在类声明中,声明为protected的成员称做保护成员。保护成员具有双重作用:对于其派生类而言,它是公有的;而对于其外部的程序而言,则是私有的。通常,如果一个类主要是作为基类以供派生新类而用,则其数据成员声明成保护的比较方便。但在这种情况下,如果由于某种原因而改变了保护成员的表示形式,则这些改变也要影响到派生类。因此,在实用中应仔细权衡程序的效率与程序的可维护性,以决定是否采用保护成员。
在C++中,还有所谓抽象类。抽象类只能作为基类派生新类,在程序中不能声明抽象类的对象。有多种因素可以使得一个类成为抽象类,例如使用保护的构造函数。保护的构造函数对除该类的派生类以外的所有外部程序来讲是私有的,所以,外部程序由于无法调用该构造函数而不能创建该类的对象。对该类的派生类来讲,该构造函数却是公有的,因而在创建其派生类的对象时就可以调用它为基类成员分配内存。
保护的析构函数同样阻止了在撤消对象时对它的调用,因此,如果一个类的析构函数被声明为保护的,则该类也是一个抽象类。
[例8-1] 从Person类公有派生一个职员类。
// Example 8-1:职员类
class Employee:public Person
{
char m_sDepartment[21];
char m_sPosition[21];
float m_fSalary;
public:
Employee(){}
Employee(const char *,int,char,const char *,const char *,float);
void SetDepartment(const char *);
void SetPosition(const char *);
void SetSalary(float);
char *GetDepartment() const;
char *GetPosition() const;
float GetSalary() const;
};
分 析:类Employee继承了其基类Person所有的成员。因此,在对Employee类的对象进行操作时,其基类的成员函数如GetName()等的用法与其自己的成员函数用法完全相同。
当派生类和基类中都定义有初始化构造函数时,则可在创建派生类的对象时调用基类中相应的构造函数来初始化基类中的成员。带有初始化基类成员的派生类初始化构造函数的定义具有如下的一般形式:
<派生类名>::<派生类名>(<参数表>):<基类1>(<实参表>),…,<基类n>(<实参表>)
{
… …
}
例如,职员类的构造函数为
#include <string.h>
Employee::Employee(const char *name,int age,char sex,const char dept*,
const char * posi,float salary):Person(name,age,sex)
{
strcpy(m_sDepartment,dept);
strcpy(m_sPosition,posi);
m_fSalary = salary;
}
8.2 虚函数
多态性是面向对象程序设计技术的关键概念之一。多态性概念是用来描术过程的,利用多态性可以用同一个函数名访问一个函数的不同实现。
C++支持编译时多态性和运行时多态性。所谓编译时多态性是指在编译器对源程序进行编译时就可以确定所调用的是哪一个函数,编译时多态性通过重载(函数重载和运算符重载)来实现;而运行时多态性则是指在程序运行过程中根据具体情况来确定调用的是哪一个函数,运行时多态性通过继承和虚函数来实现。
虚函数是一个在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数。声明一个虚函数的一般形式为:
virtual <类型> <函数名>(<参数表>);
一个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中该函数都保持virtual特性。因此,在派生类中重新定义该函数时,不再需要关键字virtual。但习惯上,为了提高程序的可读性,常在每层派生类中都重复地使用virtual关键字。在运行时,不同类对象调用的是各自的虚函数,这就是运行时的多态性。
使用虚函数时应注意:
1.在派生类中重新定义虚函数时,必须保证该函数的值和参数与基类中的声明完全一致,否则就属于重载(参数不同)或是一个错误(仅返回值不同);
2.如果在派生类中没有重新定义虚函数,则该类的对象将使用其基类中的虚函数代码;
3.析构函数可以是虚函数,但构造函数则不得是虚函数。一般地讲,若某类中定义有虚函数,则其析构函数也应当声明为虚函数。特别是在析构函数需要完成一些有意义的操作,比如释放内存时,尤其应当如此。
在编写面向对象的程序时,并非必须使用虚函数。然而,利用虚函数可使所设计的软件系统变得灵活,提高了代码的可重用性。同时,虚函数为一个类体系中所有子类的同一行为提供了统一的接口,这就使得程序员在使用一个类体系时只须记往一个接口即可。这种接口与实现分离的机制也提供了对类库(如MFC)的支持。如果能正确地实现这些类库,则它们将操作一个公共的接口,可以用来派生自己的类以满足特定的需要。正因为如此,有时在声明一个基类时无法为虚函数定义其具体实现,这时仍可以将其声明为纯虚函数,其具体实现留给派生类来定义。纯虚函数的声明方法为:
virtual <返回值类型> <函数名> (<参数表>)= 0;
纯虚函数是构成抽象类的因素之一,包含有纯虚函数的类为抽象类。
8.3 运算符重载
在C++中,运算符和函数一样,也可以重载。重载运算符主要用于对类的对象的操作。与函数的重载和虚函数一样,运算符重载也从一个方面体现了OOP技术的多态性。
重载一个运算符,必须定义该运算符的具体操作。为了使程序员能像定义函数的具体操作一样来重载一个运算符,C++提供了operator函数。该函数的一般形式为:
<类型> <类名>::operator <操作符>(<参数表>)
{
...,..
}
其中<类型>为函数的返回值,也就是运算符的运算结果值的类型;<类名>为该运算符重载所属类的类名;而<运算符>即所重载的运算符,可以是C++中除了“::”、“.”、“*(访问指针内容的运算符,与该运算符同形的指针说明运算符和乘法运算符允许重载)”和“?:”以外的所有运算符。
[例8-2] 声明一个复数类,并重载加法和赋值运算符以适应对复数运算的要求。
程 序:
// Example 8-2,复数类
class Complex
{
double m_fReal,m_fImag;
public:
Complex(double r = 0,double i = 0),m_fReal(r),m_fImag(i){}
double Real(){return m_fReal;}
double Imag(){return m_fImag;}
Complex operator +(Complex&);
Complex operator +(double);
Complex operator =(Complex);
};
Complex Complex::operator + (Complex &c) // 重载运算符 +
{
Complex temp;
temp.m_fReal = m_fReal+c.m_fReal;
temp.m_fImag = m_fImag+c.fImag;
return temp;
}
Complex Complex::operator + (double d) // 重载运算符+
{
Complex temp;
temp.m_fReal = m_fReal+d;
temp.m_fImag = m_fImag;
return temp;
}
Complex Complex::operator = (Complex c) // 重载运算符=
{
m_fReal = c.m_fReal;
m_fImag = c.m_fImag;
return *this;
}
// 测试主函数
void main()
{
Complex c1(3,4),c2(5,6),c3;
cout << "C1 = " << c1.Real() << "+j" << c1.Imag() << endl;
cout << "C2 = " << c2.Real() << "+j" << c2.Imag() << endl;
c3 = c1+c2;
cout << "C3 = " << c3.Real() << "+j" << c3.Imag() << endl;
c3 = c3+6.5;
cout << "C3 + 6.5 = " << c3.Real() << "+j" << c3.Imag() << endl;
}
输 出: C1 = 3+j4
C2 = 5+j6
C3 = 8+j10
C3+6.5 = 14.5+j10
分 析:在本例中,对运算符“+”进行了两次重载,分别用于两个复数的加法运算和一个复数与一个实数的加法运算。
可以看出,运算符重载事实上也是一种函数重载,但运算符重载的参数个数有限制。例如,对于双目运算符的重载须有且仅能有一个参数,该参数即为运算的右操作数,而左操作数则为该类的对象本身。这一点可以从函数的定义中清楚地看出。
在运算符重载函数的定义中,由程序员给出了该运算符重载的具体操作。该具体操作并不要求与所重载之运算符的意义完全相同。
一个重载了的运算符虽然其具体操作发生了变化,但其用法与该运算符的原始定义完全一样。应当说明的是:无论运算符重载的具体定义如何,重载了的运算符的优先级与结合性仍保持与其原始运算符的相同。
自学内容
8.4 类模板类是对问题空间的抽象,而类模板则是对类的抽象,即更高层次上的抽象。与函数模板相似,程序中可以通过高度抽象首先声明一个模板类,然后通过使用不同的实参生成不同类的对象。模板类的声明方法为:
template <class <类型参数>>
class <类名>
{
… …
};
模板类的具体内容与普通类没有本质上的区别,只是在其成员中要用到模板类型参数。例如:
template <class T>
class AnyType
{
T x,y;
public:
AnyType(T a,T b),x(a),y(b){}
T GetX(){return x;}
T GetY(){return y;}
};
上面声明的模板类中,所有的成员函数都是内联函数。如果在类外定义模板类的成员函数,则必须遵循以下语法:
template <class <类型参数>>
<返回值类型> <类名> <<类型参数>>::<函数名>(<参数表>)
{
… …
}
如果模板中有多个类型参数,则无论具体的成员函数是否用到它们,所有的参数类型必须在类名后一一列出。
使用模板类的第一步就是声明一个模板类的对象,其方法为:
<类名> <<类型实参>> <对象>;
其中<类型实参>是任何已存在的数据类型,也可以是非模板类。如果模板类带有多个参数类型,则除缺省参数外的所有参数都必须给出其实参类型。
[例8-3] 声明一个通用的栈类。
// Example 8-3,通用的栈类
#include <iostream.h>
template <class T,int n = 10>
class AnyStack
{
T m_tStack[n];
int m_nMaxElement;
int m_nTop;
public:
AnyStack():m_nMaxElement(n),m_nTop(0){}
int GetTop(){return Top;}
BOOL Push(T);
BOOL Pop(T&);
};
template <class T,int n>
BOOL AnyStack <T,n>::Push(T elem)
{
if(m_nTop<=MaxElement)
{
m_tStack[m_nTop] = elem;
m_nTop++;
return TRUE;
}
}
template <class T,int n>
BOOL AnyStack <T,n>::Pop(T &elem)
{
if(m_nTop > 0)
{
m_nTop--;
elem = m_tStack[m_nTop];
return TRUE;
}
else
return FALSE;
}
// 测试用主函数
void main()
{
AnyStack <int> iStack;
int n;
iStack.Push(5);
iStack.Push(6);
iStack.Pop(n);
cout <<,n =,<< n << endl;
iStack.Pop(n);
cout <<,n =,<< n << endl;
}
分 析:本例声明了一个通用的栈类模板,使用缺省参数n给出栈大小。在测试主函数中,使用该模板声明了一个整数栈对象。然后将两个整数压入栈中,然后逐一弹出并打印。
8.5 文件处理
在前面各单元的举例程序中,数据的输入输出工作均使用cin和cout通过标准输入设备(一般设置为键盘)和标准输出设备(一般设置为显示器)进行。一般来说,键盘和显示器适合于处理少量数据和信息的输入输出工作,方便、快捷,是最常用的输入输出设备。但是如果要进行大量数据的加工处理,键盘和显示器的局限就很明显了。通常的做法是利用磁盘作为数据存放的中介,应用程序的输入模块通过键盘或其他输入设备将数据读入磁盘,处理模块对存放在磁盘中的数据进行加工,加工后的数据或者仍然存放在磁盘上,以备今后再处理,或者由输出模块通过打印机等设备以报表等格式输出。对于少量数据(如查询结果等)也可以直接显示在屏幕上。
在实际应用中,无论是数据库系统、管理信息系统、科学与工程计算,还是文字处理与办公自动化、图形图象处理,都要处理大量存放在磁盘上的数据。
数据在磁盘中是以文件的方式存放的。由于磁盘的容量一般都很大,如常用的3英寸软盘的容量为1.44M(1M = 1024K = 220字节),如果用来存放中文信息,仅一张软盘就可以存放一部六、七十万字的小说。而现在微机上常用的硬磁盘的容量则更为巨大,一般均在4.3G~6.8G,有的已超过10G(1G = 1024M)!在这样大容量的磁盘中,要想按实际地址存放或者查找一个数据是非常困难的。因此,在计算机中引入了一个叫做文件系统的软件,统一管理存放在软盘或硬盘中的数据。文件系统是操作系统的一部分,有点象城市里的户籍系统,通过将数据组织成文件进行管理。
所谓文件,就是逻辑上有联系的一批数据(可以是一批实验数据,或者一篇文章,一幅图象,甚至一段程序等),有一个文件名作为标识。每个文件在磁盘中的具体存放位置、格式以及读写等工作都由文件系统管理,对于使用操作系统的用户来说,只需告诉操作系统一个文件的文件名即可对其进行读、写、删除、拷贝、显示和打印等工作,这是每个和计算机,和操作系统打过交道的人都非常熟悉的。
在使用C++编写应用程序时也可以通过操作系统来处理以文件形式存放在磁盘上的数据。操作系统命令一般是将文件作为一个整体来处理的,例如删除文件、拷贝文件等等,而应用程序往往要求对文件的内容进行处理。由于文件的内容可能千变万化,文件的大小各不相同,那么以什么为单位处理文件中的数据呢?C++中引入了流式文件(stream)的概念,即无论文件的内容是什么,一律看成是由字符(或字节)构成的序列,即字符流。流式文件中的基本单位是字节,磁盘文件和内存变量之间的数据交流以字节为基础。如果实际数据还有比单纯按字节划分更高级的逻辑结构,可以通过一次读写多个字节来实现。
下面介绍文件处理中的几个基本概念:
,打开和关闭文件:对文件操作前,要为其准备相应的缓冲区、缓冲区管理变量和文件指针等,还要将文件和一个特定的变量联系起来,这个工作就叫做打开文件。如果应用程序不再使用某个文件了,就应该及时将其占用的缓冲区等资源释放,这个工作就叫做关闭文件。
,读:从文件中将数据拷贝到内存变量中来。根据情况不同,一次可以读一个字节,也可以根据内存变量的大小读相应数量的字节,甚至可以一次将一批数据读到一片连续的存储区 (如数组或动态分配的存储块) 中;
,写:将内存变量中的数据拷贝到文件中去。和读文件的情况相似,一次可以将一个变量或者一片连续存储区中的数据写入文件;
,文件指针:由于通常文件中的数据很多,所以在读写时应该指明是对哪些数据进行操作。在流式文件中采用的方法是设立一个存放文件读写位置的变量,又称文件指针。在开始对某文件进行操作时将文件指针的值设置为0,表示读写操作应从文件首部开始执行;每次读、写之后,自动将文件指针的值加上本次读、写的字节数,作为下次读写的位置。
,缓冲区:由于磁盘的读写速度比内存的处理速度要慢一个数量级,而且磁盘驱动器是机电设备,定位精度相对比较差,所以磁盘数据存取以扇区(sector,磁盘上某磁道中的一个弧形段,通常存放固定数量的数据。扇区之间有间隙隔开)或者簇(cluster,由若干连续的扇区组成)为单位。具体做法是在内存中划出一片存储单元,称为缓冲区。从磁盘中读取数据时先将含有该数据的扇区或簇读到缓冲区中,然后再将具体的数据拷贝到应用程序的变量中去。下次再读数据时,首先判断数据是否在缓冲区中,如果在,则直接从缓冲区中读,否则就要从磁盘中再读另一个扇区或簇。向磁盘中写数据也是这样,数据总是先写入缓冲区中,直到缓冲区写满之后再一起送入磁盘。为了能使应用程序同时处理若干个文件,就必需在内存中开辟多个缓冲区。对缓冲区的管理是操作系统的基本功能之一。
在C++程序中对文件的处理由以下步骤组成:打开文件,数据定位,读写数据,关闭文件。
在MFC中,用CFile类处理文件。
CFile类由CObject类派生,是MFC中所有其他文件类的基类。
磁盘文件在CFile类构建时自动打开,而在析构时自动关闭。使用CFile类的一些静态成员函数还可以在不打开文件的情况下获取文件的一些状态。下面介绍CFile类的部分方法。
1,构造函数
CFile(LPCTSTR lpszFileName,UINT nOpenFlags);
该函数有两个参数,第1个是要打开的文件的路径,第2个参数是文件存取和共享模式:
CFile::modeCreate 建立一个新文件。如果文件存在,则长度截为0
CFile::modeNoTruncate 同CFile::modeCreate,但不将文件长度截为0
CFile::modeRead 以只读方式打开文件
CFile::modeReadWrite 以读写方式打开文件
CFile::modeWrite 以只写方式打开文件
CFile::typeBinary 在子类中用于二进制模式
CFile::typeText 在子类中用于文本模式
CFile::shareDenyNone 文件打开时不禁止其它进程写该文件
CFile::shareDenyRead 文件打开时禁止其它进程读该文件
CFile::shareDenyWrite 文件打开时禁止其它进程写该文件
CFile::shareExclusive 文件以独占方式打开时,禁止其它进程读写该文件一般至少需要一种存取模式和一项共享模式,各选项间以或运算“|”连接。例如:
char* pFileName = "test.dat";
CFile MyFile(pFileName,CFile::modeCreate|CFile::modeWrite );
使用构造函数打开的文件在该CFile对象销毁时自动关闭。
2,文件内容读写
virtual UINT Read( void* lpBuf,UINT nCount );
virtual void Write(const void* lpBuf,UINT nCount );
read()从文件中读数据至指定变量,write()将指定变量内容写入文件。其中lpBuf为存放要读写内容的变量的地址,nCount为指定读写字节数。Read()函数的返回值为实际读写字节数。
3,定位函数
virtual LONG Seek( LONG lOff,UINT nFrom );
用于定位文件指针。参数nFrom可以是CFile::begin(文件始端),CFile::current(当前指针位置),CFile::end(文件尾部)之一,参数lOff 为相对于nForm的位移量。
4,状态函数
virtual DWORD GetLength( ) const; // 获取文件长度
virtual DWORD GetPosition( ) const; // 获取当前文件指针
virtual CString GetFileName( ) const; // 获取文件名
virtual CString GetFileTitle( ) const; // 获取文件标题
virtual CString GetFilePath( ) const; // 获取文件全路径以上所列仅为CFile类中部分成员函数的功能,有关内容请参看MSDN的说明。
CFile类还有若干派生类,其中的CStdioFile类提供了对文本文件的成行读写操作方法:
BOOL ReadString(CString& rString);
virtual void WriteString( LPCTSTR lpsz );
文件读写应用举例见例8-4和例8-5。
调试技术
8.6 异常处理机制
程序可能按我们的意愿终止(例如使用return语句或调用exit()函数),也可能因为程序中发生了错误而终止。例如程序执行时遇到除数为0的除法运算或数组下标越界(注意C++是不理会数组下标越界的),这时将产生系统中断,从而导致上在执行的程序提前终止。
异常处理(exception handling)机制是C++中用于管理程序运行期间错误的一种结构化方法。异常处理机制将程序中的正常处理代码与异常处理代码明显地区别开来,提高了程序的可读性。
C++的异常处理机制的基本思想是将异常的检测与处理分离。当在一个函数体中检到异常条件存在,但无法确定相应的处理方法时,将引发一个异常,并由函数的直接或间接调用者检测并处理这个异常。
这一基本思想用三个保留字实现:throw、try和catch。在一般情况下,被调用函数直接检测到异常条件并使用throw引发一个异常;在上层调用函数中使用try检测函数调用是否引发了异常,被检测到的各种异常由catch语句捕获并作相应处理。
需要检测异常的程序段(包括函数调用)必须放在try语句块中执行,异常由紧跟着try语句块后面的catch语句捕获并处理。因而try与catch总是结合使用的,其形式为:
try
{
… …
}
catch (<类型1> <参数1>)
{
… …
}
… …
catch (<类型n> <参数n>)
{
… …
}
catch(…)
{
… …
}
在上述结构中,一个try语句可与多个catch语句相联系。如果某个catch语句的参数类型与引发异常的信息数据类型相匹配,则执行该catch语句的异常处理(捕获异常),此时由throw语句抛出的异常信息(值)传送给catch语句中的参数。
在多个catch语句的最后可以使用catch(…)捕获所有其他类型的异常。其中的省略号表示可与任何数据类型匹配。
引发异常的throw语句必须在try语句块内,或是由try语句块中直接或间接调用的函数体执行。throw语句的一般形式为:
throw exception;
这里exception表示一个异常值,它可以是任意类型的变量、对象或值。
注意,catch语句的类型匹配过程中不作任何类型转换,例如unsigned int类型的异常值不能被int类型的catch参数捕获。
MFC提供类CException作为用于MFC异常的基础类,例如CFileException是其用于处理文件异常的派生类。CFileException类提供有关处理文件输入/输出时的异常信息,可通过检查其成员变量m_cause的值判断文件异常。m_cause可取值有:
CFileException::none 没有错误发生
CFileException::generic 未指明的错误
CFileException::fileNotFound 无法找到文件
CFileException::badPath 路径错误
CFileException::tooManyOpenFiles 同时打开的文件数目太多
CFileException::accessDenied 文件拒绝访问
CFileException::invalidFile 错误应用文件
CFileException::removeCurrentDir 试图删除当前目录
CFileException::directoryFull 目录满
CFileException::badSeek 设置文件指针错误
CFileException::hardIO 硬件错误
CFileException::sharingViolation 文件共享错误
CFileException::lockViolation 试图为已加锁区域再加锁
CFileException::diskFull 磁盘满
CFileException::endOfFile 已达文件尾
检测MFC文件处理异常的举例见例8-4和例8-5。
程序设计举例
[例8-4] 编写一个用于文件拷贝的程序。
算 法:编写这类应用程序时要注意的问题是应该充分考虑到用户在使用该程序时可能遇到的各种问题,并在程序中设计出相应的处理方法。
程 序:
// Example 8-4,文件拷贝
#include <iostream.h>
#include <afx.h>
void main()
{
char SourceName[81];
char DestinName[81];
cout << "\n请输入源文件名:";
cin >> SourceName;
cout << "\n请输入目标文件名:";
cin >> DestinName;
try
{
CFile fileSource(SourceName,CFile::modeRead);
CFile fileDestin(DestinName,CFile::modeCreate |CFile::modeWrite);
char c;
while(fileSource.Read(&c,1))
fileDestin.Write(&c,1);
}
catch(CFileException *e)
{
switch(e->m_cause)
{
case CFileException::fileNotFound:
cout <<,未找到文件!” << endl;
break;
case CFileException::badPath:
cout <<,路径输入有错!” << endl;
break;
case CFileException::accessDenied:
cout <<,没有访问权限!” << endl;
break;
case CFileException::diskFull:
cout <<,磁盘满!” << endl;
break;
default:
cout <<,在文件拷贝过程中发生不知名错误!” << endl;
break;
}
}
}
分 析:本程序是文件处理类应用程序的典型。在设计这类程序时,要注意两个问题:一是要使用异常处理机制捕获在文件操作过程中可能发生的各种错误;二是对各种不正常情况都要有适当的提示,避免用户认为程序失控,或者出了毛病。可以看出,本程序中大部分语句都是为以上两个目的服务的。至于文件操作本身,和普通的输入输出操作几乎完全一样。
[例8-5] 编写一个用于显示文本文件内容的程序。
算 法:利用CStdioFile类的成行读命令读出文本内容,然后分屏显示。为了达到分屏显示的目的,设置了一个计数器变量count,每当count = 23时,暂停显示文件内容,显示提示信息,并等待用户的按键。
程 序:
// Example 8-5,显示文本文件
#include <iostream.h>
#include <conio.h>
#include <afx.h>
void main()
{
char SourceName[81];
cout << "请输入文件名,";
cin >> SourceName;
try
{
CStdioFile SourceText(SourceName, CFile::modeRead|CFile::typeText);
int count = 0;
CString line;
while(SourceText.ReadString(line))
{
cout << line << endl;
if(count==22)
{
cout << "Press any key to continue..." << endl;
_getch();
count = 0;
}
else
count++;
}
}
catch(CFileException *e)
{
switch(e->m_cause)
{
case CFileException::fileNotFound:
cout << "未找到文件!" << endl;
break;
case CFileException::badPath:
cout << "路径输入有错!" << endl;
break;
case CFileException::accessDenied:
cout << "没有访问权限!" << endl;
break;
default:
cout << "文件操作错误!" << endl;
break;
}
}
}
分 析:本程序是为阅读文本文件的内容设计的,不能用来阅读非文本文件(如命令文件、可执行文件等)的内容,功能类似于DOS命令TYPE,但可以分页显示,这对于阅读比较长的文件来说是非常方便的。程序中用函数_getch()捕捉用户的按键。该函数的原型在头文件conio.h中。
[例8-6] 设计一个成员函数,用于将例7-5中定义的工资单写入数据文件。
算 法:在例8-4中,我们已经使用了CFile::Write()函数将一个字符写入到文件中,下面使用该函数将一个对象中的所有数据成员,以二进制方式写入到文件。
程 序:
// Example 8-6,保存某职工的工资单
void Salary::Save_Salary(CFile* fp)
{
fp->Write(this,sizeof(Salary));
}
分 析:该函数的结构非常简单,直接调用CFile::Write()函数输出对象。对象的地址由其this指针提供,对象的大小由sizeof()计算出。
[例8-7] 从人事档案文件中读一个记录。
程 序:
// Example 8-7,从数据文件读一个职工的工资单
void Salary::Read_Salary(CFile* fp)
{
fp->Read(this,sizeof(Salary));
}
分 析:使用函数CFile::Read()和CFile::Write()读写文件时,必须以二进制方式打开文件,否则可能出错。该函数的调用方法为:
Salary person; // 职工工资单类型的工作对象
CFile m_cfile("salary.dat",CFile::modeRead); // 打开数据文件
,..,..
person.Read_Salary(&m_cfile); // 将数据读入到对象中单元上机练习题目
8.1 从类Person中派生出一个教师类,新增的属性有:专业、职称和主讲课程(一门),并为这些属性定义相应的方法。
8.2 编写一个人事档案管理程序,将例6-6的人事档案存放在文件中。
8.3 编写一个图书馆的流通管理程序,要求具有借书登记、还书销账、对逾期不还的图书打印催还书通知单,以及书卡查询等功能。提示:分别为书卡、借书证和借书记录建立3个结构体类型和3个文件。