第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()等的用法与其自己的成员函数用法完 全相同 当派生类和基类中都定义有初始化构造函数时,则可在创建派生类的对象时调用基类中 相应的构造函数来初始化基类中的成员。带有初始化基类成员的派生类初始化构造函数的定 义具有如下的一般形式: <派生类名>::<派生类名〉(<参数表》):<基类1>(<实参表》),,<基类。>(<实参表》) 例如,职员类的构造函数为 #include <string. h> 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()等的用法与其自己的成员函数用法完 全相同。 当派生类和基类中都定义有初始化构造函数时,则可在创建派生类的对象时调用基类中 相应的构造函数来初始化基类中的成员。带有初始化基类成员的派生类初始化构造函数的定 义具有如下的一般形式: <派生类名>::<派生类名>(<参数表>):<基类 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 <类型> <函数名>(<参数表>); 一个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中
第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;