多态的实现(重载、虚函数、抽象类)

来源:互联网 发布:程序员屏幕设置 编辑:程序博客网 时间:2024/06/06 02:44

1.函数重载

由静态联编支持的多态性称为编译时的多态性或静态多态性,也就是说,确定同名操作的具体操作对象的过程是在编译过程中完成的。在C++中,可以用函数重载和运算符重载来实现编译时的多态性。

2.虚函数

由动态联编支持的多态性称为运行时的多太性或动态多态性,也就是说,确定同名操作的具体操作对象的过程是在运行过程中完成的。在C++中,可以用虚函数来实现运行时的多态性。

2.1.虚函数定义

虚函数的定义是在基类中进行的,即把基类中需要定义为虚函数的成员函数声明为virtual。当基类中的某个成员函数被声明为虚函数后,就可以在派生类中被重新定义。在派生类中重定义时,其函数原型,包括返回类型、函数函数名、参数个数和类型、参数的顺序都必须与基类中的原型完全一致

定义形式为:

[cpp] view plaincopyprint?
  1. virtual <函数类型><函数名>(参数表)  
  2.   
  3. {  
  4.   
  5. 函数体  
  6.   
  7. }  

例如:解决博文多态概念时的问题代码如下:

[cpp] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class Animal  
  4. {  
  5.       public:  
  6.              void sleep()  
  7.              {  
  8.                   cout<<"Animal Sleep"<<endl;  
  9.                   }  
  10.             virtual void breathe()//加上virtual   
  11.              {  
  12.                   cout<<"Animal Breathe"<<endl;  
  13.                   }  
  14.       };  
  15. class Fish:public Animal  
  16. {  
  17.       public:  
  18.              void breathe()  
  19.              {  
  20.                   cout<<"Fish Bubble"<<endl;  
  21.                   }  
  22.       };  
  23. int main()  
  24. {  
  25.     Fish fs;  
  26.     Animal *an=&fs;  
  27.     an->breathe();  
  28.     system("pause");  
  29.     }  

只是加了一个virtual问题就解决了。输出结果为:

事实上,当将基类中的成员函数breathe()声明为virtual即虚函数时,编译器在编译的时候发现Animal类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数组中存放每个虚函数的地址。上述代码中,Animal类和Fish类都包含了一个虚函数breathe(),因此编译器会为这两个类分别建立一个虚表,如下图所示。

类Animal和类Fish的虚表

在上述代码中,当Fish类的fs对象构造完毕后,其内部的虚表指针也就被初始化为指向Fish类的虚表。在转换后,调用an->breathe(),由于an实际指向的是Fish类的对象,该对象内部的虚表指针指向的是Fish类的虚表,因此最终调用的是Fish类的breathe()函数。

使用派生对象指针时应注意的问题:

1.声明为指向基类对象的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象。

2.允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象。

3.声明为指向基类对象的指针,当其指向它的公有派生类的对象时(满足第1条),只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。要想访问其公有派生类中的成员,可将基类指针用显式类型转换方式转换为派生类指针。例如:

[cpp] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class B  
  4. {  
  5.       public:  
  6.              void vf()  
  7.              {  
  8.                   cout<<"This is class B"<<endl;  
  9.                   }  
  10.       };  
  11. class D:public B  
  12. {  
  13.       public:  
  14.              void vf()  
  15.              {  
  16.                   cout<<"This is class D"<<endl;  
  17.                   }  
  18.       };  
  19. int main()  
  20. {  
  21.     B b,*pb;  
  22.     D d,*pd;  
  23.     pb= &b;  
  24.     pb->vf();  
  25.     pb=&d;//满足第1第2条。   
  26.     pb->vf();  
  27.  // pd=&b;//违背第2条。invalid conversion from `B*' to `D*'   
  28.  // pd->vf();  
  29.     pd=(D*)&b;//满足第3条。   
  30.     pd->vf();  
  31.     pd=&d;  
  32.     pd->vf();  
  33.     system("pause");  
  34.     }  

如果把vf改成virtual类型的,有:

[cpp] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class B  
  4. {  
  5.       public:  
  6.              virtual void vf()  
  7.              {  
  8.                   cout<<"This is class B"<<endl;  
  9.                   }  
  10.       };  
  11. class D:public B  
  12. {  
  13.       public:  
  14.              void vf()  
  15.              {  
  16.                   cout<<"This is class D"<<endl;  
  17.                   }  
  18.       };  
  19. int main()  
  20. {  
  21.     B b,*pb;  
  22.     D d,*pd;  
  23.     pb= &b;  
  24.     pb->vf();  
  25.     pb=&d;//满足第1第2条。   
  26.     pb->vf();  
  27.   //pd=&b;//违背第2条。invalid conversion from `B*' to `D*'   
  28.   //pd->vf();  
  29.     pd=(D*)&b;//满足第3条。 这里是多态。  
  30.     pd->vf();  
  31.     pd=&d;  
  32.     pd->vf();  
  33.     system("pause");  
  34.     }  


虚函数可以很好的实现多态,在使用虚函数时应注意如下问题

1.虚函数的声明只能出现在类函数原型的声明中,不出现在函数体实现的时候,而且,基类中只有保护成员或公有成员才能被声明为虚函数。

2.在派生类中重新定义虚函数时,关键字virtual可以写也可以不写,但是在容易引起混乱时,应该写上该关键字。

3.动态联编只能通过成员函数来调用或者通过指针、引用来访问虚函数,如果以对象名的形式来访问虚函数,将采用静态联编。

派生类中重新定义基类中的虚函数,是函数重载的另一种形式,但它与函数重载 又有如下区别:

1.一般的函数重载,要求其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同。

2.重载一个虚函数时,要求函数名、返回类型、参数个数、参数类型和参数顺序都必须与基类中的虚函数完全一致。

3.如果仅返回类型不同,其余相同,则系统会给出错误信息。

4.如果函数名相同,而参数个数、参数的类型或参数的顺序不同,系统认为是普通的函数重载,虚函数的特性将被丢掉。

2.2 多级继承和虚函数

原生态多级继承例子:

[html] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class B  
  4. {  
  5.       public:  
  6.              int ms;  
  7.       public:  
  8.             void set()  
  9.              {  
  10.                   cout<<"Base set"<<endl;  
  11.                   }  
  12.       };  
  13. class D:public B  
  14. {  
  15.       public:  
  16.              void set()  
  17.              {  
  18.                   cout<<"D set"<<endl;  
  19.                   }  
  20.       };  
  21. class DD:public D  
  22. {  
  23.       };  
  24. class DDD:public DD  
  25. {  
  26.       public:  
  27.              void set()  
  28.              {  
  29.                   cout<<"DDD set"<<endl;  
  30.                   }  
  31.       };  
  32. class DDDD:public DDD  
  33. {  
  34.         
  35.       };  
  36. class DDDDD:public DDDD  
  37. {  
  38.       };  
  39. int main()  
  40. {  
  41.     DD d;  
  42.     d.set();  
  43.     DDDD dddd;  
  44.     dddd.set();  
  45.     DDDDD ddddd;  
  46.     ddddd.set();  
  47.     system("pause");  
  48.     }  


上述代码结果说明,如果类中没有对应的成员函数,则会向上一级一级找,直到出现所需要的成员函数。如上面的set。d.set在DD类中无set类,就向上找,找到离自己最近的父类的成员函数。

多级继承可以看做是多个单继承的组合,多级继承的虚函数与单继承的虚函数的调用相同,一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。例如:

[cpp] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class Base  
  4. {  
  5.       public:  
  6.              virtual ~Base(){}  
  7.              virtual void func()  
  8.              {  
  9.                      cout<<"Base func"<<endl;  
  10.                      }  
  11.       };  
  12. class Derived1:public Base  
  13. {  
  14.       void func()  
  15.       {  
  16.            cout<<"Derived1 func"<<endl;  
  17.            }  
  18.       };  
  19. class Derived2:public Derived1  
  20. {  
  21.       void func()  
  22.       {  
  23.            cout<<"Derived2 func"<<endl;  
  24.            }  
  25.       };  
  26. void test(Base &b)//引用方式   
  27. {  
  28.      b.func();  
  29.      }  
  30. int main()  
  31. {  
  32.     Base b;  
  33.     Derived1 d1;  
  34.     Derived2 d2;  
  35.     test(b);  
  36.     test(d1);  
  37.     test(d2);  
  38.     system("pause");  
  39.     }  

上述代码中在析构函数前面加上关键字virtual,则该析构函数就称为虚析构函数。

上述代码扩展例子:

[html] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class Base  
  4. {  
  5.       public:  
  6.              virtual ~Base(){}  
  7.              virtual void func()  
  8.              {  
  9.                      cout<<"Base func"<<endl;  
  10.                      }  
  11.       };  
  12. class Derived1:public Base  
  13. {  
  14.       void func()  
  15.       {  
  16.            cout<<"Derived1 func"<<endl;  
  17.            }  
  18.       };  
  19. class Derived2:public Derived1  
  20. {  
  21.   /*    void func()  
  22.       {  
  23.            cout<<"Derived2 func"<<endl;  
  24.            }*/  
  25.       };  
  26. class Derived3:public Derived2  
  27. {  
  28.       void func()  
  29.       {  
  30.            cout<<"Derived3 func"<<endl;  
  31.            }  
  32.       };  
  33. class Derived4:public Derived3  
  34. {  
  35.       void func()  
  36.       {  
  37.            cout<<"Derived4 func"<<endl;  
  38.            }  
  39.       };  
  40. void test(Base &b)//引用方式   
  41. {  
  42.      b.func();  
  43.      }  
  44. int main()  
  45. {  
  46.     Base b;  
  47.     Derived1 d1;  
  48.     Derived2 d2;  
  49.     Derived3 d3;  
  50.     Derived4 d4;  
  51.     test(b);  
  52.     test(d1);  
  53.     test(d2);  
  54.     test(d3);  
  55.     test(d4);  
  56.     system("pause");  
  57.     }  

注意d2使用的是其父类的成员函数。

在使用虚析构函数时,要注意以下两点:

1.只要基类的析构函数被声明为虚函数,则派生类的析构函数,无论是否使用virtual关键字进行声明,都自动成为虚函数。

2.如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。

一般来说,在程序中最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中用delete运算符准备删除一个对象,而delete运算符操作对象是指向派生类的基类指针,系统会调用相应的析构函数;否则系统只执行蕨类的析构函数,而不执行派生类的析构函数,从而可能导致异常情况的发生。

构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的关联。

补充例子:

[cpp] view plaincopyprint?
  1. #include <iostream>   
  2. using namespace std;   
  3. class A   
  4. {   
  5. protected:   
  6.            int m_data;   
  7. public:   
  8.         A(int data = 0):m_data(data) { }   
  9.         int GetData() { return doGetData(); }   
  10.         virtual int doGetData() { return m_data; }  
  11.  };   
  12. class B:public A   
  13. {   
  14.       protected:   
  15.                  int m_data;   
  16.       public:   
  17.               B(int data = 1): m_data(data) { }   
  18.               int doGetData() { return m_data; }  
  19.  };   
  20. class C:public B {   
  21.       protectedint m_data;   
  22.       public:   
  23.               C(int data = 2):m_data(data) { }   
  24. };   
  25. int main()   
  26. {   
  27.     C c(10);   
  28.     cout << c.GetData() << endl;   
  29.     cout << c.A::GetData() << endl;   
  30.     cout << c.B::GetData() << endl;   
  31.     cout << c.C::GetData() << endl;   
  32.     cout << c.doGetData() << endl;   
  33.     cout << c.A::doGetData() << endl;   
  34.     cout << c.B::doGetData() << endl;   
  35.     cout << c.C::doGetData() << endl;   
  36.     system("pause");  
  37.     return 0;   
  38.     }   

1    //C中getdata未定义,B中也是,所以调用A的。A的dogetdata是虚函数,所以调用B的,返回B::m_data1    //同上1    //同上1    //同上1    //C中dogetdata未定义,调用B的,返回B::m_data0    //直接调用A中的dogetdata,返回A::m_data1    //调用B中doggetdata,返回B::m_data1    //同c.doGetData()Press any key to continue . . .

3.纯虚函数与抽象类

抽象类是一种特殊的类,它提供统一的操作接口。建立抽象类是为了多态地使用抽象类成员函数。抽象类是包含纯虚函数的类。

3.1 纯虚函数

定义形式:

[cpp] view plaincopyprint?
  1. virtual <函数类型><函数名>(参数表)=0;  

纯虚函数的作用是为派生类提供一个统一的接口,纯虚函数的实现可以留给派生类来完成。一般来说,一个抽象类中带有至少一个纯虚函数。

例:

[cpp] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3. class Point  
  4. {  
  5.       protected:  
  6.                 int x0,y0;  
  7.       public:  
  8.              Point(int i,int j)  
  9.              {  
  10.                        x0=i;  
  11.                        y0=j;  
  12.                        }  
  13.       virtual void set()=0;  
  14.       virtual void draw()=0;  
  15.       };  
  16. class Line:public Point  
  17. {  
  18.       protected:  
  19.                 int x1,y1;  
  20.       public:  
  21.              Line(int i=0,int j=0,int m=0,int n=0):Point(i,j)  
  22.              {  
  23.                       x1=m;  
  24.                       y1=n;  
  25.                       }  
  26.              void set(){cout<<"Line_set()"<<endl;}  
  27.              void draw(){cout<<"Line_draw()"<<endl;}  
  28.       };  
  29. class Ellipse:public Point  
  30. {  
  31.       protected:  
  32.                 int x2,y2;  
  33.       public:  
  34.              Ellipse(int i=0,int j=0,int p=0,int q=0):Point(i,j)  
  35.              {  
  36.                          x2=p;  
  37.                          y2=q;  
  38.                       }  
  39.              void set(){cout<<"Ellipse_set()"<<endl;}  
  40.              void draw(){cout<<"Ellipse_draw()"<<endl;}  
  41.       };  
  42. void drawobj(Point *p)  
  43. {  
  44.      p->draw();  
  45.      }  
  46. void setobj(Point *p)  
  47. {  
  48.      p->set();  
  49.      }  
  50. int main()  
  51. {  
  52.   Line *li=new Line();//new Line;  
  53.   drawobj(li);  
  54.   setobj(li);  
  55.   cout<<endl;  
  56.     
  57.    Ellipse *el=new Ellipse();//new Ellipse;  
  58.    drawobj(el);  
  59.    setobj(el);  
  60.    cout<<endl;  
  61.       
  62.    Line *li2=new Line;  
  63.    drawobj(li2);  
  64.    setobj(li2);  
  65.    cout<<endl;  
  66.      
  67.    Ellipse elp;  
  68.    drawobj(&elp);  
  69.    setobj(&elp);  
  70.    cout<<endl;      
  71.      
  72.     system("pause");  
  73.     }   


在上述代码中,类Line和类Ellipse都公有继承自类Point,在该程序中要实现多态,就必须定义set()函数和draw()函数为虚函数。同时,由于点是不需要在应用程序中画出的,因此Point的set()函数和draw()函数定义为虚函数。在主函数main()中,创建Line对象和Ellipse对象后,调用这两个同名的函数即可。这就是虚函数的作用,它一般用于基类函数没有具体操作,而派生类中该函数可能需要的场合。

3.2 抽象类

包含虚函数的类称为抽象类。抽象类是一种特殊的类,是为了抽象和设计的目的而建立的,处于继承层次结构的较上层。抽象类是不能创建对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数声明为保护的访问控制权限。

抽象类的主要作用是将有关的类组织在一个继承层次中,由抽象类来为它们提供一个公共的根,相关的子类是从这个根派生出来的。抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。使用抽象类时应该注意以下问题:

1.抽象类只能用做其他类的基类,不能创建抽象类对象,因为它的纯虚函数没有定义功能,其纯属虚函数的实现由派生类给出。

2.抽象类不能用做参数类型、函数的返回类型或显式转换的类型。

3.可以声明抽象类的对象指针或对象引用,从而可以访问派生类对象成员,实现动态联编。

4.若派生类中没有给出抽象类的所有纯虚函数的函数体,派生类仍是一个抽象类。若抽象类的派生类中给出了所有纯虚函数的函数体,这个派生类不再是一个抽象类,它可以创建自己的对象了。例:

[cpp] view plaincopyprint?
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class Vehicle  
  5. {  
  6.       protected:  
  7.                 float speed;  
  8.                 int total;  
  9.       public:  
  10.              Vehicle(float speed,int total)  
  11.              {  
  12.                            Vehicle::speed = speed;//成员变量和函数参数重名,注意加上::以区分成员变量和函数参数,否则初始化会有问题  
  13.                            Vehicle::total = total;//或者this->total = total;  
  14.                            }  
  15.              virtual void ShowMember()=0;  
  16.       };  
  17. class Car:public Vehicle  
  18. {  
  19.       protected:  
  20.                 int aird;  
  21.       public:  
  22.              Car(int aird,float speed,int total):Vehicle(speed,total)  
  23.              {  
  24.                      Car::aird = aird;  
  25.                      }  
  26.              void ShowMember()  
  27.              {  
  28.                   cout<<"Speed is: "<<speed<<endl;  
  29.                   cout<<"Toatal is: "<<total<<endl;  
  30.                   cout<<"Aird is: "<<aird<<endl;  
  31.                   }  
  32.       };  
  33. int main()  
  34. {  
  35.     Car c(250,150,4);  
  36.     c.ShowMember();  
  37.     system("pause");  
  38.     }