多态性,虚函数与抽象类
1.什么是多态性
多态性实质指同样的消息被不同类型的对象接受导致不同的行为。这里,同样的消息理解为【对类成员同名函数的调用】,而不同的行为则是产生不同的输出结果。例如在运算符重载,同样是调用“+”,但是结果可以是int,float等等。
系统的角度来说,多态性分为静态多态性(编译多态性)和动态多态性(运行多态性)。函数重载和运算符重载属于编译多态性,在编译的时候,程序就能确定具体调用哪个函数实例。而虚函数属于运行多态性,在程序的运行过程中,程序才能动态的确定操作所针对的对象。
1.1编译多态性
编译的多态性由重载函数体现,分为两种方式。
1)在类中说明的重载。例如构造函数或成员函数的重载,不同的参数表决定不同的系统编译调用。
2)基类成员函数在派生类中的重载。倘若派生类中的成员函数(非虚函数)与基类的成员函数同名。则基类的同名函数的所有重载均会被覆盖。这时,可以使用类作用域符"::"加以区分或者根据对象区分。
实例1.基类成员函数在派生类中的重载
实例代码如下:
#include "stdafx.h" #include"iostream" using namespace std; class Point { int x;float y; public: Point(int a=0,double b=0):x(a),y(b){} /*此处基类中使用了成员函数重载*/ void show(int a){cout<<"Point-int----"<<x<<endl;} void show(double b){cout<<"Point-float---"<<y<<endl;} }; class Circles:public Point { int r; public: Circles(int x,double y,int z):Point(x,y){r=z;} void show(){ cout<<"Circles---"<<r<<endl;} }; int _tmain(int argc, _TCHAR* argv[]) { Point *pointer1; Circles *pointer2; Point p(1,2.34);Circles c(2,3.45,6); p.show(1);p.show(1.00);//执行Point类中的show() c.Point::show(1);c.Point::show(1.00);//执行Point类中的show() /*输出结果中覆盖了基类的所有同名函数重载,且是在函数参数表不同的情况下*/ c.show();//执行Circles类中的show() cout<<"--------------以下使用指针----------------"<<endl; pointer1=&p; pointer1->show(1);pointer1->show(1.00); pointer1=&c; pointer1->show(1);pointer1->show(1.00); pointer2=&c; pointer2->show();//此时使用----pointer2->show(1) AND pointer2->show(1.00)无效 return 0; }
运行结果如下:
结果分析如下:
1.使用指向派生类对象的派生类指针或派生类对象调用函数时。基类中的同名函数的所有定义均被覆盖,尽管参数表不同;
2.参数表的作用基本被忽略,可以看到3个show()的参数表相异;
3.可以添加类作用域符"::",利用派生类对象名调用基类中与派生类中函数同名的成员函数。
4.基类指针可以指向基类对象或者派生类对象。这时调用的函数仍然为基类的成员函数,也就是说函数参数表仍然要和基类的成员函数(重载的成员函数)匹配;简单的说,基类指针指向派生类对象,实质等同于3中添加类作用域符"::"基类中与派生类中函数同名的成员函数。这是与虚函数最大的不同。
5.派生类指针用于指向派生类对象。调用派生类的成员函数时,同样参数表应该匹配(类型和个数)。
1.2 运行多态性
如前文所说,运行多态性基于虚函数实现。见下文2.虚函数
2.虚函数
在面向对象的程序设计中,为了保留基类的特性且减少新类的开发时间,经常要用到继承。但是继承来的函数并不能完全适应派生类的需要。派生类需要重写基类的函数的时候,同名则会发生覆盖,不同名则会在派生层次较多时命名过于混杂。虚函数此时就能体现意义,在派生类中能够对基类中的函数重定义,赋予新的功能。使用指向基类的指针,分别指向同一类族的不同类对象,从而调用其中的同名函数,实现函数的多态性。
2.1 虚函数的声明定义和使用
声明:
virtual 类型说明符 函数名(参数表)
某个类成员函数为虚函数,其派生类所有同名函数皆为虚函数,重定义时virtual可省略。
定义:
1.可以在类外或者类内定义(即使虚函数在内定义,编译仍然视作非内敛。),类外定义时virtual可省略,但类作用域(如A::)不可省。
2.派生类重定义时,务必返回同类型、同名、同参数表(类型、个数),否则丢失虚函数特性。这里不同于函数的重载。
使用:
定义指向基类的指针,分别指向同一类族中需要调用某个虚函数的不同的对象。此时调用最高层派生类的重定义虚函数。具体见实例。
注意:
1.只有成员函数才能声明为虚函数,普通函数不行。
2.构造函数不能声明为虚函数,因为执行构造函数时对象尚未生成,谈不上函数对象关联。
3.静态成员函数不能声明为虚函数,因为其不受限于对象。
4.内联函数也不能。内联函数不能在运行中动态确定其位置。
2.2 虚函数的适用情形
1.该成员函数所在类是否为基类。在派生类中是否会用到该函数,是否希望更改该函数的功能。均是,则可考虑定义虚函数。
2.调用是否是通过基类指针调用。如果是指针调用成员函数,其满足条件1则建议定义为虚函数。但是倘若成员函数经由对象访问,由于编译时即可知道调用的虚函数属于哪个类即静态关联,此时不用定义虚函数,因为无法实现运行时的多态性。
2.3 虚函数的特例——虚析构函数
1.在基类的析构函数前加virtual,则可将析构函数声明为虚函数。2.一旦声明,该基类的所有派生类的析构函数皆为虚函数,即使不同名。3.虚析构函数的作用在于,用delete删除对象时,保证析构函数正确执行,具体分析见实例3。
2.4 实例代码、运行结果与分析
实例2. 虚函数的使用
#include "stdafx.h" #include"iostream" using namespace std; class A{ public: virtual void show(){cout<<"call A"<<endl;}//类内定义类内声明 }; class B:public A{ public: virtual void show(); }; void B::show(){cout<<"call B"<<endl;}//类内定义类外声明 class C:public A{ public: virtual void show(){cout<<"call C"<<endl;}//类内定义类内声明 }; int _tmain(int argc, _TCHAR* argv[]) { A *p1,a; B *p2,b; C *p3,c; cout<<"/**********基类指针调用************/"<<endl; p1=&a;p1->show(); p1=&b;p1->show(); p1=&c;p1->show(); cout<<"/**********派生类指针调用**********/"<<endl; p1=&a;p1->show(); p2=&b;p2->show(); p3=&c;p3->show(); cout<<"/**********对象名调用*************/"<<endl; a.show();b.show();c.show(); return 0; }运行结果如下:
结果分析如下:
1.基类指针指向派生类对象时,调用的是派生类中重定义的虚函数。不同于实例1编译多态性中,基类指针指向派生类对象时,调用的仍然是基类中的函数。
2.仍旧可以使用对象名,静态地调用每个派生层次中的同名函数。相同于实例1编译多态性情况。
3.指向派生类对象的指针,调用的是对应派生类中的定义的虚函数。相同于实例1编译多态性情况。
实例3.虚析构函数是否使用的结果对比
#include "stdafx.h" #include "iostream" using namespace std; class A{ public: virtual ~A(){cout<<"call ~A() "<<endl;} }; class B:public A{ public: virtual ~B(){cout<<"call ~B() "<<endl;} }; int _tmain(int argc, _TCHAR* argv[]) { cout<<"****使用虚析构函数*****"<<endl; A* ptr=new B; delete ptr; return 0; }
结果分析如下:
1.基类使用虚析构时,将先调用派生类的析构再调用基类析构,符合人们的愿望。这是无论指针指向哪一类族的哪一类对象,系统都将采用动态关联,依次调用析构函数对该对象进行清洗。
2.基类不使用虚析构时,由于静态关联,只调用基类析构函数。
3.继承(派生)情况下,优先使用虚析构函数,即使基类不需要析构函数。这样能保证撤销动态分配空间时能得到正确的处理结果
3.纯虚函数和抽象类
3.1 纯虚函数
倘若有这样一种情形。基类中不需要对虚函数有定义或者有意义的实现,具体实现由派生类做。很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。这里就需要纯虚函数上马了。声明方法如下,类似初始化为0。
声明:
virtual 类型说明符 函数名(参数表)=0;
纯虚函数不具备函数功能,不能被调用。只至少包括纯虚函数的基类不能用来实例化对象,即3.2抽象类。派生类中若仍旧未定义,则该函数仍旧是纯虚函数,派生类也是是抽象类。少废话,看代码:
实例4.纯虚函数的应用实例
#include "stdafx.h" #include"iostream" using namespace std; class A{ public: virtual void show()=0; }; class B:public A{ public: void show(); }; void B::show(){cout<<"call B"<<endl;} class C:public A{ public: void show(){cout<<"call C"<<endl;} }; void show(A *p) { p->show(); } int _tmain(int argc, _TCHAR* argv[]) { A *a; B *b=new B; C *c=new C; show(b);show(c);//注意派生类指针可以作为基类指针形参 b->show();c->show();//派生类指针调用 delete b;delete c; /*******基类指针调用**********/ a=new B;a->show();delete a; a=new C;a->show();delete a; return 0; }
不出意外,运行结果均是"call B call C"。
比如一个基类派生了 N 个派生类,需要调用派生类中的处理函数,但不知调用哪个派生类(需要在运行时候确定)。这个时候,通用的基类指针就可以解决这个问题了。
这里存在派生类指针作为基类指针使用的问题。属于运行多态性的实例,类似于派生类指针被"强制转化"为基类指针。以上面的子函数show()为例,形参为抽象类A指针,但是并不知道指向的是B还是C对象,当分别给予B,C类指针的时候,就分别调用B,C类的虚函数定义了。这就体现了多态性。
未改变指针指向的对象,虚表没有修改,只是改变了编译器“看到”的该指针的方式。继承类别会继承基础类别的虚拟函数表(以及所有其他可以继承的成员),当我们在继承类别中改写虚拟函数时,虚拟函数表就受到了影响:表中所指的函数位置将不再是基础类别的函数位置,而是继承类别的函数位置。所以当我们用基类指针访问虚函数时实绩上访问的是继承类的函数。
关于虚函数和虚表覆盖(override)机制,点击这里。
3.2 抽象类
至少含有一个纯虚函数的类被称为抽象类,抽象类在包含纯虚函数的同时,也能包括其他所有类型的成员函数。抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承结构的上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为protected。
举个例子来说,比如我们设计了一个交通工具的抽象类。显而易见的,由交通工具类可以派生出汽车类,飞机类等具备具体特性的类。但是对于基类交通工具来说,它的特性却是模糊的,广泛的,此时建立一个交通工具类的对象是没有任何实际意义的,对于这种没有必要建立对象的类进行约束,C++引入了抽象类的特性,而抽象类的约束控制来自于纯虚函数。
抽象类的主要作用就是描述一组相关子类的通用操作接口。一般而言,抽象类只描述这组子类共同的操作接口,而实现交给子类来完成。抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它就可以创建该类的实例了。
抽象类不能作为参数类型,函数返回类型或显式转换类型。
实例4中的类A即为抽象类,不能用来实例化对象。