多态性与虚函数

来源:互联网 发布:百度大数据部怎么样 编辑:程序博客网 时间:2024/05/07 06:09

多态性概念:

在面向对象的概念中,多态性是指不同对象接收到相同消息时,根据对象类的不同产生不同的动作。多态性提供了同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数。具体地说,就是同一个函数名称,作用在不同的对象上将产生不同的操作。

    多态性提供了把接口与实现分开的另一种方法,提高了代码的组织性和可读性,更重要的是提高了软件的可扩充性。


一. 重载函数
1. 在C++语言中,编译程序选择相应的重载函数版本时函数返回值类型是不起作用的。不能仅靠函数的返回值来区别重载函数,必须从形式参数上区别开来。例如:
void print(int a);
void print(int a,int b);
int print(float a[]);
这三个函数是重载函数,因为C++编译程序可以从形式参数上将它们区别开来。

但是:
int f(int a);
double f(int a);
不能进行重载,会发生编译错误。

 

2. 函数重载的二义性(ambiguity):
  (1). 是指C++语言的编译程序无法在多个重载函数中选择正确的函数进行调用。这些二义性错误是致命的,因而编译程序将无法生成目标代码。
函数重载的二义性主要源于C++语言的隐式类型转换与默认参数.


  (2). 在重载函数中使用默认参数也可能造成二义性

3. 析构函数不能进行重载,因为析构函数本来就是无参数的,无返回类型的。

 

/* 例1:test1.cpp   */
/* IDE环境: Dev-C++ 4.9.9.2 */
/* 重载函数举例 */

#include <stdio.h>
class base
{
    public:
           //构造函数重载
           base():i(0){ };
           base(int x) { i = 10; }
           //base (int y){ j = 20; } // compile error: F:/devcpptest/devcpptest/test1.cpp `base::base(int)' and `base::base(int)' cannot be overloaded
                                       //形式与上一个一样。
           base(int x,int y) { i = x; j = y; }
          
          
           int get_i(){ return i; }
          
           //其它函数重载:
           void  display(){ printf(" base::display_i, i = %d, j = %d /n", i, j); } 
           void  display(int type)
           {
                 if (type)
                    printf(" base::display, i = %d/n", i);
                 else
                     printf(" base::display, j = %d /n", j);
           }
          
           float abs(float x)
           {
               return (x>0?x:-x);
           }
           double abs(double x)
           {
               return (x>0?x:-x);
           }

           //析构函数不能重载:
           ~base(){};
           //~base(int type) { i = type; } //compile error: F:/devcpptest/devcpptest/test1.cpp `base::~base()' and `base::~base()' cannot be overloaded
          
    private:
            int i;
            int j;
};


int main()
{
    base b;
    printf("abs(1.78) = %f /n", b.abs(1.78)) ; // OK, 调用abs(double)
    //printf("abs(-7) = %f /n", b.abs(-7));    // compile error,编译程序无法确定调用哪一个abs()函数

    while(1);
}

 

二. 虚函数

虚函数是重载的另一种形式,实现的是动态的重载,即函数调用与函数体之间的联系是在运行时才建立,也就是动态联编。
在基类用virtual声明的成员函数即为虚函数。

1. 静态联编与动态联编
   (1)静态联编:是指联编工作出现在编译连接阶段,这种联编过程是在程序开始运行之前完成的。在编译时所进行的这种联编又称静态绑定。
        在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又称为绑定,在编译时绑定又称静态聚束。
   (2)动态联编:编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,
        为此要确切知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或称动态绑定。
        动态联编实际上是进行动态识别。
   (3)比较:

 

程序中的形式

聚束方式

函数原型

决策依据

特点

重载函数

静态聚束

不同

参数数目及类型

高效

虚函数

动态聚束

相同

运行时指针的指向或引用

高效灵活,抽象性,可扩充性

 


  
  
2. 虚函数的概念
   虚函数是在基类中冠以关键字 virtual 的成员函数。它是动态联编的基础。虚函数是成员函数,而且是非static的成员函数。
   它提供了一种接口界面,并且可以在一个或多个派生类中被重定义。
   一个虚函数是属于它所在的类层次结构的,而不是只属于某一个类。
3. 动态联编与虚函数
   派生类中对基类的虚函数进行替换时,要求派生类中说明的虚函数与基类中的被替换的虚函数之间满足如下条件:
(1)与基类的虚函数有相同的参数个数;
(2)其参数的类型与基类的虚函数的对应参数类型相同;
(3)其返回值或者与基类虚函数的相同,或者都返回指针或引用,
     并且派生类虚函数所返回的指针或引用的基类型是基类中被替换的虚函数所返回的指针或引用的基类型的子类型。
(4)在派生类对基类中声明为虚函数进行重新定义时,关键字virtual也可以不用写,即 对于
    class Point {
    private:
        flat x,y;
    public:
        virtual float area() // 虚函数
        {
            return 0.0;
        }/
    };
    const float PI = 3.141593;
    class Circle: public Point {
    private:
        float radius;
    public:
        virtual float area()
        {
            return PI*radius*radius;
        }
    };
    派生类类中的virtual float area() 也可以写成下面的形式:
    float area()。

 

4. 虚函数的限制

(1)在类体系中访问一个虚函数时,应使用指向基类类型的指针或对基类类型的引用,以满足运行时多态性的要求。
(2)在派生类中重新定义虚函数时,必须保证该函数的值和参数与基类中的说明完全一致,否则就属于重载(参数不同)或是一个错误(返回值不同)。
(3)若在派生类中没有重新定义虚函数,则该类的对象将使用其基类中的虚函数代码。
(4)虚函数必须是类的一个成员函数,不能是友元,但它可以是另一个类的友元。
(5)析构函数可以是virtual的虚函数,但构造函数则不得是虚函数。
(6)一个类的虚函数仅对派生类中重定义的函数起作用,对其他函数没有影响。

 

5. 虚函数与重载函数的比较
(1)重载函数要求函数有相同的返回值类型和函数名称,并有不同的参数序列;而虚函数则要求这三项(函数名、返回值类型和参数序列)完全相同。
(2)重载函数可以是成员函数或友元函数,而虚函数只能是成员函数。
(3)重载函数的调用是以所传递参数序列的差别作为调用不同函数的依据;虚函数是根据对象的不同去调用不同类的虚函数。
(4)虚函数在运行时表现出多态功能,这是C++的精髓;而重载函数则在编译时表现出多态性。

 

6. 虚函数的访问:
(1)用基指针访问与用对象名访问;
     虚函数被指向基类的指针(或引用)调用时,C++对其进行动态聚束,向实际的对象传递消息。
     但是,通过一个对象名访问虚函数时,C++系统将采用静态聚束。
     例如,对于上面的Point 和 Circle 类,
     Point *pp;
     Circle c;
     pp = &c;
     pp->area(); //调用的是Circle的area
     调用的是Circle的area;
     而,
     Circle c;
     c.area();            // 调用的是本类Circle的area。
     c.Point::area();     // 调用的是父类Point的area,采用作用域运算符来指定调用父类的方法。
     c.Circle::area();    // 调用的是本类Circle的area。
    
    

(2)由类的成员函数访问该类层次中的的虚函数,要使用this指针;例如,

/* 例2:test2.cpp   */
/* IDE环境: Dev-C++ 4.9.9.2 */
/* 虚函数举例 */


      #include <stdio.h>
     
      class A {
            public:
                   virtual void vir_func1() {
                       printf(" v1 is called in A./n");
                       a1();
                   }
                   virtual void vir_func2() {
                       printf(" v2 is called in A./n");
                   }
                   void a1() {
                       printf(" a1 is called in A./n");
                       vir_func2();  // 等价于 this->vir_func2()
                   }
      };
     
      class B: public A {
            public:
                  
                   virtual void vir_func2() {
                       printf(" v2 is called in B./n");
                   }
                   void b1() {
                       printf(" b1 is called in B./n");
                       vir_func2();
                   }
      };
      int main()
      {
          A a;
          a.vir_func1();
          printf(" OK./n");
         
          B b;
          b.vir_func1();
         
          while(1);
          return 0;
      }
      /*
      result:
            v1 is called in A.
            a1 is called in A.
            v2 is called in A.
            OK.
            v1 is called in A.
            a1 is called in A.
            v2 is called in B.
           
      */

    
(3)用构造函数和析构函数访问:
     这两个函数访问虚函数时,C++采用静态聚束。例如,

/* 例3:test3.cpp   */
/* IDE环境: Dev-C++ 4.9.9.2 */
/* 构造函数访问虚函数举例 */
      #include <stdio.h>
     
      class A {
            public:
                   A() { };
                   virtual void vir_func1() {
                       printf(" v1 is called in A./n");
                   }
      };
     
      class B: public A {
            public:
                   B() {
                       printf(" call v1 in B./n");
                       vir_func1(); //  调用本类中定义的虚函数
                   }
                   virtual void vir_func1() {
                       printf(" v1 is called in B./n");
                   }
      };
     
      class C: public B {
            public:
                   C() {
                       printf(" call v1 in C./n");
                       A::vir_func1();  // 调用基类中定义的虚函数
                   }
                   void vir_func1() {
                       printf(" v1 is called in C./n");
                   }
      };
      int main()
      {
          B b;
          printf(" OK./n");
         
          C c;
        
          while(1);
          return 0;
      }
      /*
      result:
            call v1 in B.
            v1 is called in B.
            OK.
            call v1 in B.
            v1 is called in B.
            call v1 in C.
            v1 is called in A.
           
      */

 

 7. 多基派生中虚函数的二义性 :

 

在本博的"继承"一篇中,提到了"同一基类多次拷贝引起的二义性",消除这种二义性的方法是把公共基类定义为虚基类。

同样,多基派生中,虚函数也有这样的问题。具体来说,有两种场景:

(1) 函数被定义为虚函数,但基类不是虚基类;

(2) 函数被定义为虚函数,基类也被定义成虚基类。

 

首先,看第一种情形:函数被定义为虚函数,但基类不是虚基类。

下面程序中,

 

A *a = &d 语句是错误的。

 

因为不能使用指向公共基类A的指针来访问多条路径汇聚处的派生类D的对象。

 

/* IDE环境: Dev-C++ 4.9.9.2 */

/* 例4: virtual2.cpp */

 

#include <stdio.h>

         

    class  A 

    {

           public :

                  virtual void test_func1() { printf("A::test_func1() /n"); }

                  virtual void test_func2() { printf("A::test_func2() /n"); }

                  virtual void test_func3() { printf("A::test_func3() /n"); }

     };

   

    class  B :  public  A

    {

           public :

                  virtual void test_func1() { printf("B::test_func1() /n"); }

                  virtual void test_func2() { printf("B::test_func2() /n"); }

    };

   

    class  C :  public  A

    {

           public :

                  virtual void test_func1() { printf("C::test_func1() /n"); }

                  virtual void test_func2() { printf("C::test_func2() /n"); }

                                  

    };

   

    class  D : public  B , public  C

    {

           public :

                  virtual void test_func1() { printf("D::test_func1() /n"); }

    };

   

    int main()

    {

        D d;

        //A *a = &d; // compile error: D:/devcpptest/virtual2.cpp `A' is an ambiguous base of `D'

                     // 因为:不能使用指向公共基类A的指针来访问多条路径汇聚处的派生类D的对象。

       

        B *b = &d; //OK. 可以使用指针指向类层次结构中无分支的某条路径上的某个类

        b->test_func1(); // 调用 D的test_func1

        b->test_func2(); // 调用 B的test_func2 

        b->test_func3(); // 调用 A的test_func3

       

        while(1);

        return 0;

    }

   

/* result:

          D::test_func1()

          B::test_func2()

          A::test_func3()

*/

 

 

从例4可以看出,在这种场景下,有:

 (1) 不能使用指向公共基类A的指针来访问多条路径汇聚处的派生类D的对象。

 (2) 只可以使用指针指向类层次结构中无分支的某条路径上的某个类。

 (3) 派生类中定义的函数可以覆盖基类中的同名函数,虚函数也不例外。

 

 

再看第二种情形:虚基类中含有虚函数

在例4类的定义的基础上,将基类A定义为B和C的虚基类,如下,

 

     class  B : virtual public  A

     class  C : virtual public  A,

    

在这样的类层次结构中,就可以使用指向公共基类指针来访问派生类对象了,但是,这和编译器相关。

有的编译器认为这种用法是正确的,有的认为是非法。

如下:

    int main()

    {

        D d;

        A *a = &d;

        a->test_func1(); // 

        a->test_func2(); //  有的编译器认为这种用法是正确的,有的认为是非法。  

        a->test_func3(); // 

       

        while(1);

        return 0;

    }

这时,会产生对函数test_func2()引用的二义性,因为test_func2()没有在D中重新定义,

即在对象d中继承的test_func2()来自B还是C呢?这和编译器有关。有的编译器(如Borland C++)

会以B与C作为D的基类的顺序有关,哪个在前,就用以哪个为准。GCC编译器会报错。

在Borland C++ 中,上面运行的结果是:

/* result:

          D::test_func1()

          B::test_func2() //因为是 class  D : public  B , public  C

                          //而不是 class  D : public  C , public  B

          A::test_func3()

*/

 

/* 说明:

result:

1.  Borland C++ 中,正确运行, 结果如下:

          D::test_func1()

          B::test_func2() //因为是 class  D : public  B , public  C

                          //而不是 class  D : public  C , public  B

          A::test_func3()

 

2.  VC6.0中,会有编译错误: 'D' : ambiguous inheritance of 'B::test_func2'

                                                   'D' : ambiguous inheritance of 'C::test_func2'

     Dev-C++ 4.9.9.2中,会有编译错误: no unique final overrider for `virtual void A::test_func2()' in `D'

*/

 

 

8. 再给出一个虚函数的例子:

 

/* 例5:virtual3.cpp   */

/* IDE环境: Dev-C++ 4.9.9.2 */

/* 虚函数举例 */

 

 

#include <stdio.h>

class point

{

      private:

              float x,y;

      public:

             point(){printf("point::point/n");}

             point(float i,float j)

             {

                 printf("point::point(i,j)/n");

                 x =i;

                 y =j;

             }

             virtual float area()

             {

                   return 0.0;

             }

             float girth()

             {

                   printf("point::girth/n");

                   return 0.0;

             }

      private:

              void print()

              {

                   printf("point::print/n");

              }

};

 

const float PI = 3.14;

class circle1: public point

{

      private:

              float r;

      public:

             circle1(float r)

             {

                 printf("circle1::circle1/n");

                 this->r = r;

             }

             float area()

             {

                   return PI*r*r;

             }

             float gir()

             {

                   printf("circle1::gir/n");

                   return PI*2*r;

             }

     

                  

};

 

int main()

{

     point *pp;

     circle1 c1(5.1);

     pp = &c1;

     printf("area of c1 = %f/n",pp->area());         //81.671700

     printf("area of c1 = %f/n",c1.point::area());   //0.000000

     printf("area of c1 = %f/n",c1.area());          //81.671700

    

     point *p = new circle1(2);  //1.其实是new了一个circle1的对象,而基类指针p又指向

                                 //这个对象,使得p可以访问circle1的area函数.

                                 //2.又由于这是一个point的指针,所以可以访问point的函数.

                                 //又,既然对象是circle1类型的,它继承了point的public函数,

                                 //所以,p肯定能够访问到自己类型的函数.

                                 //3.那么,circle1不继承point的private函数,p还能够访问

                                 //point的private函数么? 当然不能了,因为new的对象是

                                 //circel1的.p指向的实际上是circle1类型的对象,而这个

                                 //对象并没有继承point的private的函数.

                                

     printf("area of p = %f/n",p->area()); //12.560000

    

    

     p->girth(); //ok,

     //p->print(); //error  

     //p->gir();  //error  

     //girth is function of point, gir is function of circle1,可以看出,p的确是

     //point的指针,而这个基类指针是可以访问派生类的area函数(这正是virtual function

     //的作用.但是,p不能访问派生类的gir函数,因为这个函数不是point的成员函数.

     while(1);

}

 

/* result:

    point::point

    circle1::circle1

    81.671400

    0.000000

    81.671400

    point::point

    circle1::circle1

    area of p = 12.560000

    point::girth

*/

 

9. 后记:关于多态,虚函数,抽象类,纯虚函数这几个重要的概念与应用,请参考以下书籍:

    effectivec++

    c++编程思想第二版

    c++程序设计语言第三版

    C++ Primer 3rd Edition