攻城狮凌风

多态性,虚函数与抽象类

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 虚函数的适用情形

     虚函数要求系统具备一定的空间开销。是否对某个成员函数使用虚函数考虑以下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即为抽象类,不能用来实例化对象。

   


posted on 2014-07-26 17:05  攻城狮凌风  阅读(278)  评论(0编辑  收藏  举报

导航