1、第14章多态,-2-,本章内容安排,多态的概念 虚函数实现多态 虚函数机制 纯虚函数与抽象类,-3-,赋值兼容性规则,每一个派生类的对象,都是基类的一个对象。赋值兼容规则是指在公有派生情况下,一个公有派生类的对象可以当作基类的对象使用,反之则禁止。 派生类的对象可以赋值给基类对象。 派生类的对象可以初始化基类的引用。 指向基类的指针也可以指向派生类。 通过基类对象名、指针只能使用从基类继承的成员,-4-,派生类对象赋值给基类对象,Mammal m(3,5); Dog d(2,3,LAB); m=d; 通过m不能访问或间接访问breed成员,-5-,基类对象赋值给派生类对象,Mammal m(2
2、, 3); Dog d(3,4, LAB); d=m;,X,?,-6-,派生类对象初始化基类引用,Dog d(2,3, LAB); Mammal 虽然rp是c的引用,但只能访问基类部分数据和方法,-7-,派生类对象地址赋值基类指针,Dog d(2,3, LAB); Mammal *pd= /基类指针可指向派生类对象 通过pd只能访问基类部分的数据和接口,-8-,Mammal基类,class Mammal public:Mammal() : age(1) cout “Mammal constructor .n“; Mammal() cout “Mammal destructor .n“; voi
3、d speak() const cout “Mammal speak!n“; void move() const cout “Mammal, move one stepn“; protected:int age; ;,-9-,Dog类,class Dog : public Mammal public:Dog() cout “Dog constructor .n“; Dog() cout “Dog destructor n“; void wagTail() cout “Wagging tail .n“; void speak() const cout “Woof!n“; void move()
4、const cout “Dog moves 5 steps .n“; ;,Dog类中重写了speak和move方法,同时增加了wagTail方法。,-10-,测试程序,int main() Mammal *pDog = new Dog;pDog-move();pDog-speak();delete pDog; pDog=NULL;return 0; ,在堆中创建Dog对象,使用Mammal类指针pDog指向它。 因为pDog的类型为Mammal指针类型,会调用Mammal类的move和speak方法。编译时期进行的函数调用绑定。 尽管pDog指向的是Dog对象,但只能调用基类方法。,-11-,
5、一种期望,void speak( Mammal *pAnimal) pAnimal-speak(); int main() Mammal *pDog = new Dog;speak(pDog);Mammal *pCat = new Cat;speak(pCat);delete pDog; pDog=NULL;delete pCat; pCat=NULL;return 0; ,定义通用的处理函数speak,形参指向Mammal的指针,可以接收Dog或Cat对象地址 希望能够调用实际传递对象的speak方法,统一处理不同的对象。,由于编译期静态绑定,导致2次调用的都是基类的speak。,-12-,
6、引入多态,在基类中,将子类中都会存在并需要重写的方法声明为虚函数(virtual),就会引入多态的效果。 定义基类的指针或引用,使其指向或引用基类或派生类的对象,调用虚函数时,系统将调用实际所指向对象的方法。 使用基类指针或引用调用方法,不用关心实际所指向的对象,是一种运行时多态,编译器编译时未做绑定,而是在运行时根据实际对象临时决策。,-13-,多态的变化,class Mammal public:Mammal() : age(1) cout “Mammal constructor .n“; Mammal() cout “Mammal destructor .n“; virtual void
7、speak() const cout “Mammal speak!n“; void move() const cout “Mammal, move one stepn“; protected:int age; ;,通过virtual关键词将speak声明为虚函数。,-14-,多态的变化,class Dog : public Mammal public:Dog() cout “Dog constructor .n“; Dog() cout “Dog destructor n“; void wagTail() cout “Wagging tail .n“; virtual void speak()
8、 const cout “Woof!n“; void move() const cout “Dog moves 5 steps .n“; ;,基类中的speak声明为virtual后,派生类中重写的方法自动成为虚函数,但明确指出是一种良好编程习惯。 从Dog再派生子类时,看Dog头文件,可判断speak为虚函数。,-15-,测试程序,int main() Mammal *pDog = new Dog;pDog-move();pDog-speak();delete pDog; pDog=NULL;return 0; ,在堆中创建Dog对象,使用Mammal类指针pDog指向它。 由于将speak
9、声明为虚函数,通过pDog调用speak时调用的将是Dog类的方法。而move方法没有声明为虚函数,通过pDog只能调用Mammal类的move方法。,-16-,多态的要点,实现多态的3个基本条件 基类中声明虚函数; 子类中重写虚函数,函数签名与基类完全相同; 定义基类指针或引用,通过指针或引用调用虚函数。,-17-,本章内容安排,多态的概念 虚函数实现多态 虚函数机制 纯虚函数与抽象类,-18-,完整示例:Mammal类,class Mammal public:Mammal() : age(1) Mammal() virtual void speak() const cout “Mammal
10、 speak!n“; protected:int age; ;,-19-,完整示例:子类,class Dog : public Mammal public:virtual void speak() const cout “Woof!n“; ; class Cat : public Mammal public:virtual void speak() const cout “Meow!n“; ;,派生类中重写基类的虚函数,并且函数签名完全一致,为多态做好准备。,-20-,完整示例:子类,class Horse : public Mammal public:virtual void speak()
11、 const cout “Whinny!n“; ; class Pig : public Mammal public:virtual void speak() const cout “Oink!n“; ;,-21-,测试程序,int main() Mammal* array5;Mammal* ptr;int choice, i;,array是一个指针数组,含有5个元素,每个元素是1个Mammal类型的指针。,-22-,测试程序,int main() for (i = 0; i choice;switch (choice)case 1: ptr = new Dog; break;case 2:
12、ptr = new Cat; break;case 3: ptr = new Horse; break;case 4: ptr = new Pig; break;default: ptr = new Mammal; break;arrayi = ptr;,在堆中创建对象,使用基类指针指向所创建的对象。,-23-,测试程序,#include int main() for (i=0; i speak();delete arrayi;return 0; ,array是一个指针数组,arrayi是Mammal类型指针,通过其调用speak方法实现多态效果。 不用关心指针实际指向的对象类型,只需要调用s
13、peak方法,系统多态性帮助我们调用正确的方法版本。,-24-,本章内容安排,多态的概念 虚函数实现多态 虚函数机制 纯虚函数与抽象类,-25-,对象的构建(无虚函数),创建派生类对象时,首先调用基类的构造函数,再调用派生类的构造函数。 每个派生类对象有2个部分:基类部分由基类构造函数构建,派生类部分由派生类构造函数构建。,-26-,对象的构建(含虚函数),类中声明虚函数后,多数编译器将为每个类建立虚函数表(v-table),在每个对象中插入1个指针,指向对应的虚函数表。 定义Mammal对象时,vptr指向该类的虚函数表,Mammal类的虚函数表,-27-,对象的构建(含虚函数),定义Dog
14、类对象时,先构建Mammal部分,此时vptr指向基类的虚函数表,再构建Dog部分时,将vptr替换指向Dog类的虚函数表。,Mammal类的虚函数表,Dog类的虚函数表,-28-,多态的要点,实现多态的3个基本条件 基类中声明虚函数; 子类中重写虚函数,函数签名与基类完全相同; 定义基类指针或引用,通过指针或引用调用虚函数。,基类中声明虚函数,子类中没有重写,不产生多态,直接调用基类中的方法。 子类中扩展的方法,基类中没有定义,通过基类指针或引用禁止调用(语法错误) 子类中声明虚函数,子类中重写但签名不同,不产生多态,直接调用的基类中的方法。 按值传递对象时,不能实现多态,赋值时会产生切片效
15、果。,-29-,基类指针不能访问子类特有方法,如果Dog类中扩展了wagTail方法,但Mammal类中没有wagTail方法。通过基类指针不允许访问wagTail方法。 Mammal *pDog = new Dog; pDog-wagTail(); Dog *pRealDog = dynamic_cast (pDog); if(pRealDog)pRealDog-wagTail();,X,如果pDog确实指向1个Dog对象,可通过dynamic_cast将其转换为Dog类型指针,从而可以调用wagTail方法。 dynamic_cast进行运行时类型检查,若转换失败,会返回空指针。但尽量避免
16、这种强制转换。,-30-,对象不能实现多态,void valueFunction( Mammal mammalValue) mammalValue.speak(); void ptrFunction (Mammal *pMammal) pMammal-speak(); void refFunction (Mammal ,通过指针或引用才能实现多态,传递对象不能实现多态,-31-,多态的补充说明,应该将析构函数声明为虚函数,如果希望所写的类被继承并多态执行。 不声明虚析构函数情况下,基类指针指向堆中创建的子类对象,执行delete操作时,将会产生内存泄漏 声明虚析构函数情况,执行delete时,
17、由于多态效果,调用子类的析构函数,从而引起基类析构函数调用。 构造函数不能声明为虚函数。 使用虚函数实现多态增强了程序的灵活性,但由于维护虚函数表,会带来运行时的开销。,-32-,本章内容安排,多态的概念 虚函数实现多态 虚函数机制 纯虚函数与抽象类,-33-,Shape类,class Shape public:Shape() virtual Shape() virtual double getArea() return -1; virtual double getPerim() return -1; ;,形状类是其它具体形状类型的基类,计算面积、周长和绘图功能对shape类实际上没有多大的意
18、义。,-34-,Circle类,const double PI=3.14; class Circle : public Shape public:Circle(double newRadius) : radius( newRadius ) virtual Circle() virtual double getArea() return PI * radius * radius; virtual double getPerim() return 2 * PI * radius; private:double radius; ;,Circle类重写了基类的2个虚函数。,-35-,Rectangle
19、类,class Rectangle : public Shape public:Rectangle(double newLen, double newWidth):length(newLen), width(newWidth) virtual Rectangle() virtual double getArea() return length * width; virtual double getPerim() return 2 * length + 2 * width; virtual double getLength() const return length; virtual doubl
20、e getWidth() const return width; private:double width;double length; ;,Rectangle类重写了基类的2个虚函数,-36-,将Shape声明为抽象数据类型,Shape是各种形状的基类,但定义Shape类对象并没有意义,Shape更应该作为一种抽象概念。 将Shape类中的虚函数定义为纯虚函数,表明Shape是一种抽象概念(抽象类)。 纯虚函数只需要给出声明,通常情况下不给出纯虚函数的定义(函数体) C+禁止定义抽象类对象; 抽象类基础上派生的类,应该重写基类的纯虚函数,若没有重写,该类仍然是抽象类。,-37-,抽象Shape类,class Shape public:Shape() virtual Shape() virtual double getArea() = 0;virtual double getPerim() = 0; ;,Shape类是抽象类,不允许实例化。Shape类的子类必须重写Shape类的纯虚函数后才能实例化。 抽象类给出所有子类应该遵循的抽象接口,通常不在抽象类中定义数据成员。,