第8单元类与对象(I) 154 第8单元类与对象(I〕 本单元教学目标 介绍类的继承与派生、虚函数和运算符重载等面向对象程序设计的基本概念,以及文件 处理的基本方法 学习要求 深入理解面向对象程序设计方法的基本思想,包括封装、继承和多态及其在C++中的实 现方法。 教学内容 C艹+中的类体现了面向对象技术所要求的抽象和封装机制,同时为继承提供了基础。面 向对象技术中的抽象、封装、继承和多态性强有力地支持了对复杂的大型软件系统的构建、 分析和维护,是现代软件工程的基础。在本单元中,我们介绍面向对象技术中的继承、重载 和多态性等特性在C++中的实现。 81继承 继承这一概念源于分类概念。首先请看如图8-1所示的分类树 在图8-1中,最高层为一般化概 念,其下面的每一层都比其上的各 层更具体化。一旦在分类中定义了 桃 梨 苹果 个特征,则由该分类细分而成的 下层类目均自动含有该特征。例如 莱阳梨 红富士 秦冠 一旦确定某物为红富士苹果,则可 以确定它具有苹果的所有特性,当 图8-1水果的分类树 然是水果。这种层次结构也可用“IS 一A”关系表达,即如某物为红富士苹果,则其是一个(isa)苹果,是一个水果 在C+中,类的继承关系类似这种分类层次关系。如果一个类继承了另一个类的成员 (包括数据成员和成员函数),则称前者基类(或父类),后者为其派生类(或子类),后 者从前者派生。类的派生过程可以继续下去,即派生类又可作其他类的基类。 由某基类派生一个新类的形式为:
第 8 单元 类与对象(II) - 154 - 第 8 单元 类与对象(II) 本单元教学目标 介绍类的继承与派生、虚函数和运算符重载等面向对象程序设计的基本概念,以及文件 处理的基本方法。 学习要求 深入理解面向对象程序设计方法的基本思想,包括封装、继承和多态及其在 C++中的实 现方法。 教学内容 C++中的类体现了面向对象技术所要求的抽象和封装机制,同时为继承提供了基础。面 向对象技术中的抽象、封装、继承和多态性强有力地支持了对复杂的大型软件系统的构建、 分析和维护,是现代软件工程的基础。在本单元中,我们介绍面向对象技术中的继承、重载 和多态性等特性在 C++中的实现。 8.1 继承 继承这一概念源于分类概念。首先请看如图 8-1 所示的分类树。 在图 8-1 中,最高层为一般化概 念,其下面的每一层都比其上的各 层更具体化。一旦在分类中定义了 一个特征,则由该分类细分而成的 下层类目均自动含有该特征。例如, 一旦确定某物为红富士苹果,则可 以确定它具有苹果的所有特性,当 然是水果。这种层次结构也可用“IS -A”关系表达,即如某物为红富士苹果,则其是一个(is a)苹果,是一个水果。 在 C++中,类的继承关系类似这种分类层次关系。如果一个类继承了另一个类的成员 (包括数据成员和成员函数),则称前者基类(或父类),后者为其派生类(或子类),后 者从前者派生。类的派生过程可以继续下去,即派生类又可作其他类的基类。 由某基类派生一个新类的形式为: 图8-1 水果的分类树 水果 桃 梨 苹果 莱阳梨 红富士 秦冠
第8单元类与对象(I) 155 class:<访问权限〉<基类名〉 其中访问权限可以是关键字 public或 private之一。如果为 public,称派生类从基类公有派生; 如果为 private,称派生类从基类私有派生。 公有派生时,基类成员的访问权限在派生类中保持不变,即原来基类中的私有成员在派 生类中仍为私有成员;原来基类中的公有成员在派生类中仍为公有成员。这就意味着在派生 类外可以访问其从基类继承下来的公有成员。然而,对基类而言,派生类也是其“外部” 因此在派生类中不能直接访问基类中的私有成员,也必须通过基类所提供的公共接口(成员 函数)才可以访问基类中的私有成员。 私有派生时,基类中所有成员的访问权限在派生类中均为私有。即从派生类外部来看, 其基类的所有成员均不可见。因此,为了对基类中的数据成员进行操作,在派生类中必须声 明相应的公有成员函数 在类声明中,声明为 protected的成员称做保护成员。保护成员具有双重作用:对于其 派生类而言,它是公有的:而对于其外部的程序而言,则是私有的。通常,如果一个类主要 是作为基类以供派生新类而用,则其数据成员声明成保护的比较方便。但在这种情况下,如 果由于某种原因而改变了保护成员的表示形式,则这些改变也要影响到派生类。因此,在实 用中应仔细权衡程序的效率与程序的可维护性,以决定是否采用保护成员 在C+中,还有所谓抽象类。抽象类只能作为基类派生新类,在程序中不能声明抽象 类的对象。有多种因素可以使得一个类成为抽象类,例如使用保护的构造函数。保护的构造 函数对除该类的派生类以外的所有外部程序来讲是私有的,所以,外部程序由于无法调用该 构造函数而不能创建该类的对象。对该类的派生类来讲,该构造函数却是公有的,因而在创 建其派生类的对象时就可以调用它为基类成员分配内存 保护的析构函数同样阻止了在撤消对象时对它的调用,因此,如果一个类的析构函数被 声明为保护的,则该类也是一个抽象类 例8-11从 Person类公有派生一个职员类 / Example8-1:职员类 class Employee: public Person char m dEpartment [21] char m sPosition [21] float fSalary public ployee o仆} Employee(const char * int, char, const char * const char * float (const char * void SetPosition(const char *
第 8 单元 类与对象(II) - 155 - 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 Set Salary(float) float Get Salary( const 分析:类 Employee继承了其基类 Person所有的成员。因此,在对 Employee类的 对象进行操作时,其基类的成员函数如 GetName()等的用法与其自己的成员函数用法完 全相同 当派生类和基类中都定义有初始化构造函数时,则可在创建派生类的对象时调用基类中 相应的构造函数来初始化基类中的成员。带有初始化基类成员的派生类初始化构造函数的定 义具有如下的一般形式: ::(( Employee: Employee(const char *name, int age, char sex, const char depts const char posi, float salary): Person(name, age, sex strcpy(m dEpartment, dept) strcpy(m sPosition, posi m fSalary salary 82虚函数 多态性是面向对象程序设计技术的关键概念之一。多态性概念是用来描术过程的,利用 多态性可以用同一个函数名访问一个函数的不同实现。 C++支持编译时多态性和运行时多态性。所谓编译时多态性是指在编译器对源程序进行 编译时就可以确定所调用的是哪一个函数,编译时多态性通过重载(函数重载和运算符重载) 来实现;而运行时多态性则是指在程序运行过程中根据具体情况来确定调用的是哪一个函 数,运行时多态性通过继承和虚函数来实现 虚函数是一个在某基类中声明为 virtual并在一个或多个派生类中被重新定义的成员函 数。声明一个虚函数的一般形式为: virtual(<参数表》); 个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中
第 8 单元 类与对象(II) - 156 - void SetSalary(float); char *GetDepartment() const; char *GetPosition() const; float GetSalary() const; }; 分 析:类 Employee 继承了其基类 Person 所有的成员。因此,在对 Employee 类的 对象进行操作时,其基类的成员函数如 GetName()等的用法与其自己的成员函数用法完 全相同。 当派生类和基类中都定义有初始化构造函数时,则可在创建派生类的对象时调用基类中 相应的构造函数来初始化基类中的成员。带有初始化基类成员的派生类初始化构造函数的定 义具有如下的一般形式: ::():(),…,() { … … } 例如,职员类的构造函数为 #include 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 (); 一个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中
第8单元类与对象(I) 157 该函数都保持 virtual特性。因此,在派生类中重新定义该函数时,不再需要关键字 virtual 但习惯上,为了提高程序的可读性,常在每层派生类中都重复地使用 virtual关键字。在运 行时,不同类对象调用的是各自的虚函数,这就是运行时的多态性。 使用虚函数时应注意 1.在派生类中重新定义虚函数时,必须保证该函数的值和参数与基类中的声明完全 致,否则就属于重载(参数不同)或是一个错误(仅返回值不同) 2.如果在派生类中没有重新定义虚函数,则该类的对象将使用其基类中的虚函数代码 3.析构函数可以是虚函数,但构造函数则不得是虚函数。一般地讲,若某类中定义有 虚函数,则其析构函数也应当声明为虚函数特别是在析构函数需要完成一些有意义的操作, 比如释放内存时,尤其应当如此 在编写面向对象的程序时,并非必须使用虚函数。然而,利用虚函数可使所设计的软件 系统变得灵活,提高了代码的可重用性。同时,虚函数为一个类体系中所有子类的同一行为 提供了统一的接口,这就使得程序员在使用一个类体系时只须记往一个接口即可。这种接口 与实现分离的机制也提供了对类库(如MFC)的支持。如果能正确地实现这些类库,则它 们将操作一个公共的接口,可以用来派生自己的类以满足特定的需要。正因为如此,有时在 声明一个基类时无法为虚函数定义其具体实现,这时仍可以将其声明为纯虚函数,其具体实 现留给派生类来定义。纯虚函数的声明方法为: virtual()=0 纯虚函数是构成抽象类的因素之一,包含有纯虚函数的类为抽象类 83运算符重载 在C++中,运算符和函数一样,也可以重载。重载运算符主要用于对类的对象的操作 与函数的重载和虚函数一样,运算符重载也从一个方面体现了OOP技术的多态性 重载一个运算符,必须定义该运算符的具体操作。为了使程序员能像定义函数的具体操 作一样来重载一个运算符,C+提供了 operator函数。该函数的一般形式为 类型〉: operator(为函数的返回值,也就是运算符的运算结果值的类型:为该运算符重载 所属类的类名;而即所重载的运算符,可以是C++中除了“:”、“ (访 问指针内容的运算符,与该运算符同形的指针说明运算符和乘法运算符允许重载)”和“?” 以外的所有运算符。 [例8-2]声明一个复数类,并重载加法和赋值运算符以适应对复数运算的要求 程序 / Example8-2:复数类
第 8 单元 类与对象(II) - 157 - 该函数都保持 virtual 特性。因此,在派生类中重新定义该函数时,不再需要关键字 virtual。 但习惯上,为了提高程序的可读性,常在每层派生类中都重复地使用 virtual 关键字。在运 行时,不同类对象调用的是各自的虚函数,这就是运行时的多态性。 使用虚函数时应注意: 1.在派生类中重新定义虚函数时,必须保证该函数的值和参数与基类中的声明完全一 致,否则就属于重载(参数不同)或是一个错误(仅返回值不同); 2.如果在派生类中没有重新定义虚函数,则该类的对象将使用其基类中的虚函数代码; 3.析构函数可以是虚函数,但构造函数则不得是虚函数。一般地讲,若某类中定义有 虚函数,则其析构函数也应当声明为虚函数。特别是在析构函数需要完成一些有意义的操作, 比如释放内存时,尤其应当如此。 在编写面向对象的程序时,并非必须使用虚函数。然而,利用虚函数可使所设计的软件 系统变得灵活,提高了代码的可重用性。同时,虚函数为一个类体系中所有子类的同一行为 提供了统一的接口,这就使得程序员在使用一个类体系时只须记往一个接口即可。这种接口 与实现分离的机制也提供了对类库(如 MFC)的支持。如果能正确地实现这些类库,则它 们将操作一个公共的接口,可以用来派生自己的类以满足特定的需要。正因为如此,有时在 声明一个基类时无法为虚函数定义其具体实现,这时仍可以将其声明为纯虚函数,其具体实 现留给派生类来定义。纯虚函数的声明方法为: virtual ()= 0; 纯虚函数是构成抽象类的因素之一,包含有纯虚函数的类为抽象类。 8.3 运算符重载 在 C++中,运算符和函数一样,也可以重载。重载运算符主要用于对类的对象的操作。 与函数的重载和虚函数一样,运算符重载也从一个方面体现了 OOP 技术的多态性。 重载一个运算符,必须定义该运算符的具体操作。为了使程序员能像定义函数的具体操 作一样来重载一个运算符,C++提供了 operator 函数。该函数的一般形式为: ::operator () { ... ... } 其中为函数的返回值,也就是运算符的运算结果值的类型;为该运算符重载 所属类的类名;而即所重载的运算符,可以是 C++中除了“::”、“.”、“*(访 问指针内容的运算符,与该运算符同形的指针说明运算符和乘法运算符允许重载)”和“?:” 以外的所有运算符。 [例 8-2] 声明一个复数类,并重载加法和赋值运算符以适应对复数运算的要求。 程 序: // Example 8-2: 复数类
第8单元类与对象(I) 158 class Complex double m fReal, m fImag: public Complex(double r =0, double i =0): m fReal(r), m fImag(i) double Real( freturn m fReal: 1 double Imago freturn m fImag: I Complex operator +(Complex& Complex operator +(double) Complex operator =(Complex) Complex Complex: operator+( Complex&c)//重载运算符 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 omplex Complex: operator=( Complex c)//重载运算符 m fReal c. m fReal m fImag c m fImag return *this //测试主函数 void maino Complex cl(3, 4), c2(5, 6), c3: cout <<C1 =< cl RealO<<+j"<< cl Imag(<< endl cout < C2 =< c2 Real(<<+j<< c2 Imag(<< endl
第 8 单元 类与对象(II) - 158 - 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;
第8单元类与对象(I) 159 cout class Any Type
第 8 单元 类与对象(II) - 159 - cout > class { … … }; 模板类的具体内容与普通类没有本质上的区别,只是在其成员中要用到模板类型参数。 例如: template class AnyType {
第8单元类与对象(I) 160- blie Any type (t a, t b): x(a), y(b)1 T GetXo freturn x: 1 T GetYO return y: 上面声明的模板类中,所有的成员函数都是内联函数。如果在类外定义模板类的成员函数, 则必须遵循以下语法: template() 如果模板中有多个类型参数,则无论具体的成员函数是否用到它们,所有的参数类型必须在 类名后一一列出。 使用模板类的第一步就是声明一个模板类的对象,其方法为: 类名〉是任何已存在的数据类型,也可以是非模板类。如果模板类带有多个参数类 型,则除缺省参数外的所有参数都必须给出其实参类型。 [例8-3]声明一个通用的栈类。 / Example8-3:通用的栈类 include iostream. h> template OoL AnyStack :: Push(T elem)
第 8 单元 类与对象(II) - 160 - T x, y; public: AnyType(T a, T b): x(a), y(b){} T GetX(){return x;} T GetY(){return y;} }; 上面声明的模板类中,所有的成员函数都是内联函数。如果在类外定义模板类的成员函数, 则必须遵循以下语法: template > >::() { … … } 如果模板中有多个类型参数,则无论具体的成员函数是否用到它们,所有的参数类型必须在 类名后一一列出。 使用模板类的第一步就是声明一个模板类的对象,其方法为: > ; 其中是任何已存在的数据类型,也可以是非模板类。如果模板类带有多个参数类 型,则除缺省参数外的所有参数都必须给出其实参类型。 [例 8-3] 声明一个通用的栈类。 // Example 8-3: 通用的栈类 #include template 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 BOOL AnyStack ::Push(T elem) {
第8单元类与对象(I) if(m nTop BOOL AnyStack :: Pop (t &elem) if(m nTop >0) m nTop- elem = m tStack[m nTop return truE: else return False //测试用主函数 void main o AnyStack sTack; sTack Push(5) sTack Push(6) sTack Pop(n) cout < wn =n<< n << endl cout < << n< endl 分析:本例声明了一个通用的栈类模板,使用缺省参数n给出栈大小。在测试主函 数中,使用该模板声明了一个整数栈对象。然后将两个整数压入栈中,然后逐一弹出并打印 85文件处理 在前面各单元的举例程序中,数据的输入输出工作均使用cin和cout通过标准输入设备 (一般设置为键盘)和标准输出设备(一般设置为显示器)进行。一般来说,键盘和显示器
第 8 单元 类与对象(II) - 161 - if(m_nTop BOOL AnyStack ::Pop(T &elem) { if(m_nTop > 0) { m_nTop--; elem = m_tStack[m_nTop]; return TRUE; } else return FALSE; } // 测试用主函数 void main() { AnyStack 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 通过标准输入设备 (一般设置为键盘)和标准输出设备(一般设置为显示器)进行。一般来说,键盘和显示器
第8单元类与对象(I) 162 适合于处理少量数据和信息的输入输出工作,方便、快捷,是最常用的输入输出设备。但是 如果要进行大量数据的加工处理,键盘和显示器的局限就很明显了。通常的做法是利用磁盘 作为数据存放的中介,应用程序的输入模块通过键盘或其他输入设备将数据读入磁盘,处理 模块对存放在磁盘中的数据进行加工,加工后的数据或者仍然存放在磁盘上,以备今后再处 理,或者由输出模块通过打印机等设备以报表等格式输出。对于少量数据(如査询结果等) 也可以直接显示在屏幕上。 在实际应用中,无论是数据库系统、管理信息系统、科学与工程计算,还是文字处理与 办公自动化、图形图象处理,都要处理大量存放在磁盘上的数据 数据在磁盘中是以文件的方式存放的。由于磁盘的容量一般都很大,如常用的3英寸软 盘的容量为14M(1M=1024K=220字节),如果用来存放中文信息,仅一张软盘就可以 存放一部六、七十万字的小说。而现在微机上常用的硬磁盘的容量则更为巨大,一般均在 43G~6.8G,有的已超过10G(lG=1024M)!在这样大容量的磁盘中,要想按实际地址存 放或者查找一个数据是非常困难的。因此,在计算机中引入了一个叫做文件系统的软件,统 一管理存放在软盘或硬盘中的数据。文件系统是操作系统的一部分,有点象城市里的户籍系 统,通过将数据组织成文件进行管理 所谓文件,就是逻辑上有联系的一批数据(可以是一批实验数据,或者一篇文章,一幅 图象,甚至一段程序等),有一个文件名作为标识。每个文件在磁盘中的具体存放位置、格 式以及读写等工作都由文件系统管理,对于使用操作系统的用户来说,只需告诉操作系统一 个文件的文件名即可对其进行读、写、删除、拷贝、显示和打印等工作,这是每个和计算机, 和操作系统打过交道的人都非常熟悉的 在使用C艹+编写应用程序时也可以通过操作系统来处理以文件形式存放在磁盘上的数 据。操作系统命令一般是将文件作为一个整体来处理的,例如删除文件、拷贝文件等等,而 应用程序往往要求对文件的内容进行处理。由于文件的内容可能千变万化,文件的大小各不 相同,那么以什么为单位处理文件中的数据呢?C++中引入了流式文件( stream)的概念, 即无论文件的内容是什么,一律看成是由字符(或字节)构成的序列,即字符流。流式文件 中的基本单位是字节,磁盘文件和内存变量之间的数据交流以字节为基础。如果实际数据还 有比单纯按字节划分更高级的逻辑结构,可以通过一次读写多个字节来实现 下面介绍文件处理中的几个基本概念 打开和关闭文件:对文件操作前,要为其准备相应的缓冲区、缓冲区管理变量和文件 指针等,还要将文件和一个特定的变量联系起来,这个工作就叫做打开文件。如果应用程序 不再使用某个文件了,就应该及时将其占用的缓冲区等资源释放,这个工作就叫做关闭文件 读:从文件中将数据拷贝到内存变量中来。根据情况不同,一次可以读一个字节,也 可以根据内存变量的大小读相应数量的字节,甚至可以一次将一批数据读到一片连续的存储 区(如数组或动态分配的存储块)中 写:将内存变量中的数据拷贝到文件中去。和读文件的情况相似,一次可以将一个变 量或者一片连续存储区中的数据写入文件 文件指针:由于通常文件中的数据很多,所以在读写时应该指明是对哪些数据进行操 作。在流式文件中采用的方法是设立一个存放文件读写位置的变量,又称文件指针。在开始
第 8 单元 类与对象(II) - 162 - 适合于处理少量数据和信息的输入输出工作,方便、快捷,是最常用的输入输出设备。但是 如果要进行大量数据的加工处理,键盘和显示器的局限就很明显了。通常的做法是利用磁盘 作为数据存放的中介,应用程序的输入模块通过键盘或其他输入设备将数据读入磁盘,处理 模块对存放在磁盘中的数据进行加工,加工后的数据或者仍然存放在磁盘上,以备今后再处 理,或者由输出模块通过打印机等设备以报表等格式输出。对于少量数据(如查询结果等) 也可以直接显示在屏幕上。 在实际应用中,无论是数据库系统、管理信息系统、科学与工程计算,还是文字处理与 办公自动化、图形图象处理,都要处理大量存放在磁盘上的数据。 数据在磁盘中是以文件的方式存放的。由于磁盘的容量一般都很大,如常用的 3 英寸软 盘的容量为 1.44M(1M = 1024K = 220 字节),如果用来存放中文信息,仅一张软盘就可以 存放一部六、七十万字的小说。而现在微机上常用的硬磁盘的容量则更为巨大,一般均在 4.3G~6.8G,有的已超过 10G(1G = 1024M)!在这样大容量的磁盘中,要想按实际地址存 放或者查找一个数据是非常困难的。因此,在计算机中引入了一个叫做文件系统的软件,统 一管理存放在软盘或硬盘中的数据。文件系统是操作系统的一部分,有点象城市里的户籍系 统,通过将数据组织成文件进行管理。 所谓文件,就是逻辑上有联系的一批数据(可以是一批实验数据,或者一篇文章,一幅 图象,甚至一段程序等),有一个文件名作为标识。每个文件在磁盘中的具体存放位置、格 式以及读写等工作都由文件系统管理,对于使用操作系统的用户来说,只需告诉操作系统一 个文件的文件名即可对其进行读、写、删除、拷贝、显示和打印等工作,这是每个和计算机, 和操作系统打过交道的人都非常熟悉的。 在使用C++编写应用程序时也可以通过操作系统来处理以文件形式存放在磁盘上的数 据。操作系统命令一般是将文件作为一个整体来处理的,例如删除文件、拷贝文件等等,而 应用程序往往要求对文件的内容进行处理。由于文件的内容可能千变万化,文件的大小各不 相同,那么以什么为单位处理文件中的数据呢?C++中引入了流式文件(stream)的概念, 即无论文件的内容是什么,一律看成是由字符(或字节)构成的序列,即字符流。流式文件 中的基本单位是字节,磁盘文件和内存变量之间的数据交流以字节为基础。如果实际数据还 有比单纯按字节划分更高级的逻辑结构,可以通过一次读写多个字节来实现。 下面介绍文件处理中的几个基本概念: . 打开和关闭文件:对文件操作前,要为其准备相应的缓冲区、缓冲区管理变量和文件 指针等,还要将文件和一个特定的变量联系起来,这个工作就叫做打开文件。如果应用程序 不再使用某个文件了,就应该及时将其占用的缓冲区等资源释放,这个工作就叫做关闭文件。 . 读:从文件中将数据拷贝到内存变量中来。根据情况不同,一次可以读一个字节,也 可以根据内存变量的大小读相应数量的字节,甚至可以一次将一批数据读到一片连续的存储 区 (如数组或动态分配的存储块) 中; . 写:将内存变量中的数据拷贝到文件中去。和读文件的情况相似,一次可以将一个变 量或者一片连续存储区中的数据写入文件; . 文件指针:由于通常文件中的数据很多,所以在读写时应该指明是对哪些数据进行操 作。在流式文件中采用的方法是设立一个存放文件读写位置的变量,又称文件指针。在开始
第8单元类与对象(I) 163 对某文件进行操作时将文件指针的值设置为0,表示读写操作应从文件首部开始执行:每次 读、写之后,自动将文件指针的值加上本次读、写的字节数,作为下次读写的位置。 缓冲区:由于磁盘的读写速度比内存的处理速度要慢一个数量级,而且磁盘驱动器是 机电设备,定位精度相对比较差,所以磁盘数据存取以扇区( sector,磁盘上某磁道中的 个弧形段,通常存放固定数量的数据。扇区之间有间隙隔开)或者簇( cluster,由若干连续 的扇区组成)为单位。具体做法是在内存中划出一片存储单元,称为缓冲区。从磁盘中读取 数据时先将含有该数据的扇区或簇读到缓冲区中,然后再将具体的数据拷贝到应用程序的变 量中去。下次再读数据时,首先判断数据是否在缓冲区中,如果在,则直接从缓冲区中读 否则就要从磁盘中再读另一个扇区或簇。向磁盘中写数据也是这样,数据总是先写入缓冲区 中,直到缓冲区写满之后再一起送入磁盘。为了能使应用程序同时处理若干个文件,就必需 在内存中开辟多个缓冲区。对缓冲区的管理是操作系统的基本功能之 在C艹程序中对文件的处理由以下步骤组成:打开文件,数据定位,读写数据,关闭 文件。 在MFC中,用CFle类处理文件 CFle类由 CObject类派生,是MFC中所有其他文件类的基类。 磁盘文件在CFle类构建时自动打开,而在析构时自动关闭。使用 CFile类的一些静态 成员函数还可以在不打开文件的情况下获取文件的一些状态。下面介绍CFe类的部分方法 1.构造函数 CFile(Lpctstr lpsz FileName, UINT nOpen Flags) 该函数有两个参数,第1个是要打开的文件的路径,第2个参数是文件存取和共享模式: CFile:: modeCreate 建立一个新文件。如果文件存在,则长度截为0 File:: modeNotruncate同 CFile: moderate,但不将文件长度截为0 CFile:: modeRead 以只读方式打开文件 CFile: modeReadWrite以读写方式打开文件 CFile:: modeWrite 以只写方式打开文件 CFile: typeBinary 在子类中用于二进制模式 CFile: typeText 在子类中用于文本模式 CFile: share Deny None 文件打开时不禁止其它进程写该文件 CFile share deny Read 文件打开时禁止其它进程读该文件 CFle: share Deny Write文件打开时禁止其它进程写该文件 CFile: shareExclusive 文件以独占方式打开时,禁止其它进程读写该文件 一般至少需要一种存取模式和一项共享模式,各选项间以或运算“|”连接。例如: char* fIlenAme =test. dat CFile My File(pFileName, CFile:: mode Create/CFile: mode Write 使用构造函数打开的文件在该CFie对象销毁时自动关闭。 2.文件内容读写
第 8 单元 类与对象(II) - 163 - 对某文件进行操作时将文件指针的值设置为 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. 文件内容读写