上机考核标准 1.程序基本结构和功能60% 2.程序安全性(逻辑错误、内存溢出和测试错误等)20% 3.程序效率(指针、引用、临时对象、算法的时间和空间复杂度等)15% 4. 程序界面5%
第八章 继承与多态 继承(inheritance): 第八章 继承与多态 继承(inheritance): 在自然界中,继承这个概念是非常普遍的。小猫仔继承了猫爸猫妈的特性,当然,子代同时还具有父代没有的特性 该机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。 这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。
多态性(polymorphism): 多态性同名(类)函数之间的调用关系。重载和模板属于编译时的多态性。以虚函数为基础的运行时的多态性是面向对象程序设计的标志性特征。 template<typename T> 模板 运算符重载 多态指同一个实体同时具有多种形式。它是面向对象程序设计(OOP)的一个重要特征。如果一个语言只支持类而不支持多态,只能说明它是基于对象的,而不是面向对象的。C++中的多态性具体体现在运行和编译两个方面。运行时多态是动态多态,其具体引用的对象在运行时才能确定。编译时多态是静态多态,在编译时就可以确定对象使用的形式。 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。 C++中,实现多态有以下方法:虚函数,抽象类,重载,覆盖,模板。 运行时多态 函数重载
第八章 继承与多态 8.1 继承与派生的概念 8.4 虚基类 (选读) 8.2 派生类的构造函数与析构函数 8.5 派生类应用讨论 第八章 继承与多态 8.1 继承与派生的概念 8.4 虚基类 (选读) 8.2 派生类的构造函数与析构函数 8.5 派生类应用讨论 本章重点: 1.继承与派生概念、实现机制、语法和应用 2.继承与派生中的构造函数和析构函数的执行 3.继承中的多态,函数重写、覆盖和调用机制 8.3 多重继承与派生类成员标识 8. 6 多态性与虚函数
8.1 继承与派生的概念 层次概念: 下面的显示了交通工具分类情况: 8.1 继承与派生的概念 层次概念: C++通过继承(inheritance)机制可对类(class)分层,形成基类型(base class)/子类型derived class)的层次结构(hierarchy)。 下面的显示了交通工具分类情况: 如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型(subtype)。
8.1 继承与派生的概念 8.1.1 类的派生与继承 8. 1.2 公有派生与私有派生
8.1.1 类的派生与继承 派生类的概念 priA priA pubA pubA priB pubB class A 8.1.1 类的派生与继承 派生类的概念 class A { public: int pubA; pirvate: int priA; }; class B : public A { public: int pubB; private: int priB; }; priA pubA priA pubA priB pubB 在最简单的情况下,一个类B继承类A或者从类A派生类B,通常将类A称为基类(父类),类B称为派生类(子类)。这时,类B的对象具有类A对象的所有特性,甚至还会更多一些。也可以这样说,类B从类A派生出来。这意味着类B至少描述了与类A同样的接口,至少包含了同类A一样的数据,可以共享类A的成员函数。
8.1.1 类的派生与继承 派生类的定义: class Derived:public Base{ private: double m_d; 8.1.1 类的派生与继承 派生类的定义: class 派生类名:访问限定符 基类名1《,访问限定符 基类名2,……,访问限定符 基类名n》{ 《 private: 成员表1;》 //派生类增加或替代的私有成员 《public: 成员表2;》 //派生类增加或替代的公有成员 《protected: 成员表3;》 //派生类增加或替代的保护成员 };//分号不可少 其中基类1,基类2,……是已定义(非声明,课本说法有出入)的类。 在派生类定义的类体中给出的成员称为派生类成员,它们是新增加成员,它们给派生类添加了不同于基类的新的属性和功能。派生类成员也包括取代基类成员的更新成员。 class Base{ public: void fun(); protected: int m_i; private: string m_s; }; class Derived:public Base{ private: double m_d; public: void test(); }; 派生反映了事物之间的联系,事物的共性与个性之间的关系。 派生与独立设计若干相关的类,前者工作量少,重复的部分可以从基类继承来,不需要单独编程。 继承就是从先辈处得到属性和行为特征。类的继承,是新的类从已有的类获取已有的特征;换个说法就是从已有类产生新类的过程就是派生。类的派生实际是通过扩展、更改和特殊化,从一个已知类建立一个新类的过程。通过类的派生,可以建立具有共通关键特征的对象家族,从而实现父类代码的重用,这种继承和派生机制对于已有程序的发展和改进是极为有力的。可以描述基类和派生类的关系为:派生类是基类的具体化,基类是派生类的抽象。即基类综合了派生类的公共特征,派生类则在基类的基础上增加了某些特征,把抽象类变成具体的、实用的类型。 protected: private 和public 的混合: 1. 不能在类的外面访问; 2. 可以在派生类内部访问 void Derived::test(){ m_i=2;// ok m_s="blabla";//error } void init(){ Base b; b.m_i=2;//error }
单继承:派生类只有一个直接基类的情况称为单继承(single-inheritance)。 8.1.1 类的派生与继承 多重继承:如果一个派生类可以同时有多个基类,称为多重继承(multiple-inheritance),这时的派生类同时得到了多个已有类的特征。 单继承:派生类只有一个直接基类的情况称为单继承(single-inheritance)。 一个基类可以直接派生出多个派生类 派生类可以由多个基类共同派生出来,称多重继承。 基类1 基类2 …… 基类n 派生类1 派生类2 基类 派生类1 派生类2 我们始终接触的单个类的继承,但是在现实生活中,一些新事物往往会拥有两个或者两个以上事物的属性,为了解决这个问题,C++引入了多重继承的概念,C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。 (a)多重继承 (b)单继承 图8.1 多重继承与单继承
8.1.1 类的派生与继承 多层次继承: 直接参与派生出某类称为直接基类,而基类的基类,以及更深层的基类称为间接基类。
8.1.1 类的派生与继承 派生编程步骤: 1.吸收基类的成员 编制派生类时可分四步 2.发展新成员 3.改造基类成员 8.1.1 类的派生与继承 派生编程步骤: 1.吸收基类的成员 不论是数据成员,还是函数成员,除构造函数与析构函数外全盘接收 编制派生类时可分四步 派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。 2.发展新成员 派生类构造的一般过程,经验性的规则。 注意第三步, 通常利用虚函数的技术 声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为同名覆盖(overriding) 3.改造基类成员 4.重写构造函数与析构函数
8.1.1 类的派生与继承 第二步中,独有的新成员才是继承与派生的核心特征。 第三步中,新成员如是成员函数,参数表也必须一样,否则是重载。 8.1.1 类的派生与继承 第二步中,独有的新成员才是继承与派生的核心特征。 第三步中,新成员如是成员函数,参数表也必须一样,否则是重载。 第四步是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用一律重写可免出错。
8.1.1 类的派生与继承 派生与继承具体例子, person, Baseball player, Employee, supervisor. 继承增加成员包括数据成员(属性)和函数成员(在新的数据上的新的操作)
成员函数重写(overriding) 有时需要重写函数成员,对象变了,相应的操作也应该更改,如上例。注意Overloading和overriding区别。 Overriding基本别虚函数代替
成员函数重载Overloading
8.1.1 类的派生与继承 class Base{ ... ... }; class Derived: public Base{ 8.1.1 类的派生与继承 继承方式,是对基类成员访问的进一步的限制,有三种方式: 公有(public)方式,亦称公有继承 保护(protected)方式,亦称保护继承 私有(private)方式, 亦称私有继承。 派生类继承基类有三种方式,对派生类访问基类成员的限制 class Base{ ... ... }; class Derived: public Base{ protected private
8.1.1 类的派生与继承 基类的private成员:只有基类和基类的友元能够访问,派生类及其他地方无权访问 8.1.1 类的派生与继承 基类的private成员:只有基类和基类的友元能够访问,派生类及其他地方无权访问 基类的public和protected成员:派生类可直接访问 公有继承:基类的public为派生类public成员,基类的protected为派生类的protected成员 受保护继承:基类的public和protected为派生类protected成员 私有继承:基类的public和protected为派生类private成员 从基类的成员访问控制标识符角度来分析解释
8.1.2 公有派生与私有派生 不可直接访问 private protected public 私有派生 可直接访问 公有派生 访问限定符两方面含义:派生类成员(新增成员)函数对基类(继承来的)成员的访问(调用和操作),和从派生类对象之外对派生类对象中的基类成员的访问。 不可直接访问 private protected public 私有派生 可直接访问 公有派生 在派生类对象外访问派生类对象的基类成员 在派生类中对基类成员的访问限定 基类中的访问限定 派生方式 (1)public(公有继承):继承时保持基类中各成员属性不变,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象只能访问基类中的public成员。 (2)private(私有继承):继承时基类中各成员属性均变为private,并且基类中private成员被隐藏。派生类的成员也只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。 (3)protected(保护性继承):继承时基类中各成员属性均变为protected,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。 公有派生是绝对主流。
通过具体实例解释各种访问控制的区别,加深同学们的记忆和理解
IS-A 还是HAS-A IS-A:表示的是属于得关系。比如兔子属于一种动物(继承关系)。 假设有两个类:Computer和Employee。明显地,这两个类之间不存在"is a"的关系,即Employee不是计算机,它们之间没有继承关系的必要。因此不可能产生代码重用性。但这两个类之间是"has a"关系,即是支持的关系。例如,Employee"has a"Computer。明显地是一种支持关系。这种支持关系落实到代码中,就是在Employee中创建Computer的对象,调用其方法,到达完成某种运算和操作的目的。 Employee和Manager 类存在的则是"is a"关系,即Manager是Employee。它们之间存在共性,或者共同的属性。Manager是Employee的具体化;Employee是Manager的概括和抽象。概括性和抽象性的类,如Employee,在继承中则定义为超类。具体或代表对象特性的类,如Manager,则定义为子类。如果这是一个用来计算雇员工资的程序,那么在超类Employee中,我们应当包括所有子类都应该具有的、与计算工资有关的数据,例如name、employeeID、jobTitle、seniority、baseSalary以及用来计算基本工资部分的方法,如baseSalary()等。在Manager这个子类中,我们不仅继承Employee的所有数据和方法,还增加针对Manager的新的数据,如是否董事会成员boardMember、职务补贴merit等,因为除基本工资的计算之外,这些都影响到具有经理职务雇员的收入。
what's the value of ep->m_IEmployeeID? 转换与继承 规则:派生类对象的引用或指针可以自动转换为基类对象的引用或指针。但没有基类对象的引用或指针到派生类对象引用或指针的 自动转换。 Employee employee,*ep; Person person(employee); //ok Person(const Person &per); employee=person;//error ep=&person; //error 子类对象可以强制转换为父类或者赋值给父类,按父类来引用子类一般是正确的,这种转换叫做向上强制类型转换。如果说这种转换或者是赋值是直接在对象上进行操作的,那么会发生切割问题,原因是父类对象没有子类自己定义的附加功能 想一想:为什么? what's the value of ep->m_IEmployeeID?
8.2 派生类的构造函数与析构函数 派生类构造函数的定义: 注意: 派生类名::派生类名(参数列表):基类名1(参数名表1)《,基类名2(参数名表2),……,基类名n(参数名表n)》,《成员对象名1(成员对象参数名表1),……,成员对象名m(成员对象参数名表m)》{ ……//派生类新增成员的初始化; } //所列出的成员对象名全部为新增成员对象的名字 在C++程序员的面试中,经常会出现派生类与基类的构造函数、析构函数的执行顺序。其实这是一个很基本的问题,没有什么难度,只需要记住就OK了 注意: 1.在构造函数的声明中,冒号及冒号以后部分必须略去。 2.所谓不能继承并不是不能利用,而是把基类的构造函数作为新的构造函数的一部分,或者讲调用基类的构造函数。基类名仅指直接基类,写了底层基类,编译器认为出错。 3.冒号后的基类名,成员对象名的次序可以随意,这里的次序与调用次序无关。
8.2 派生类的构造函数与析构函数 派生类构造函数各部分执行次序: 注意: 1.调用基类构造函数,按它们在派生类声明的先后顺序,顺序调用。 2.调用成员对象的构造函数,按它们在类定义中声明的先后顺序,顺序调用。 3.派生类的构造函数体中的操作。 class D:public B1,public B2{}; D d; 派生类和基类的构造函数析构函数调用次序, 重点 注意: 在派生类构造函数中,只要基类不是使用无参的默认构造函数都要显式给出基类名和参数表。 如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的默认构造函数。 如果基类定义了带有形参表的构造函数时,派生类就应当定义构造函数。
8.2 派生类的构造函数与析构函数 析构函数: 析构函数的功能是作善后工作。 8.2 派生类的构造函数与析构函数 析构函数: 析构函数的功能是作善后工作。 只要在函数体内把派生类新增一般成员处理好就可以了,而对新增的成员对象和基类的善后工作,系统会自己调用成员对象和基类的析构函数来完成。 析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构。
通过这个例子强化派生类基类构造函数析构函数声明定义规则和执行顺序
8.3 多重继承与派生类成员标识(选读) 教职工::教职工编号 唯一标识问题: 恐怖的钻石问题 通常采用作用域分辨符“::”: 8.3 多重继承与派生类成员标识(选读) 歧义性问题: 比如行政人员兼教师,在其基类教师中有一个“教职工编号”,另一基类行政人员中也有一个“教职工编号”,如果只讲教职工编号那么是哪一个基类中的呢?这两者可能是一回事,但计算机系统并不这么认为。 进一步,如果“教职工编号” 是由两个基类“教师”和“行政人员”共同的基类“教职工”类继承来的,只有同一个标识符,也不能用改标识符来区分。 恐怖的钻石问题 重点理解多重继承带来的钻石问题:基类成员通过两条不同途径继承到派生类,在派生类中就会有两个基类成员的拷贝,从而造成二义性问题。所以在类的设计过程中尽量避免这种问题的产生。 教职工::教职工编号 唯一标识问题: 通常采用作用域分辨符“::”: 基类名::成员名; //数据成员 基类名::成员名(参数表); //函数成员
派生类成员标识 被屏蔽的基类成员函数可通过类名+作用域标识符(::)来访问。 显式覆盖
8.3 多重继承与派生类成员标识(选读) 多重继承实例: 8.3 多重继承与派生类成员标识(选读) 多重继承实例: 由多个基类共同派生出新的派生类,这样的继承结构被称为多重继承或多继承(multiple-inheritance) 椅子 床 沙发(单继承) 躺椅(多重继承) 两用沙发(多重继承) 图8.2 椅子,床到两用沙发 多重继承存在的意义,往往是功能的叠加,但要注意钻石问题。
含义:右操作数的名字可以在左类型名的作用域中找到 成员操作符. 对象.成员名字(对象相关联的类的作用域中) class Person int No: 身份证号 ………. class Person int No: 身份证号 ………. 定义EGStudent类对象EGStudent1,并假定派生全部为公有派生,而int No全为公有成员: EGStud1.No EGStud1.GStudent::No EGStud1.GStudent::Student::No EGStud1.GStudent::Student::Person::No EGStud1.Employee::No EGStud1.Employee::Person::No 注意:课本的这段文字描述有很多问题 作用域操作符 ::的使用 namespace::name 含义:右操作数的名字可以在左类型名的作用域中找到 成员操作符. 对象.成员名字(对象相关联的类的作用域中) class Student int No int sid:学生号 ………. class Employee int No int eid:工作证号 ………. 指出课本关于作用域操作符使用的错误。 class GStudent int No int sid:学生号 ………. 两个身份证号从逻辑上讲应是一回事,但是物理上是分配了不同内存空间,是两个变量,请参见下图8.4(b)。 class EGStudent int No 学生号 工作证号 ……… 图8.4(a)在职研究生派生类关系
8.3 多重继承与派生类成员标识(选读) 注意: 一般数据成员总是私有成员,访问身份证号,应通过class Person中的公有成员函数(接口)GetNo()和SetNo()进行: EGStud1.Employee::Person::SetNo(int no); no=EGStud1.Employee::Person::GetNo(); 课本错误:作用域分辨符不能嵌套使用 如: EGStud1.GStudent::Student::No //学生号 EGStud1.GStudent::Student::Person::No //身份证号
【例8.2】由圆和高多重继承派生出圆锥 【例8.2】由圆和高多重继承派生出圆锥。 圆类Circle定义 高类Line定义 圆锥类Cone定义 因为公有派生时,在派生类中不可以直接访问基类的私有成员,但可以直接访问基类的保护成员,当需要在派生类中访问基类的数据成员时,可以将它们定义为保护的,而不是私有的。 圆类Circle定义 高类Line定义 圆锥类Cone定义 检证主程序:
8.6 多态性与虚函数 多态性: 编译时的多态性 在C++中有两种多态性 8.6 多态性与虚函数 多态性: 多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。 通过函数的重载和运算符的重载来实现的。 编译时的多态性 在C++中有两种多态性 多态的概念 运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定。它是通过类继承关系和虚函数来实现的。目的也是建立一种通用的程序。通用性是程序追求的主要目标之一。 运行时的多态性
8.6 多态性与虚函数 8.6.1 虚函数的定义 8.6.2 纯虚函数 8.6.3 继承与多态的应用—— 单链表派生类(选读) 8.6 多态性与虚函数 8.6.1 虚函数的定义 8.6.2 纯虚函数 8.6.3 继承与多态的应用—— 单链表派生类(选读) 8.6.4 动态绑定 (选读)
8.6.1 虚函数的定义 虚函数的概念: virtual 返回类型 函数名(参数表){…}; 虚函数是一个类的成员函数,定义格式如下: 8.6.1 虚函数的定义 虚函数的概念: 虚函数是一个类的成员函数,定义格式如下: virtual 返回类型 函数名(参数表){…}; 关键字virtual指明该成员函数为虚函数。virtual仅用于成员函数的声明中,成员函数类外定义不可再加virtual。 虚函数的语法,基类中的虚函数在派生类中的虚函数特性 当一个类的某个成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。
例子,通过基类指针触发实现多态特性,并解释为什么需要虚函数,虚函数功能
8.6.1 虚函数的定义 虚函数定义要点: 当在派生类中重新定义虚函数(overriding a virtual function,亦译作超载或覆盖)时,不必加关键字virtual。但重新定义时不仅要同名,而且它的参数表和返回类型(不全是)全部与基类中的虚函数一样,否则出错。 强调虚函数定义的语法, 函数的三要素要完全一致,否则是重载,构成同名覆盖。 虚函数返回类型如果是类类型指针或引用,可以构成多态 虚函数与在8.1.1节中介绍的派生类的第二步——改造类成员,同名覆盖(override)有关:如未加关键字virtual,则是普通的派生类中的新成员函数覆盖基类同名成员函数(当然参数表必须一样,否则是重载),可称为同名覆盖函数,它不能实现运行时的多态性。
举例子强调上述注意
触发动态绑定的两个要点: 1. 只有指定为虚函数的成员才能进行动态绑定,成员函数默认为非虚函数,非虚函数不能进行动态绑定; 2. 必须通过基类类型的引用或指针进行函数调用; 必须搞懂触发多态的两个要点,缺一不可
8.6.1 虚函数的定义 虚函数与运行时的多态性: 【例8.6】计算学分。可由本科生类派生出研究生类,但它们各自的从课程学时数折算为学分数的算法是不同的,本科生是16个学时一学分,而研究生是20个学时一学分。 课本例子 【例8.7】计算学分。派生类定义不再重复。
8.6.1 虚函数的定义 成员函数设置为虚函数的要点: 8.6.1 虚函数的定义 成员函数设置为虚函数的要点: 1. 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。如基类中返回基类指针(或引用),派生类中返回派生类指针(或引用)是允许的,这是一个例外。 2. 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。 3. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。 4. 一个类对象的静态和动态构造是相同的,实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。 静态类型:在编译时可知的引用类型或指针类型 class Base; Base *p; //指针p的静态类型为Base 动态类型:指针或引用所绑定的对象类型,仅运行时可知 class Derived:public Base; Derived d; Base *p=&d; //指针p的动态类型为Derived 什么样的成员函数可以声明为虚函数
8.6.1 虚函数的定义 5.内联函数因为每个对象有独立的一份函数代码,无映射关系,不能作为虚函数。 8.6.1 虚函数的定义 5.内联函数因为每个对象有独立的一份函数代码,无映射关系,不能作为虚函数。 6. 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。 7. 函数执行速度要稍慢一些。为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标(参考 Item24 More Effective C++)。 8. 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。 举例说明基类的析构函数声明为虚函数的必要性 //只调用shape类析构函数
8.6.1 虚函数的定义 9. 虚函数的基类版本和派生类版本如果带有默认值的形参,其默认值必须相同,否则出现异常 10.在派生类虚函数中如果调用基类版本,必须利用作用域标识符,否则,调用自身,导致无穷递归 11. 成员函数为模板,不能声明为virtual 调用自身,无穷递归 fun(i);
8.6.2 纯虚函数 抽象类:不能用来创建对象的类,如下面Shape 类 纯虚函数存在的意义
纯虚函数:(pure virtual function) 纯虚函数的定义: 是指被标明为不具体实现的虚拟成员函数。 纯虚函数的定义: virtual 返回类型 函数名(参数表)=0; 含有一个或多个纯虚函数的类是抽象类。 它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类 含有纯虚函数的基类是不能用来定义对象的。纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类是抽象类。
8.6.2 纯虚函数 定义纯虚函数的要点: 1 声明纯虚函数时,不能在类内部定义虚函数的实现部分,在类外部可以给出函数定义。 8.6.2 纯虚函数 定义纯虚函数的要点: 定义纯虚函数需要注意的地方 1 声明纯虚函数时,不能在类内部定义虚函数的实现部分,在类外部可以给出函数定义。 2 “=0”必须放在成员函数的声明语句中。 3 在派生类中必须有重新定义的纯虚函数的函数体,这样的派生类才能用来定义对象。
例子讲解
8.6.2 纯虚函数 【例8.8】学校对在册人员进行奖励,依据是业绩分,但是业绩分的计算方法只能对具体人员进行,如学生,教师,行政人员,工人,算法都不同,所以可以将在册人员类作为一个抽象类,业绩计算方法作为一个纯虚函数。 在主函数中全部用指向基类的指针来调用 业绩分基类定义 业绩分学生派生类定义 业绩分教师派生类定义 验证主函数
return Base::mem;//return Base::mem; 基类和派生类作用域 与基类同名的派生类数据成员将屏蔽对基类的直接访问 class Base{ protected: int mem; }; class Derived: public Base{ public: int getMem(){ return mem;// return Derived::mem; } protected: int mem; 基类和派生类作用域相关知识点:注意同名覆盖规则 return Base::mem;//return Base::mem; 建议:避免与基类成员名字相冲突
基类和派生类作用域 在派生类中,与基类同名的成员函数屏蔽其基类成员函数,即使函数原型不同,基类成员也会被屏蔽。 struct Base{ void fun(); }; struct Derived: Base{ void fun(int);//屏蔽基类fun() Derived d;Base b; b.fun();//ok, calls Base::fun d.fun(10);//ok, calls Derived::fun d.fun();//error,基类无参fun被屏蔽 d.Base::fun();//ok, calls Base::fun struct Derived: Base{ using Base::fun; void fun(int);//屏蔽基类fun() }; d.fun();//ok,calls Base::fun d.fun(10);//ok, calls Derived::fun
基类和派生类作用域 以上屏蔽原则同样适用于虚函数成员 struct Base{ virtual int fun();// virtual function }; struct D1: Base{ int fun(int);//非虚函数,基类fun函数被屏蔽 struct D2: D1{ int fun(int);//非虚函数,D1::fun()被屏蔽 int fun();//虚函数,重写基类fun }; D1 d1;D2 d2; Base b;Base *p; p=&b;p->fun(); //ok, calls Base::fun p=&d1; p->fun();// ok , calls Base::fun d1.fun();//error, Base::fun被屏蔽,无法访问 p=&d2; p->fun();// ok,calls D2::fun
派生类与基类: struct Base{int i;}; struct Derived: Base{ int j;}; 在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则称赋值兼容规则。它包括以下情况: struct Base{int i;}; struct Derived: Base{ int j;}; Derived d; Base b; b=d; //ok d=b; //error Base *p=&d; p->i;//ok p->j;//error Base &rb=d; rb.i=0;//ok rb.j=0;//error 1. 派生类的对象可以赋值给基类的对象,这时是把派生类对象中 从对应基类中继承来的成员赋值给基类对象。反过来不行,因为派生类的新成员无值可赋。 派生类对象和基类对象赋值规则 2. 可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的成员,不能访问派生类中的新成员。同样也不能反过来做。 3. 派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的由基类继承来的成员。
如何确定继承中的函数调用 1.首先确定函数调用的对象、指针或引用的静态类型 2.在该类中查找函数,如果找不到,在其直接基类中查找,以此沿着继承链向上查找,直到找到为止。 否则调用出错。 3.一旦找到,进行常规类型检查,判断函数调用是否合法 4.如果合法,查看该函数是否是虚函数且通过指针或引用调用,确定动态绑定版本。
8.5 派生类应用讨论 二、再谈继承与聚合 两个类A 和B的关系:继承、聚合或相互独立, 如何确定A和B的关系? 8.5 派生类应用讨论 二、再谈继承与聚合 继承使派生类可以利用基类的成员,如果我们把基类的对象作为一个新类的对象成员,也可以取得类似的效果。派生类采用继承方法,成员对象是聚合的概念。 两个类A 和B的关系:继承、聚合或相互独立, 如何确定A和B的关系? 经验准则: IS-A 还是 HAS-A 若B IS-A A, 类B应该是A的子类,即: class B:public A{}; class Cloth; class Trousers: public Cloth; class Pants: public Trousers; 若B HAS-A A,类B应该包含类A ,即: class B{ A a;}; class Color; class Cloth{ Color clr;} 更深入地探讨后会发现:成员对象体现了封装更深层次的含义。在派生类和它的基类中是不应该有内存的动态分配的,动态分配的部分应该封装在成员对象中,在该成员对象的析构函数中释放内存,在该成员对象中提供深复制。类string就是如此。它的内部就是一个完备的小系统。这样程序员就可以放心地使用它,而不需要为它做任何事情。
二、再谈继承与聚合 想一想合理的设计? 符合自然逻辑和事实 class Cat{ m_fur; void purr(); }; class Dog{ m_fur; void bark(); }; class Dog:public Cat{ //IS-A void bark(); }; Dog dog; dog.bark(); // 但是dog.purr(); 怎么办 继承和聚合的使用原则 class Dog{ //HAS-A Cat m_cat; void bark(); }; Dog dog; dog.bark(); // dog不会purr()了(dog.purr();X), 不符合逻辑
合理的类设计 class Mammal{ m_fur; virtual void sounding()=0; }; class Cat: public Mammal{ void purr(); void sounding(){purr();} }; class Dog:public Mammal{ void bark(); void sounding(){bark();} }; Dog dog; dog.sounding(); //dog 正常汪汪叫了 Cat cat; cat.sounding(); // cat也正常打呼噜了
IS-A:正方形是长方形 class Square:public Rectangle; Square s; g(s); cout<<s.GetWidth(); cout<<s.GetHeight(); void g(Rectangle& r) { r.SetWidth(5); r.SetHeight(4); assert(r.GetWidth() * r.GetHeight()) == 20); } 建议:基类的操作不影响派生类,换句话说基类的设计不必考虑派生类。
虚继承 恐怖的钻石问题
虚继承
本章作业 设计如下类,其功能和部分成员如下: Object: 抽象类,所有的物体都有价值(profit)属性;Point: 点的位置三维空间;Line Segment(线段),Rectangle,Cuboid, Square,Cube,Circle,Cylinder 功能:能够实现上述物体的移动(move),放大(zoomin),缩小(zoomout),大小比较(compare),打印物品信息(cout<<编号、面积、容积和价值)等操作,且所有物品的对象实现自动编号。 移动: Line类对象移动其中点,Rectangle、Square和Circle: 移动重心,Cubiod、Cube和Cylinder: 移动底面重心 放大和缩小:以倍数为参数,进行相应组件的放大和缩小 判断:空间内某一点(Point)是否在另一物体内;线段(Line)是否和另一物体相交 默认比较方式:Line: 比较长度,Rectangle、Square和Circle:比较面积 Cubiod、Cube和Cylinder: 比较体积。同维度(或不同维度)空间内的不同类物体之间可进行大小比较。 相等返回0,小于返回-1、大于返回1
本章作业 再设计一个容器类(Container). 容器具有最大容量属性。 功能: 能容纳以上定义的各种3D物品(Cylinder,Cube和Cuboid), 实现添加一个物品(add),移除容器里的一个物品(remove),重载[], 排序:不改变物品在容器中的位置(下标),把物品的id按照排序结果(根据物品某一关键字)返回 ; 附加功能:给定一定数量的物品(假设物品的总容量超过容器最大容量),挑选部分装入容器中,设计算法实现所装物品的总价值最大。
背包问题 测试数据 背包容量:6404180 物品大小(24个 ):382745 799601 909247 729069 467902 44328 34610 698150 823460 903959 853665 551830 610856 670702 488960 951111 323046 446298 931161 31385 496951 264724 224916 169684 物品价值: 825594 1677009 1676628 1523970 943972 97426 69666 1296457 1679693 1902996 1844992 1049289 1252836 1319836 953277 2067538 675367 853655 1826027 65731 901489 577243 466257 369261 最优解:13549094: 1 1 0 1 1 1 0 0 0 1 1 0 1 0 0 1 0 0 0 0 0 1 1 1 其他测试数据 https://people.sc.fsu.edu/~jburkardt/datasets/knapsack_01/knapsack_01.html
第八章 继承与派生 完 谢谢!
【例8.1】由在册人员类公有派生学生类 分析构造函数: Person::Person(string id, string name,Tsex sex,int birthday, string homeadd){ IdPerson=id; Name=name; Sex=sex; Birthday=birthday; HomeAddress=homeadd; } //作为一个管理程序,这个构造函数并无必要,因为数据总是另外输入的。仅为说明语法存在。 Person::Person(string id, string name,Tsex sex,int birthday, string homeadd): IdPerson(id), Name(name),Sex(sex),Birthday(birthday),HomeAddress(homeadd){ }
【例8.1】由在册人员类公有派生学生类 分析默认的构造函数: Person::Person(){ IdPerson="#";Name="#";Sex=mid; Birthday=0;HomeAddress="#"; } 分析析构函数: Person::~Person(){} //string内部动态数组的释放,由string自带的析构函数完成
【例8.1】由在册人员类公有派生学生类 void Person::SetName(string name){ 修改名字: void Person::SetName(string name){ Name=name; //拷入新姓名 } 修改住址: void Person::SetHomeAdd(string homeadd){ HomeAddress=homeadd; }
【例8.1】由在册人员类公有派生学生类 输出个人信息: void Person::PrintPersonInfo(){ int i; cout<<"身份证号:"<<IdPerson<<'\n'<<"姓名:" <<Name<<'\n'<<"性别:"; if(Sex==man)cout<<"男"<<'\n'; else if(Sex==woman)cout<<"女"<<'\n'; else cout<<" "<<'\n'; cout<<"出生年月日:"; i=Birthday; cout<<i/10000<<"年"; i=i%10000; cout<<i/100<<"月"<<i%100<<"日"<<'\n‘ <<"家庭住址:"<<HomeAddress<<'\n'; }
【例8.1】由在册人员类公有派生学生类 派生类构造函数: Student::Student(string id, string name,Tsex sex,int birthday, string homeadd, string nostud) :Person(id,name,sex,birthday,homeadd){ //注意Person参数表不用类型 int i; NoStudent=nostud; for(i=0;i<30;i++){ //课程与成绩清空 cs[i].coursename="#"; cs[i].grade=0; }
【例8.1】由在册人员类公有派生学生类 默认派生类构造函数: 派生类析构函数: Student::Student(){//基类默认的无参数构造函数不必显式给出 int i; NoStudent=“#"; for(i=0;i<30;i++){ //课程与成绩清空,将来由键盘输入 cs[i].coursename=“#"; cs[i].grade=0; } 派生类析构函数: Student::~Student(){} //基类析构函数以及成员对象析构函数自动调用
【例8.1】由在册人员类公有派生学生类 学生类课程设置函数: int Student::SetCourse(string coursename,int grade){ bool b=false; //标识新输入的课程,还是更新成绩 int i; for(i=0;i<30;i++){ if(cs[i].coursename=="#"){ //判表是否进入未使用部分(如有对应删除,应按顺序表方式) cs[i].coursename=coursename; cs[i].grade=grade; b=false;break; } else if(cs[i].coursename==coursename){ //是否已有该课程记录 cs[i].grade=grade;b=true;break;}} if(i==30) return 0; //成绩表满返回0 if(b) return 1; //修改成绩返回1 else return 2; //登记成绩返回2 }
【例8.1】由在册人员类公有派生学生类 查找学生课程成绩函数: int Student::GetCourse(string coursename){ int i; for(i=0;i<30;i++) if(cs[i].coursename==coursename) return cs[i].grade; return -1; } //找到返回成绩,未找到返回-1
【例8.1】由在册人员类公有派生学生类 打印学生情况函数: void Student::PrintStudentInfo(){ int i; cout<<"学号:"<<NoStudent<<'\n'; PrintPersonInfo(); for(i=0;i<30;i++) //打印各科成绩 if(cs[i].coursename!="#") cout<<cs[i].coursename <<'\t'<<cs[i].grade<<'\n'; else break; cout<<"--------完-------- "<<endl; }
例8.1验证用主函数: int main(void){ char temp[30]; int i,k; Person per1("320102820818161","沈俊", man,19820818,"南京四牌楼2号"); Person per2; per2.SetName("朱明"); per2.SetSex(woman); per2.SetBirth(19780528); per2.SetId("320102780528162"); per2.SetHomeAdd("南京市成贤街9号"); per1.PrintPersonInfo(); per2.PrintPersonInfo(); Student stu1("320102811226161","朱海鹏", man,19811226,"南京市黄浦路1号","06000123");
cout<<"请输入各科成绩:"<<'\n'; //完整的程序应输入学号,查找,再操作 while(1){ //输入各科成绩,输入"end"停止 cin>>temp; //输入格式:物理 80 if(!strcmp(temp,"end")) break; cin>>k; i=stu1.SetCourse(temp,k); if(i==0)cout<<"成绩列表已满!"<<'\n'; else if(i==1)cout<<"修改成绩"<<'\n'; else cout<<"登记成绩"<<'\n'; } stu1.PrintStudentInfo(); while(1){ cout<<"查询成绩"<<'\n'<<"请输入科目:"<<'\n'; cin>>temp; k=stu1.GetCourse(temp); if(k==-1)cout<<"未查到"<<'\n'; else cout<<k<<'\n'; } return 0;}
Circle(float a=0,float b=0,float R=0){x=a;y=b;r=R;} 【例8.2】由圆和高多重继承派生出圆锥 class Circle{ protected: float x,y,r; //(x,y)为圆心,r为半径 public: Circle(float a=0,float b=0,float R=0){x=a;y=b;r=R;} void Setcoordinate(float a,float b){x=a;y=b;} //设置圆心坐标 void Getcoordinate(float &a,float &b){a=x;b=y;} void SetR(float R){r=R;} //设置半径 float GetR(){return r;} //取圆半径 float GetAreaCircle(){return float(r*r*3.14159);} //取圆面积 float GetCircumference(){return float(2*r*3.14159);} //取圆周长};
【例8.2】由圆和高多重继承派生出圆锥 高类Line: class Line{ protected: float High; public: Line(float a=0){High=a;} void SetHigh(float a){High=a;} float GetHigh(){return High;} };
【例8.2】由圆和高多重继承派生出圆锥 派生类圆锥: class Cone:public Circle,public Line{ Cone(float a,float b,float R,float d):Circle(a,b,R),Line(d){} float GetCV(){return float(GetAreaCircle()*High/3);} //取得圆锥体积 float GetCA(){ //取得圆锥表面积 return float(GetAreaCircle()+r*3.14159*sqrt(r*r+High*Hgih)); }; //共有派生类中能直接访问直接基类的保护成员 在VC++平台上运行例8.2
【例8.2】由圆和高多重继承派生出圆锥 检证主程序: void main(){ Cone c1(5,8,3,4); float a,b; cout<<"圆锥体积:"<<c1.GetCV()<<'\n'; cout<<"圆锥表面积:"<<c1.GetCA()<<'\n'; cout<<"圆锥底面积:"<<c1.GetAreaCircle()<<'\n'; cout<<"圆锥底周长:"<<c1.GetCircumference()<<'\n'; cout<<"圆锥底半径:"<<c1.GetR()<<'\n'; c1.Getcoordinate(a,b); cout<<"圆锥底圆心坐标:("<<a<<','<<b<<")\n"; cout<<"圆锥高:"<<c1.GetHigh()<<'\n'; }
[例8.4] 虚基类与在职研究生 以虚基类定义公有派生的学生类: class Student:public virtual Person{ [例8.4] 虚基类与在职研究生 以虚基类定义公有派生的学生类: class Student:public virtual Person{ string NoStudent; //学号 //30门课程与成绩略 public: Student(string id, string name,Tsex sex,int birthday, string homeadd, string nostud); Student(); ~Student(){cout<<"析构Student"<<endl;} void PrintStudentInfo(); }; Student::Student(string id, string name,Tsex sex, int birthday, string homeadd, string nostud) :Person(id,name,sex,birthday,homeadd){ //注意Person参数名表不用类型 cout<<"构造Student"<<endl; NoStudent=nostud;}
[例8.4] 虚基类与在职研究生 以虚基类定义公有派生的研究生类: [例8.4] 虚基类与在职研究生 以虚基类定义公有派生的研究生类: class GStudent:public Student{ //以虚基类定义公有派生的研究生类 string NoGStudent; //研究生号,其他略 public: GStudent(string id, string name,Tsex sex,int birthday, string homeadd, string nostud,string nogstudent); //注意派生类构造函数声明方式 GStudent(); ~GStudent(){cout<<"析构GStudent"<<endl;}; void PrintGStudentInfo();}; GStudent::GStudent(string id, string name,Tsex sex, int birthday, string homeadd, string nostud, string nogstud): Student(id,name,sex,birthday,homeadd,nostud), Person(id,name,sex,birthday,homeadd){ //因Person是虚基类,尽管不是直接基类, Person出现。 //如不是虚拟继承Person类,出现是错误的 cout<<"构造GStudent"<<endl; NoGStudent=nogstud;}
[例8.4] 虚基类与在职研究生 以虚基类定义公有派生的教职工类: [例8.4] 虚基类与在职研究生 以虚基类定义公有派生的教职工类: class Employee:public virtual Person{ string NoEmployee; //教职工号,其他略 public: Employee(string id, string name,Tsex sex,int birthday, string homeadd, string noempl); Employee(); ~Employee(){cout<<"析构Employee"<<endl;} void PrintEmployeeInfo(); void PrintEmployeeInfo1();//多重继承时避免重复打印虚基类Person的信息 }; Employee::Employee(string id, string name,Tsex sex,int birthday, string homeadd, string noempl) :Person(id,name,sex,birthday,homeadd){ cout<<"构造Employee"<<endl; NoEmployee=noempl; }
[例8.4] 虚基类与在职研究生 多重继承的以虚基类定义公有派生的在职研究生类: [例8.4] 虚基类与在职研究生 多重继承的以虚基类定义公有派生的在职研究生类: class EGStudent:public Employee,public GStudent{ string NoEGStudent; //在职学习号,其他略 public: EGStudent(string id, string name,Tsex sex,int birthday,string homeadd, string nostud,string nogstud, string noempl, string noegstud); EGStudent(); ~EGStudent(){cout<<"析构EGStudent"<<endl;}; void PrintEGStudentInfo();}; EGStudent::EGStudent(string id, string name,Tsex sex,int birthday, string homeadd,string nostud, string nogstud, string noempl, string noegstud) :GStudent(id,name,sex,birthday,homeadd,nostud,nogstud), Employee(id,name,sex,birthday,homeadd,noempl), Person(id,name,sex,birthday,homeadd){ //显式调用指定的底层基类的构造函数 cout<<"构造EGStudent"<<endl; NoEGStudent=noegstud;}
[例8.5] 深复制函数 Person和Student复制构造函数如下: 【例8.5】为例8.1自定义复制函数,实现深复制。 [例8.5] 深复制函数 【例8.5】为例8.1自定义复制函数,实现深复制。 Person和Student复制构造函数如下: Person::Person(Person &ps){ IdPerson=ps.IdPerson; Name=ps.Name; Sex=ps.Sex; Birthday=ps.Birthday; HomeAddress=ps.HomeAddress;} Student::Student(Student &Std):Person(Std){ //按赋值兼容规则Std可为Person实参 NoStudent=Std.NoStudent; for(int i=0;i<30;i++){ cs[i].coursename=Std.cs[i].coursename; cs[i].grade=Std.cs[i].grade; }}
Person和Student复制赋值操作符如下: [例8.5] 深复制函数 Person和Student复制赋值操作符如下: Person & Person::operator=(Person &ps){ IdPerson=ps.IdPerson; Name=ps.Name; Sex=ps.Sex; Birthday=ps.Birthday; HomeAddress=ps.HomeAddress; return *this;} Student & Student::operator=(Student &Std){ this->Person::operator=(Std); //注意标准格式 NoStudent=Std.NoStudent; for(int i=0;i<30;i++){ cs[i].coursename=Std.cs[i].coursename; cs[i].grade=Std.cs[i].grade;}
[例8.5] 深复制函数 以上定义的实际上就是默认的按语义的复制构造函数和 复制赋值操作符。可在主程序中增加内容以进行检验: [例8.5] 深复制函数 以上定义的实际上就是默认的按语义的复制构造函数和 复制赋值操作符。可在主程序中增加内容以进行检验: int main(void){ string temp; int i,k; Person per1("320102820818161","沈俊",man,19820818,“ 南京四牌楼2号"); per1.PrintPersonInfo(); Person per2=per1,per3; //基类对象复制初始化 per2.PrintPersonInfo(); per3=per1; //基类对象深复制赋值 per3.PrintPersonInfo(); Student stu1("320102811226161","朱海鹏",man, 19811226,"南京市黄浦路1号","06000123"); cout<<"请输入各科成绩:"<<'\n';
[例8.5] 深复制函数 while(1){ //输入各科成绩,输入"end"停止 [例8.5] 深复制函数 while(1){ //输入各科成绩,输入"end"停止 cin>>temp; //输入格式:物理 80 if(temp=="end") break; cin>>k; i=stu1.SetCourse(temp,k); if(i==0)cout<<"成绩列表已满!"<<'\n'; else if(i==1)cout<<"修改成绩"<<'\n'; else cout<<"登记成绩"<<'\n'; } stu1.PrintStudentInfo(); Student stu2=stu1,stu3; //派生类对象深复制初始化 stu2.PrintStudentInfo(); stu3=stu2; //派生类对象深复制赋值 stu3.PrintStudentInfo(); return 0;} 在VC++平台上运行例8.5,看运行结果。
[例8.6] 虚函数计算学分 基类定义: class Student{ string coursename; //课程名 [例8.6] 虚函数计算学分 基类定义: class Student{ string coursename; //课程名 int classhour; //学时 int credit; //学分,未考虑0.5学分 public: Student(){coursename="#";classhour=0;credit=0;} virtual void Calculate(){credit=classhour/16;} void SetCourse(string str,int hour){ coursename=str; classhour=hour; } int GetHour(){return classhour;} void SetCredit(int cred){credit=cred;} void Print(){ cout<<coursename<<'\t'<<classhour <<"学时"<<'\t'<<credit<<"学分"<<endl; } };
[例8.6] 虚函数计算学分 派生类定义: class GradeStudent:public Student{ public: [例8.6] 虚函数计算学分 派生类定义: class GradeStudent:public Student{ public: GradeStudent(){}; //对基类默认的构造函数不必显式调用 void Calculate(){ SetCredit(GetHour()/20); } };
[例8.6] 虚函数计算学分 int main(){ Student s,*ps; GradeStudent g; [例8.6] 虚函数计算学分 int main(){ Student s,*ps; GradeStudent g; s.SetCourse("物理",80); s.Calculate(); g.SetCourse("物理",80); g.Calculate(); cout<<"本科生:"<<'\t'; s.Print(); cout<<"研究生:"<<'\t'; g.Print(); s.SetCourse("数学",160); g.SetCourse("数学",160); ps=&s; ps->Calculate(); cout<<"本科生:"<<'\t'; ps->Print(); ps=&g; cout<<"研究生:"<<'\t'; return 0}
结果为: 第一行学分是由Student类的成员函数Calculate()计算。 本科生:物理 80学时 5学分 研究生:物理 80学时 4学分 本科生:数学 160学时 10学分 研究生:数学 160学时 8学分 第二行学分是由GradeStudent重新定义的Calculate()计算,它屏蔽了基类的同名函数。 第三行用的是指向Student类的对象s的指针,当然用的是Student类的Calculate()。 指针类型是指向基类的指针,但这里指针指向了派生类GradeStudent的对象g,按赋值兼容规则是准许的,但只能用基类的成员,可实际上用了派生中新定义的Calculate()。这就是虚函数体现的多态性,如果不是虚函数,第四行输出是10学分。
[例8.7]引用,实现运行时的多态性 【例8.7】计算学分。派生类定义不再重复。 void Calfun(Student &ps,string str,int hour){ ps.SetCourse(str,hour); ps.Calculate(); ps.Print();} int main(){ Student s; GradeStudent g; cout<<"本科生:"; Calfun(s,"物理",80); cout<<"研究生:"; Calfun(g,"物理",80); return 0;} 这里没有用指针,而用了Student的引用,同样实现运行时的多态性,加了一个Calfun()函数,使用更为方便。 在VC++平台上运行例8.7,看运行结果。
[例8.8] 业绩分的计算 基类定义: class Person{ int MarkAchieve; string Name; [例8.8] 业绩分的计算 基类定义: class Person{ int MarkAchieve; string Name; public: Person(string name){ Name=name; MarkAchieve=0;} void SetMark(int mark){MarkAchieve=mark;}; virtual void CalMark()=0; //CalMark()为纯虚函数,Person为抽象类 void Print(){ cout<<Name<<"的业绩分为:"<<MarkAchieve<<endl;} };
[例8.8] 业绩分的计算 学生派生类定义: class Student:public Person{ [例8.8] 业绩分的计算 学生派生类定义: class Student:public Person{ int credit,grade; //学历和成绩 public: Student(string name,int cred,int grad) :Person(name){ credit=cred; grade=grad; } void CalMark() {SetMark(credit*grade); } };
例8.8 教师派生类定义: class Teacher:public Person{ 例8.8 教师派生类定义: class Teacher:public Person{ int classhour,studnum; //授课学时和学生人数 public: Teacher(string name,int ch,int sn):Person(name){ classhour=ch; studnum=sn; } void CalMark() { int K=(studnum+15)/30; //工作量系数,30人一班,15人以下不开课 switch(K){ case 1: SetMark(classhour*studnum);break; case 2: SetMark(classhour*(30+(studnum-30)*8/10));break; case 3: SetMark(classhour*(30+24+(studnum-60)*6/10));break; case 4: SetMark(classhour*(30+24+18+(studnum-90)*4/10)); break; case 5: SetMark(classhour*(30+24+12+(studnum-120)*2/10)); default:SetMark(classhour*(30+24+12+6+(studnum-150)*1/10)); } } };
[例8.8] 业绩分的计算 int main(){ Person *pp; Student s1("张成",20,80); [例8.8] 业绩分的计算 int main(){ Person *pp; Student s1("张成",20,80); Teacher t1("范英明",64,125),t2("李凯",80,85); pp=&s1; pp->CalMark(); pp->Print(); pp=&t1; pp=&t2; return 0; }
【例8.9】辛普生法求定积分类: class Simpson{;//Intevalue积分值,a积分下限,b积分上限 double Intevalue,a,b public: virtual double fun(double x)=0; //被积函数声明为纯虚函数 Simpson(double ra=0,double rb=0){a=ra;b=rb;Intevalue=0;} void Integrate(){ double dx; int i; dx=(b-a)/2000; Intevalue=fun(a)+fun(b); for(i=1;i<2000;i+=2) Intevalue+=4*fun(a+dx*i); for(i=2;i<2000;i+=2) Intevalue+=2*fun(a+dx*i); Intevalue*=dx/3; } void Print(){cout<<"积分值="<<Intevalue<<endl;} };
【例8.9】辛普生法求定积分 在派生类中加被积函数: class A:public Simpson{ public: A(double ra,double rb):Simpson(ra,rb){ }; double fun(double x){return sin(x) ;} }; class B:public Simpson{ //B也可以说明为由A派生,更利于说明动态多态性 B(double ra,double rb):Simpson(ra,rb){ }; double fun(double x){return exp(x) ;}
【例8.9】辛普生法求定积分 int main(){ A a1(0.0,3.1415926535/2.0); Simpson *s=&a1; s->Integrate(); //动态 B b1(0.0,1.0); b1.Integrate(); //静态 s->Print(); b1.Print(); return 0; } 在VC++平台上运行例8.9。
【例8.10】通用单链表派生类 Node::Node(){ info=NULL; link=NULL; } Node::~Node(){ cout<<"删除结点类"<<'\t'; delete info; //释放数据域,自动调用数据域类析构函数, //而数据域类对象是在main()中建立 void Node::Linkinfo(Object * obj){info=obj;} //把数据对象连接到结点
【例8.10】通用单链表派生类 List::List(){head=tail=new Node();} List::~List(){ MakeEmpty();cout<<"删除头结点"<<'\t';delete head; } //自动调用结点类析构函数, 因指针域空不再调数据域类析构函数 void List::MakeEmpty(){ Node *tempP; while(head->link!=NULL){ tempP=head->link; head->link=tempP->link; //把头结点后的第一个节点从链中脱离 delete tempP; } //释放该结点,先自动调用结点类的析构函数, //再自动调用数据域类的/析构函数, //不可在前面加 delete tempP->info; 以释放数据域类 tail=head; } //表头指针与表尾指针均指向表头结点,表示空链
【例8.10】通用单链表派生类 Node* List::Find(Object & obj){ //对抽象类只能用“引用” Node* tempP=head->link; while(tempP!=NULL&&*tempP->info!=obj) tempP=tempP->link; return tempP; } //搜索成功返回该结点地址,不成功返回NULL void List::PrintList(){ while(tempP!=NULL){ tempP->info->Print(); //利用数据类的打印虚函数 tempP=tempP->link;} cout<<endl;}
【例8.10】通用单链表派生类 void List::InsertOrder(Node* p){ Node *tempP=head->link,*tempQ=head; //tempQ指向tempP前面的一个节点 while(tempP!=NULL){ if(*tempP->info>*p->info) break; //找第一个比插入结点大的结点,由tempP指向 tempQ=tempP; tempP=tempP->link; } tempQ->InsertAfter(p); //插在tempP指向结点之前,tempQ之后 if(tail==tempQ) tail=tempQ->link;
【例8.10】通用单链表派生类 StringObject::~StringObject(){ cout<<“数据类析构”<<endl; } //自动进一步调用string析构函数 bool StringObject::operator>(Object & obj){ //虚函数 StringObject & temp=(StringObject &)obj; //必须转换 return sptr>temp.sptr; } bool StringObject::operator!=(Object & obj){ //虚函数 return sptr!=temp.sptr; void StringObject::Print(){ //虚函数 cout<<sptr<<'\t';
【例8.10】通用单链表派生类 int main(){ Node * P1; StringObject* p; List list1,list2,list3; char *a[5]={"dog","cat","bear","sheep","ox"},*sp="cat"; int i; for(i=0;i<5;i++){ p=new StringObject(a[i]); //建立数据对象 P1=list1.CreatNode(); //建立结点 P1->Linkinfo(p); //数据对象连接到结点 list1.InsertFront(P1); //向前生成list1 p=new StringObject(a[i]); //将在Node的析构函数中释放 P1=list2.CreatNode(); P1->Linkinfo(p); list2.InsertRear(P1); } //向后生成list2 list1.PrintList(); cout<<"list1长度:"<<list1.Length()<<endl; list2.PrintList(); cout<<"要求删除的字符串\"cat\""<<endl;
【例8.10】通用单链表派生类 p=new StringObject(sp); //为了程序的通用性只能多一次转换 P1=list1.Find(*p); delete p; if(P1!=NULL){ cout<<"删除cat"<<endl; P1=list1.DeleteNode(P1); delete P1; list1.PrintList(); cout<<"list1长度:"<<list1.Length()<<endl;} else cout<<"未找到"<<endl; cout<<"清空list1"<<endl; list1.MakeEmpty(); //清空list1 for(i=0;i<5;i++){ p=new StringObject(a[i]); P1=list3.CreatNode(); P1->Linkinfo(p); list3.InsertOrder(P1); } //升序创建list3 list3.PrintList(); cout<<"程序结束"<<endl; return 0;}
ox sheep bear cat dog //向前生成的链表list1 list1长度:5 dog cat bear sheep ox //向后生成的链表list2 要求删除的字符串"cat" 删除字符串类 //对应delete p; 删除cat 删除结点类 数据类析构 ox sheep bear dog //删除cat后的list1 list1长度:4 清空list1 删除结点类 数据类析构 //ox //删除(释放)该结点,自动调用结点类析构 函数, 进一步自动调用数据域类析构函数 删除结点类 数据类析构 //sheep 删除结点类 数据类析构 // bear 删除结点类 数据类析构 // dog bear cat dog ox sheep //升序创建的链表list3 程序结束 删除结点类 数据类析构 //自动删除链表list3的5个结点:bear 删除结点类 数据类析构 // cat 删除结点类 数据类析构 // dog 删除结点类 数据类析构 // ox 删除结点类 数据类析构 // sheep 删除头结点 删除结点类 //删除链表list3的头结点 //自动删除链表list2的5个结点: dog 删除结点类 数据类析构 // cat 删除结点类 数据类析构 // bear 删除头结点 删除结点类 //删除链表list2的头结点 //删除链表list1的头结点