zoukankan      html  css  js  c++  java
  • 多态性,虚函数与抽象类

    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即为抽象类,不能用来实例化对象。

       


  • 相关阅读:
    [JAVA安全机制]Java虚拟机-保险沙箱
    计算机网络自顶向下方法第3章-传输层 (Transport Layer).1
    Python基础:一起来面向对象 (二) 之搜索引擎
    Python基础:一起来面向对象 (一)
    计算机网络自顶向下方法第2章-应用层(application-layer).2
    Python基础:lambda 匿名函数
    Python基础:自定义函数
    Python基础:异常处理
    Python基础:条件与循环
    计算机网络自顶向下方法第2章-应用层(application-layer).1
  • 原文地址:https://www.cnblogs.com/engineerLF/p/5393153.html
Copyright © 2011-2022 走看看