360 likes | 590 Views
9 虚函数与多态性. 9.1 多态性 多态性 就是指同样的消息被类的不同的对象接收时导致的完全不同的行为的一种现象。这里所说的消息即对类成员函数的调用。 C++ 支持两种不同类型的多态:一种是编译时的多态,另一种是运行时的多态。在编译时的多态是通过静态联编实现的;而在运行时的多态则是通过动态联编实现的。
E N D
9 虚函数与多态性 • 9.1 多态性 • 多态性就是指同样的消息被类的不同的对象接收时导致的完全不同的行为的一种现象。这里所说的消息即对类成员函数的调用。 • C++支持两种不同类型的多态:一种是编译时的多态,另一种是运行时的多态。在编译时的多态是通过静态联编实现的;而在运行时的多态则是通过动态联编实现的。 • 很明显,函数的重载实现了一种多态性;这里要讲的多态性是建立在虚函数的概念和方法基础之上,通过虚函数来实现的,而虚函数又必须存在于继承的环境下。利用多态性,用户能够发送一般形式的消息,而将所有的实现细节留给了消息的对象,所以说多态性与数据封装和继承共同构成面向对象程序设计的三大机制。
多态的类型 • 面向对象的多态性可以分为四类:重载多态、强制多态、包含多态和参数多态。前面两种统称为专用多态,而后面两种也称为通用多态。我们学习过的普通函数及类的成员函数的重载都属于重载多态,还将讲述运算符重载。强制多态是指将一个变元的类型加以变化,以符合一个函数或者操作的要求,例如加法运算符在进行浮点数与整型数相加时,首先进行类型强制转换,把整型数变为浮点数再相加的情况,就是强制多态的实例。包含多态是研究类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现。(参数多态与类模板(类属)相关联,类模板是一个参数化的模板,在使用时必须赋予实际的类型才可以实例化。这样,由类模板实例化的各个类都具有相同的操作,而操作对象的类型却各不相同。) • 函数重载在介绍函数及类的部分曾做过详细的讨论,这里主要介绍虚函数是包含多态的关键内容。
虚函数与多态性 • 子类型 • 用基类指针指向公有派生类对象 • 静态联编与动态联编 • 虚函数 • 函数覆盖 • 空虚函数 • 纯虚函数与抽象类
9 虚函数与多态性 • 9.2 子类型 • C++中的动态联编是通过虚函数实现的,而要理解虚函数必须首先讨论一个与之相关的概念,即子类型。 • 如果一个特定的类型S,当且仅当它提供了类型T的行为时,则称类型S是类型T的子类型。子类型体现了类型间的一般与特殊的关系。 • 在C++中,子类型的概念是通过公有继承(或公有派生)来实现的。 • 根据继承方式的概念,我们知道,按公有继承的方式产生的派生类中,必然包含了原来基类中的全部成员。因此,一个公有派生类的对象可以提供其基类对象的全部行为(基类的全部接口),也就是说,在程序中可以把一个公有派生类对象当作其基类对象来处理。
9 虚函数与多态性 • 【例3-13】子类型的概念及实现示例。 • #include <iostream.h> • class A //定义类A • {private: • int a; • public: • A(int i=0){a=i;} • void print(); • }; • void A::print () • { cout<<"In class A, print() is called."<<endl; }
9 虚函数与多态性 • class B:public A //定义类B,类B是类A的公有派生类 • { • private: • int b; • public: • B(int j=-1){b=j;} • }; • void commfun(A &aref) • { • aref.print(); • }
9 虚函数与多态性 • void main() • {A a; • commfun(a);//以基类A的对象a作为实参调用函数commfun • B b; • commfun(b);//以派生类B的对象b调用函数commfun • } • 程序的运行结果为: • In class A, print() is called. • In class A, print() is called. • 说明:
9 虚函数与多态性 • (1)在本例中,类B是类A的公有派生类,函数commfun()的形参是一个基类A对象的引用,所以在main函数中,把基类A的对象a作为实参调用函数commfun()时,产生的结果是不言而喻的。但在main函数中,当把类B的对象b作为实参调用函数commfun()时,函数commfun()仍能正常工作,且打印结果与对象a作为实参时的结果相同,这说明,在程序中可以把一个公有派生类对象当作其基类对象来处理。 • (2)将类型B的对象b传递给函数commfun()处理是在程序运行时发生的。但在程序编译时,编译器只能对源程序代码进行静态检查。 • (3)子类型的重要性在于可以减轻程序员编写程序代码的负担。
9 虚函数与多态性 • 9.3 用基类指针指向公有派生类对象 • 既然一个公有派生类对象可以当作基类对象使用,那么,指向基类的指针自然也可以指向其公有派生类对象。因此,基类指针、派生类指针、基类对象和派生类对象四者间有以下四种组合的情况: • (1)直接用基类指针指向基类对象。 • (2)直接用派生类指针指向派生类对象。 • (3)用基类指针引用其派生类对象。 • (4)用派生类指针引用基类对象。 • 由于(1)、(2)两种情况,指针类型和对象类型统一,因此完全行得通。
9 虚函数与多态性 • 对于第(3)种情况,由于可以把一个公有派生类对象当作基类对象处理,所以可以用基类指针指向其派生类对象。但必须注意的是,由于基类指针本身的类型并没有改变,因此基类指针仅能访问派生类中的基类部分。在程序中,当把派生类对象的指针赋给基类指针时,编译器能自动完成隐式类型转换。 • 对于第(4)种情况,将派生类指针直接指向基类对象是危险的,因为编译器不允许这么做,也不提供隐式类型转换。当然,程序员如果采用强制类型转换,也可以把基类指针转换为派生类指针,但这时要正确地使用该指针。
9 虚函数与多态性 • 【例3-14】基类指针、派生类指针、基类对象和派生类对象四者间组合的使用情况示例。 • #include <iostream.h> • class A //定义类A • {private: • int a; • public: • A(int i=1){a=i;} • void print(); • int geta(); • }; • void A::print () • { cout<<"a="<<a<<endl; }
9 虚函数与多态性 • int A::geta() • { return a; } • class B:public A //定义类B,类B是类A的公有派生类 • {private: • int b; • public: • B(int j=-1){b=j;} • void print(); • }; • void B::print () • { cout<<"b="<<b<<endl; }
9 虚函数与多态性 • void main() • {A aa(10),*pa; • B bb(20),*pb; • pa=&aa; //基类指针可以指向基类对象 • pa->print(); • pb=&bb; //派生指针可以指向派生类对象 • pb->print(); • pa=&bb; //基类指针可以指向派生类对象 • cout<<pa->geta()<<endl; //如改为pa->getb();则错误, • //因为基类指针仅能看到派生类中的基类部分 • pa->print(); • bb.print();
9 虚函数与多态性 • pb=(B *)pa; //经过强制类型转换,派生类指针也可以 • //指向基类对象 • //上面语句如改为pb=pa;则错误,因为派生类指针不可 • //以直接指向基类对象} • 程序的运行结果: • a=10 • b=20 • 1 • a=1 • b=20
9 虚函数与多态性 • 程序分析:在上例的main函数中,虽然基类指针pa指向派生对象bb(即:pa=&bb),但语句pa->print()与语句bb.print()的输出结果并不相同,从结果来看,前者的输出结果是“a=1”,而后者的输出结果为“b=20”。这是由于虽然一个基类指针可以指向其派生类对象,但指针本身的属性并没有改变,因此,系统认为它所指向的仍然是一个基类对象,于是就只能调用其基类的成员函数print()。进一步分析发现,在派生类B中虽然继承了基类A的成员函数print(),但为了适应派生类自己的需要,在派生类中已经改变了这个函数的实现,即在派生类中又定义了一个同名的print()函数,而这种改变在静态联编的条件上编译器并不知道,以致于造成以上结果的不统一。所以,必须通知编译器这种可能的改变,即需要进行动态联编。其方法就是在基类中将可能发生改变的成员函数声明为虚函数。
9 虚函数与多态性 • 9.4虚函数 • C++通过虚函数实现了多态性,而虚函数存在于继承环境中,在继承关系下,派生类作为基类的子类,在任何要求基类对象的地方使用派生类对象是有意义的。 • 声明虚函数的方法是在基类中的成员函数原型前加上关键字virtual。其格式如下: • class 类名 • {…… • virtual类型 函数名(参数表); • …… • }; • 当一个类的成员函数说明为虚函数后,就可以在该类的(直接或间接)派生类中定义与其基类虚函数原型相同的函数。
9 虚函数与多态性 • 这时,当用基类指针指向这些派生类对象时,系统会自动用派生类中的同名函数来代替基类中的虚函数。也就是说,当用基类指针指向不同派生类对象时,系统会在程序运行中根据所指向对象的不同,自动选择适当的函数,从而实现了运行时的多态性。 • 虚函数可以在一个或多个派生类中被重新定义,因此,属于函数重载的情况,但这种重载与一般的函数重载是不同的,要求在派生类中重新定义时,必须与基类中的函数原型完全相同,包括函数名、返回类型、参数个数和参数类型的顺序。这时无论在派生类的相应成员函数前是否加上关键字virtual,都将视其为虚函数,如果函数原型不同,只是函数名相同,C++将视其为一般的函数重载,而不是虚函数。只有类的成员函数才能声明为虚函数,全局函数及静态成员函数不能声明为虚函数。
9 虚函数与多态性 • 【例3-15】虚函数的定义与应用举例。 • #include “iostream.h” • class Base • { public: • virtual void show() { cout<<”base class\n”; } }; • class Der1: public Base • { public: • void show() { cout<<”derived class 1 \n”; } }; • class Der2: public Base • { public: • void show() { cout<<”derived class 2”; } };
9 虚函数与多态性 • void main() • { Base bobj; • Base *p; • Der1 dobj1; • Der2 dobj2; • p=&bobj; • p->show(); • p=&dobj1; • p->show(); • p=&dobj2; • p->show(); • }
9 虚函数与多态性 • 程序的运行结果:base class • derived class 1 • derived class 2 • 由上例可以看出: • (1)通过虚函数实现了运行时的多态性。 • (2)基类用虚函数提供了一个派生类对象都具有的共同界面,派生类又各自对虚函数定义自己的具体实现,这样,使得程序既简洁又具有扩充性,并能帮助程序员控制更大的复杂性。若派生类中没有重新定义基类的虚函数,则该派生类直接继承其基类的虚函数。 • (3)当一个函数在基类被声明为虚函数后,不管经历多少层派生,都将保持其虚拟性。
9 虚函数与多态性 • 9.5 静态联编与动态联编 • 在向对象的程序设计中,联编的含义是指把一个消息和一个方法联系在一起,也就是把一个函数名与其实现代码联系在一起。根据实现联编的阶段的不同,可分为静态联编和动态联编两种。 • 静态联编是在编译阶段进行的(先期联编)。而动态联编是在程序运行过程中,根据程序运行的需要进行的联编(后期联编) 。 • 实现静态联编的前提是:在编译阶段就必须能够确定函数名与代码间的对应关系。因此,当通过对象名调用成员函数时,只可能是调用对象自身的成员,所以,这种情况可采用静态联编实现。但当通过基类指针调用成员函数时,由于基类指针可以指向该基类的不同派生类对象,因此存在需要动态联编的可能性,但具体是否使用动态联编,还要看所调用的是否是虚函数。
9 虚函数与多态性 • 9.6纯虚函数与抽象类 • 纯虚函数是在基类中只声明虚函数而不给出具体的函数定义体,将它的具体定义放在各派生类中,称此虚函数为纯虚函数。通过该基类的指针或引用就可以调用所有派生类的虚函数,基类只是用于继承,仅作为一个接口,具体功能在派生类中实现。 • 纯虚函数的声明如下:(注:要放在基类的定义体中) • virtual 函数原型=0; • 其中:函数原型的格式同前面所学格式一样,要包括函数返回值的类型、函数名、圆括号、形参及其类型等。 • 声明了纯虚函数的类,称为抽象类。 • 使用纯虚函数时应注意: • (1)抽象类中可以有多个纯虚函数。
9 虚函数与多态性 • (2)不能声明抽象类的对象,但可以声明指向抽象类 • 的指针变量和引用变量。 • (3)抽象类也可以定义其他非纯虚函数。 • (4)如果派生类中没有重新定义基类中的纯虚函数,则在派生类中必须再将该虚函数声明为纯虚函数。 • (5)从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类。 • (6)在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层。 • (7)引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口。
9 虚函数与多态性 • 【例3-16】设计一个抽象类shape,它表示具有形状的东西,体现了抽象的概念,在它下面可以派生出多种具体形状,比如三角形、矩形。 • #include<iostream.h> • class Shape • { protected: • double x,y; • public: • void set(double i, double j) • { x=i; y=j; } • virtual void area()=0; //声明纯虚函数 • };
9 虚函数与多态性 • class Triangle: public Shape • { public: • void area() • { cout<< "三角形面积: " <<0.5*x*y<<endl; ; } • }; • class Rectangle: public Shape • { public: • void area() • { cout<<"矩形面积:" <<x*y<<endl; ; } • };
9 虚函数与多态性 • void main() • { Shape *p; • Triangle t; • Rectangle r; • p=&t; • p->set(5.1,10); • p->area(); • p=&r; • p->set(5.1,10); • p->area(); } • 结果:三角形面积:25.5 • 矩形面积:51
9.2 静态成员 • C++还有一种数据成员,称作“静态”成员,静态成员是所有对象公有的。静态成员有静态数据成员和静态函数成员之分。 • 9.2.1 静态数据成员 • 说明静态数据成员的语句格式是: • static 类型说明符 成员名; • 【例3-17】报名登记处登记每一位来访者的姓名,同时使用静态数据成员account自动产生一个流水号数,记入number中。
9.2静态成员 • # include <windows.h> • # include <iostream.h> • //定义类married • class married • { • private: • int number; //编号 • char *name; //姓名 • public: • static int glob; // 定义静态数据成员glob • void set_mes (char *a); // set_mes函数说明 • } ;
9.2静态成员 • // set_mes函数定义 • void married :: set_mes (char *a) • {name = new char[strlen(a) + 1] ; • strcpy (name, a) ; //用参数a的值修改私有变量。 • number=++glob; //glob加班后赋给number • cout << " 编号:"<<number<<endl; • } • int married :: glob= 0 ; //静态变量赋初始值0
9.2静态成员 • // 主函数 • void main () • { // 生成对象数组person • married person[100]; • int i ; // 局部变量i • char str[8] ; // 局部变量str • cout<<endl; • for ( i=0; i<100; i++) // 循环100次 • { //读入姓名,存于str • cout << " 输入姓名:"; cin >> str ; • person[i].set_mes ( str ) ; //保存并显示} • cout<<endl; • }
9.2静态成员 • 说明: • (1) 不管一个类的对象有多少个,其静态数据成员也只有一个,由这些对象所共享,可被任何一个对象所访问。 • (2) 在一个类的对象空间内,不包含静态成员的空间,所以静态成员所占空间不会随着对象的产生而分配,或随着对象的消失而回收。 • (3) 静态数据成员的存储空间的分配是在程序一开始运行时就被分配。并不是在程序运行过程中在某一函数内分配空间和初始化。
9.2静态成员 • (4) 静态数据成员的赋值语句,既不属于任何类,也不属于包括主函数在内的任何函数,静态变量赋初值语句应当写在程序的全局区域中,并且必须指明其数据类型与所属的类名,并用如下格式: • 类型 类名::变量名=值; • 如:上例中的:int visited::glob=0; • (5) 对于在类的public部分说明的静态数据成员,可以不使用成员函数而直接访问,即使未定义类的对象,同样也可以直接访问,但在使用时也必须用类名指明所属的类,如在上例中的glob数据成员,可以在main函数体中直接访问,cout<<visited::glob;而private和protected部分的静态成员只能通过类的成员函数访问。
9.2静态成员 • 9.2.2 静态成员函数 • 静态成员函数的定义: • static 类型 函数名(形参) • {函数体} • 与静态数据成员一样,静态成员函数与类相联系,不与类的对象相联系,所以访问静态成员函数时,不需要对象。如:
9.2静态成员 • #include “iostream.h” • class objcount • { private: • static int count; • public: • objcount() { count++; } • static int get() { return count; } • }; • int objcount::count=0; • void main() • {cout<<objcount::get(); • objcount a,b,c,d,e,f;
9.2静态成员 • cout<<objcount::get(); • count<<a.get(); • } • 一个静态成员函数不与任何对象相联系,所以它不能对非静态成员进行默认访问。如: • #include “iostream.h” • class student • { public: • static char *sname() • { cout << noofstudent<<endl; • return name; }
9.2静态成员 • protected: • char name[40]; • static int noofstudent; • }; • int student::noofstudent=0; • void fn() • { student s; • cout<<s.sname()<<endl; //error • //…… • }