多态虚函数

来源:互联网 发布:linux 开启端口号 编辑:程序博客网 时间:2024/05/18 03:00

多态虚函数

 多态性 

       当不同的对象接受到相同的消息名(或者说当不同的对象调用相同的名称的成员函数)时,可能引起不同的行为(执行不同的的代码)。这种现象称为多态性。将函数调用链接上相应函数体的代码的过程称为函数联编(简称联编)。在C++中,根据联编时刻不同,分为静态联编和动态联编。

        不同的类可以有相同名称的成员函数(甚至还有可以相同的参数).但是编译器在编译时就对它们进行函数联编。这种在编译时刻进行的联编称为静态联编。静态联编所支持的多态性称为编译时多态性。函数重载就属于编译时多态性。

          动态联编中,在程序运行时才能确定调用哪个函数。这种在运行时的函数联编称为动态联编。动态联编所支持的多态性称为运行时多态性。在C++中,只有虚函数才可能是动态联编的。可以通过定义类的虚函数和创建派生类,然后在派生类中重新实现虚函数,实现具有运行时的多态性。

函数重载

    函数重载是C++获得多态性的途径之一.在C++中,两个或两个以上的参数说明不同的函数可以共享同一个名字.在这种情况下,共享同一名字的函数叫做被重载,而这个过程叫做函数的重载.为了说明函数重载的重要性,先考虑一下实际上在所有C编译器程序的标准库中能找到的三个函数:abs()、labs()和fabs().函数abs()返回一个整数的绝对值,labs()返回一个长整数的绝对值,fabs()返回一个双精度数的绝对值.尽管这三个函数处理几乎完全相同的事情,但是在C中却要用三个稍有不同的名字来表示三个基本相似的任务.这就使情况从概念上讲变得比实际更为复杂.尽管每个函数的基本概念是相同的,但程序员不得不记住三件事.但是,在C++中,这三个函数只用一个名字,如下所示:
       #include"iostream.h"
       //abs is overloading three ways
       int abs(int i);
       double abs(double d);
       long abs(long l);

       main()
       {
           cout<<abs(-10)<<"\n";
           cout<<abs(-11.0)<<"\n";
           cuot<<abs(-9l)<<"\n";
           return 0;
       }
       int abs(int i)
       {
             cout<<"using integer abs()\n";
             return i<0?-i:i;
       }
       double abs(double d)
       {
             cout<<"using double abs()\n";
             return d<0.0?-d:d;
       }
       long abs(long l)
       {
            cout<<"using long abs()\n";
            return l<0?-l:l;
       }
      该程序创建了三个相似但不相同的名为abs()的函数,每一个返回其变元的绝对值.编译程序根据变元的类型决定在每一种情况下需要调用哪个函数.重载函数的价值在于允许用一个相同的名字存取相关的函数的集合.因此abs()代表了将要执行的一般动作.留给编译程序的任务就是在一个特定的环境下选择一个正确的具体形式.程序员只需记住要执行的一般动作.正是由于多态性,要记住三件事减为只需记住一件事了.这个例子非常平常,但是如果拓展这个概念,就会发现多态性是如何帮助用户管理非常复杂程序的.
   一般来说,要重载一个函数,只需说明它的不同形式.编译程序会做好剩下的事情.重载函数时我们必须遵守一个重要的限制:两个函数可能仅仅在它们返回的类型上不同.它们必须在它们的变元类型或数量上不同.(返回类型不能提供足够的信息让编译程序判断使用哪个函数).下面是另一个使用函数重载的例子:
  #include"iostream.h"
  #include"stdio.h"
  #include"string.h"

void stradd(char *s1,char *s2);
void stradd(char *s1,int i);

main()
{
char str[80];
strcpy(str,"Hello");
stradd(str,"there");
cout<<str<<"\n";

stradd(str,100);
cout<<str<<"\n";

return 0;
}

//concatenate two strings
void stradd(char *s1,char *s2)
{
strcat(s1,s2);
}

//concatenate a string with a "stringized" integer
void stradd(char *s1,int i)
{
char temp[80];
sprintf(temp,"%d",i);
strcatI(s1,temp);
}
     在这个程序中,函数stradd()被重载.一种形式把两个字符串连接起来(同strcat()),另一种形式把一个整数转换为字符串,再把它添加到一个字符串的尾部.这里,重载被用于创建了一个既能把一个字符串又能把一个整数附加到另一个字符串之后的接口.
   用户可以用相同的名字重载不相关的函数,但最好不要这样做.例如,可以用名字sqr()创建返回整数的平方和双精度数的平方根的函数.但是,这两种运算是根本不同的,象这样应用函数重载就使全部的意图失败了(这是很糟糕的程序设计风格).实际上,用户只应重载那些密切相关的操作.

 

虚函数

 

     基类中说明为virtual并在派生类中重定义的函数.为了说明了一个函数为虚函数,需在其说明的前面加上关键字virtual.派生类中函数的重定义忽略基类中函数的定义.从根本上讲,基类中说明的虚函数在很大程度上扮演了说明一般类行为和规定接口的位置持有者角色.派生类对虚函数的重定义指明了函数(算法)执行的实际操作.
   正常存取时,虚函数同任何其它类型的类成员函数完全一样.使虚函数显得如此重要能支持运行时的多态性的原因,就在于当通过指针存取时它们的所作所为.一个基类指针能用来指向从该基类派生的任何类.当一个基类指针指向包含虚函数的派生对象时,C++根据该指针指向的对象的类型决定调用函数的哪一种形式.因此,当它指向不同的对象时,就执行不同形式的虚函数.在讨论更多的理论之前,先看看下面的例子:
#include"iostream.h"
class base{
public:
virtual void vfunc(){
cout<<"This is base's vfunc()\n";
}
};

class derived1:public base{
public:
void vfunc(){
cout<<"This is derived1's vfunc()\n";
}
};

class derived2:public base{
public:
void vfunc(){
cout<<"This is derived2's vfunc()\n";
}
};

main()
{
base *p,b;
derived1 d1;
derived2 d2;
//point to base
p=&b;
p->vfunc(); //access base's vfunc()
//point to derived1
p=&d1;
p->vfunc(); //access derived1's vfunc()
//point to derived2
p=&d2;
p->vfunc(); //access derived2's vfunc()

return 0;
}
    该程序显示如下:
This is base's vfunc()
This is derived1's vfunc()
This is derived2's vfunc()
     如程序所示,在base里说明了虚函数vfunc().注意,关键字virtual放在函数说明的前面.在derived1和derived2重定义vfunc()时,不再需要关键字virtual(但如果在派生类中重定义虚函数时包含了它也不算错).
     在这个程序里,base被derived1和derived2继承,在每一个类定义中,vfunc()都被重定义.在main()里,说明了四个变量:
        名称                类型
         p                基类指针 
         b                基类对象
         d1              derived1的对象
         d2              derived2的对象
     接着,把b的地址赋给p并通过p调用vfunc().由于p指向类型base的对象,所以执行与此对应的vfunc()形式.然后,把p设置为d1的地址,并通过p再次调用vfunc().这次p指向类型derived1的对象.这导致执行derived1::vfunc().最后,把d2的地址赋给p,且p->vfunc()导致执行在derived2中重定义的那个vfunc()形式.这里的关键就是:p所指的对象的类型决定了执行vfunc()的哪个形式.此外,在运行时也可以得出这个结论,且这个过程形成了运行时多态性得基础.
    注:虽然可以通过使用对象名和点运算符这种普通方式调用虚函数,但只有在通过基类指针存取时才能获得运行时得多态性.例如,拿前面的例子来说,下面的句子在语法上是合法的:
         d2.vfunc(); //calls derived2's vfunc()
    虽然象这样调用虚函数没有错误,但它没有充分利用vfunc()的虚拟特性.
     派生类对虚函数的重定义类似于函数重载.因为它们之间存在着一些差别,所以这个术语不适于虚函数重定义.大概最重要的是,重定义的虚函数的原型必须和在基类中指定的原型完全匹配.这不同于重载一个普通函数.在普通函数里,返回类型以及参数的个数和类型都不同(实际上,在重载一个函数时,函数的个数和类型都必须不同!正是通过这些区别,C++才能选择重载函数的正确形式).重定义虚函数时,其原型的所有方面都必须相同.当试图重定义一个虚函数时改变其原型,C++编译程序就只简单重载函数,因此其虚拟特性就会失去.另一个重要的规定是,虚函数必须是它所属类的成员,而不能是友员.最后,构造函数不能是虚函数,但析构函数可以是.
    鉴于这些限制以及函数重载和虚函数重定义之间的差异,我们用"越位"(overriding)来描述派生类对虚函数的重定义.

 纯虚函数

     在基类中,可以定义若干虚函数,派生类能继承它们,并可对其做修改.然而,基类中虚函数的定义不是必须的,可以用初始化符=0来代替它的函数定义,此时虚函数就成为了纯虚函数 如:
   class X
   {
           //...
           public:
           virtual void print()=0; //print()为纯虚函数
           //...
   };
    类X中的print()函数被定义成纯虚函数的类叫抽象基类.由于纯虚函数不做任何定义,抽象基类中含有"空"纯虚函数方法,因此,抽象基类只能作为别的类的基类使用,而不能生成抽象基类的对象.例如:
     X xi; //出错:X是抽象基类
应注意,纯虚函数与定义为空的函数是不同的,例如:
class XX
{
public:
virtual void print(){}
};
XX不是抽象基类.虚函数print()已有了定义,只不过定义体为空而已,因此,定义:
        XX xi; //正确
是正确的.
    从抽象基类可派生出其他的类.如果派生类对所有虚函数都有定义,就可以从派生类
生成实例.否则,派生类仍为一抽象类,无法生成它的实例.例如:
class Y:public X{
//...
public:
void print()
{
//...
}
};
Y定义了基类中的纯虚函数,因而可由它生成实例:
   Y y; //正确
然而,如果派生类一次也不定义基类的纯虚函数,生成派生类的实例是非法的:
  class Z:public X{
      //除了void print()以外的其他定义
  };
  Z zl; //出错,Z为抽象类

 虚析构函数

 

      在C++中,构造函数不能声明为虚函数.这时因为编译器在构造对象时,必须知道确切类型,才能正确的生成对象,因此,不允许使用动态束定;其次,在构造函数执行之前,对象并不存在,无法使用指向此此对象的指针来调用构造函数.然而,析构函数却可以声明为虚函数.
例如:
class X{
   private:
      char *p;
   public:
      X(int sz){p=new char[sz];}
      virtual ~X(){delete []p;}
};
例中,析构函数被声明成了虚函数.现在,从类X派生一个类:
class Y:public X{
   private:
      char *pp;
   public:
      Y(int sz1,int sz2):X(sz1)
      {
            pp=new char[sz2];
       }
      ~Y(){delete []pp;}
};
由于基类中的析构函数被声明为虚函数,它的派生类的析构函数自动成为了虚函数.虚析构函数可以动态确定实际应使用哪一析构函数,这很重要,例如:
   X *px=new Y(10,12);
   //...
   delete px;
    如果析构函数不是虚函数,在删除px所指的Y对象时,它将根据px的静态类型,调用X::~X(),结果只删除了X::p数组,而无法删除Y::pp数组.反之,由于析构函数被声明为了虚函数,在删除px所指对象时,它将根据pp所指对象的动态类型,调用Y::~Y()来删除Y::pp数组,然后再调用Y的基类的析构函数X::~X(),把X::p数组也删除.
     由上例可以看出,虚析构函数能保证派生类析构函数的正确调用.因此,当你试图用基类指针删除派生类对象时,应将其析构函数声明为虚析构函数.

原创粉丝点击