虚函数和多态性

来源:互联网 发布:r 读取股票数据 编辑:程序博客网 时间:2024/05/16 10:45

虚函数和多态性

一、 多态性的含义

多态性是指通过(基类)指针或(基类)引用调用函数,这种调用是动态解析的,即在编译期间确定调用哪个函数。

因为派生类对象包含一个完整的基类对象,换句话说,每个派生类对象也是一个基类对象。因此,可以用基类指针来存储派生类对象的地址。但是,反之就不行。不能用派生类的指针来存储基类对象的地址,因为每个基类对象只表示派生类的一部分。

从上图的派生关系,可以定义一个类Box的指针,来存储每个派生类的地址(包括多重派生类的地址)。例如:

CerealPack breakfast;

Box* pBox=&breakfast;

Carton* pCarton=&breakfast;

Contents* pContents=&breakfast;

这三个语句都是正确的,因为类Box是类CerealPack的间接基类,类Carton和类Content是它的直接基类。

二、 指针的静态类型和动态类型

对于一个基类指针或引用,定义时的类型称为它的“静态类型”,实际指向对象时的类型称为“动态类型”。

在任意时刻,指针pBox都可以包含任何以Box为基类的派生类对象的地址。该指针在声明时的类型是“Box*”,因此它的静态类型是“Box*”。同时,它还具有动态类型。当pBox指向Carton对象时,其动态类型是“Carton*”。当pBox指向ToughPack对象时,其动态类型是“ToughPack*”。在pBox指向Box对象时,其动态类型和静态类型相同,是“Box*”类型。

编译器根据基类指针的动态类型来调用合适的虚函数版本,这个就是多态性最重要的内容。

三、多态类的含义

当一个类至少包含一个虚函数时,称这个类具有多态性,或说这个类是多态类。获得多态性有两种方式:从基类继承虚函数和自定义一个虚函数。通过这两种方式,都可以获得多态性。

四、静态绑定和动态绑定

㈠静态绑定和动态绑定的定义

静态绑定又叫静态解析、静态调用,是指在源程序中确定的调用关系。很经典的例子就是类对象调用自己的成员函数。

动态绑定又叫动态解析、动态调用,是指在程序编译期间确定的调用关系。

㈡静态绑定和动态绑定的条件

动态调用的两种情况:

⒈被调函数必须是虚函数,并且必须通过基类指针或者基类引用来调用是动态调用。

⒉如果通过this指针间接调用虚函数,也是动态调用。

对两种情况各举一个例子。假设pBox是基类指针,rBox是引用形参,volume()是一个虚函数,

    pBox->volume();      //通过基类指针直接调用虚函数是动态调用

    rBox.volume();       //通过引用形参直接调用虚函数是动态调用

又如函数showVolume()定义为:

     void showVolume(){cout<<volume()<<endl;},那么

    pBox->showVolume();

    rBox.showVolume();

    hardcase.showVolume();

这三种情况都是通过showVolume()间接调用虚函数volume(),都会正确地调用各自的volume()。可以认为是通过this指针的动态调用,因为在函数showVolume()内部会表示成cout<<this->volume()<<endl;

综上,可以得出结论:

动态调用必须是针对虚函数的调用,不能是针对一般成员函数的调用。并且,就算把成员函数声明(定义)为虚函数,还要满足用基类指针或基类引用直接调用,或者是通过this指针间接调用这两个条件才能实现动态调用。

㈡静态调用的条件

除了上述满足动态调用条件的调用,其它的都是静态调用。有一点是肯定的:对一般成员函数(不是虚函数)的调用必定是静态调用。另外,还有几种情况,即使调用虚函数,也会使用静态调用,这会后面详细说明。

五、虚函数的定义

㈠虚函数的定义方式为:

    virtual 返回值类型 函数名(){

              函数体

            }

如果类定义中只包含函数声明,那么关键字virtual不能出现在函数定义中,而应该在声明中。

把一个函数声明为虚函数后,所有以该类为基类的派生类都会继承该虚函数。这里说明下:派生类中仍然包含的基类的那个虚函数,只是它被同名的派生类虚函数隐藏了。这点和一本成员函数出现重名的屏蔽情况相同。(可以参见类的继承章节。)

对虚函数的动态调用取决于基类指针、基类引用以及this指针的动态类型。

㈡虚函数的定义规则

虚函数定义有个规定:如果在基类中把一个函数定义为虚函数,那么在它的所有派生类中具有相同返回值类型和函数签名(包括关键字const)的函数也被认为是虚函数。

如果在派生类中,函数的返回值类型和函数签名有任何一个与基类的虚函数不同,就不会把派生类的函数认为是虚函数,在调用时就会执行静态调用。

㈢虚函数与类层次结构

在一个类中把某个函数声明为虚函数后,该函数在所有派生类(直接派生或间接派生)中都是虚函数。即使在派生类中没有声明虚函数,它也可以通过继承基类的虚函数,获得多态性。

㈣访问指定符和虚函数

基类中虚函数的访问指定符和继承方式共同决定了派生类中相应虚函数的在动态调用时的访问状态。见下表。

基类虚函数访问指定符

继承方式

派生类中相应虚函数的访问状态

是否允许调用

是否允许外部调用

public

public

public

protected

protected

private

private

protected

public

protected

protected

protected

private

private

private

all

private

注:all表示publicprivateprotected三种状态。 

从上表可以看出,继承方式对动态调用有影响,因此如果要使用动态调用,最好把继承方式设置为public

六、虚函数的默认参数值

如果虚函数在基类声明中带有默认参数值,那么在动态调用中,总是使用基类声明中的默认值,不会使用派生类中的虚函数的默认值。

七、调用虚函数时的静态调用

有如下几种情况会静态调用虚函数:

先设pBox是基类指针,rBox是基类引用形参,volume()是虚函数。

⒈使用类名和作用域运算符来强制调用指定类的虚函数。例如:

pBox->Box::volume()-pBox->volume();   //强制调用基类的volume()    ——①

rBox.Box::volume();    //强制调用基类的volume()

这就强制调用了Box类的volume()

①这个例子可以证明基类的虚函数在派生类中得到了继承。因为在此并没有定义基类的对象,pBox不可能是调用基类对象的volume()函数,只可能是调用对象hardcasevolume()

⒉使用对象名和成员运算符来调用虚函数时总是静态调用。例如:

hardcase.volume();     //显式调用对象hardcasevolume()

另外,用基类指针来调用一般成员函数(非虚函数)总是静态调用。

八、指针类型在派生关系中的转换

对一个指向类类型的指针,它可以向上转换成基类类型,也可以向下转换成派生类类型。先解释向基类类型转换的情况,后解释向派生类类型转换的情况。

㈠派生类指针转换成基类指针

派生类指针可以隐式转换成基类(直接基类或间接基类)指针类型,并且总是合法的。转换的意义如图所示:

由图可以看出两点:⒈从派生类向基类转换指针类型总是合法的;⒉转换后指针指向的位置有所不同。例如:pPack在转换成pCarton后,虽然pPackpCarton都是指向同一对象,但是pPack是指向的CerealPack对象的开头部分,而pCarton是指向CerealPack对象的Carton部分。同理,当pPack转换成pBox后,pBox是指向的CerealPackBox部分。

尽管指向的位置略有不同,但是不妨碍调用这个对象(毕竟都是指向该CerealPack对象)。

㈡基类指针转换成派生类指针

基类指针不能隐式转换成派生类指针,如果需要这样转换,必须采用强制转换的方式,并且,不能保证转换一定成功。例如,下图中,pBox可以强制转换成Carton*类型和,但是如果继续向下转换,就会失败(因为没有更往下的派生类型了)。

注:根据派生关系可以画出一幅派生层次图,从上面可以清晰地看出基类与派生类的派生关系。因此,称从派生类到基类的走向为沿着派生关系(层次)向上,反之,称为沿着派生关系(层次)向下。

㈢动态强制转换

动态强制转换是在程序运行期间进行的,静态强制转换是在程序编译期间进行的。要制定动态强制转换,必须使用dynamic_cast()运算符。

dynamic_cast运算符只能作用于多台类型的指针或引用,即指针指向的类型必须包含虚函数。另外,动态强制转换的类型必须是同一类层次结构中的类指针或引用。关于这一点,后面详细说明。

动态转换有两种基本转换方式:第一种是基类转换成派生类,称为downcast;第二种是从一个基类转换成另一个基类(多重继承的情况下),称为crosscast。如图所示。

downcast转换

downcast转换很简单,对于pBox指针,直接使用dynamic_cast运算符即可:

Carton* pCarton=dynamic_cast<Carton*>(pBox);

这个运算符的使用方式和static_cast()相同。尖括号中式要转换成的目标类型,括号中的是要转换的操作对象。转换成Carton*指针后,该指针可以指向Carton类型对象和Carton类型的派生类型对象。如果转换失败,dynamic_cast()会返回为“空指针”。

crosscast转换

交叉转换要求要转换成的目标类型是多重继承中的另一个基类,即在多重继承中不同基类之间的转换。转换形式仍然是:

Contents* pContents=dynamic_cast<Contents*>(pBox);

这里要特别说明下,虽然转换成了Contents*类型,但是这个指针是不能存储Contents类型对象的地址的!Box*类型的指针pBox包含的是Box类型对象的地址,而Contents*类型的指针包含的是Contents类型对象的地址。要在Contents*指针中存储Box类型对象的地址,有且只有一种可能——这个对象既是Contents对象,又是Box对象。因此,转换后的指针只能存储两个基类共同的派生类对象的地址(在这里是CerealPack对象)!

无论是上面哪种转换,都有可能出现失败。如果动态转换失败,dynamic_cast()的返回值是0(空指针)。因此,在使用之前,先要测试该表达式是否为空:

if(Carton* pCarton=dynamic_cast<Carton*>(pBox))

             pCarton->surface();

九、动态调用的实质

包含虚函数的类需要额外的内存,这是因为在多态类的对象中,生成了一个特殊的指针和一个虚函数指针表。这个虚函数指针表的每个数据项都指向该对象的一个虚函数,通常称这个虚函数指针表为vtable;这个特殊的指针没有名称,但它指向vtable,可以称它为pvtable。因此,假设有一个动态调用:

pObject->doMore();

其实质的调用过程如图所示:

从图中可以看出,基类指针pObject先调用了指针pvtablepvtable有调用了表vtable中的相应虚函数指针,进而最终用虚函数指针调用相应的虚函数。其过程可以表示为:

pObjectpvtable→相应虚函数指针→虚函数。

当然,间接调用时,只需把pObject换成this指针即可。

十、纯虚函数和抽象类

㈠纯虚函数

纯虚函数是指没有定义的虚函数,它只有一个函数声明,没有函数定义。例如,要将虚函数move()声明为纯虚函数,必须采用如下形式:

virtual void move(const Point& newPosition)=0;

切记:纯虚函数没有函数定义,只有一个函数声明。“=0”在函数头的末尾,它是纯虚函数的标识。

说明:

一般把基类中的虚函数声明为纯虚函数,因为往往基类的主要用来派生新类型,不用来创建对象,基类中的虚函数就没有定义的必要,所以把它声明为纯虚函数。

㈡抽象类

包含纯虚函数的类称为抽象类。因为抽象类中包含纯虚函数,它是一个不完整的类型定义,所以它在使用时有两个个限制:

① 不能创建对象,作为一般变量的类型;

② 不能用作返回值类型和函数的形参类型。

但是,可以定义抽象类的指针和引用,并且它们可以用作参数或返回值类型,这点举例说明。

定义抽象类Box

     class Box{

       public:

        Box(double a=1.0,double b=1.0,double c=1.0);     ——②

        virtual double volume() const=0;       //declare a pure virtual function, not define

        protected:

        double length;

        double width;

        double height;

};

因为抽象类不能定义一般对象,所以这里定义Box类的指针:

Box* pBox;

可以把“Box*”和“Box&”用作参数类型或返回值类型,但不能把“Box”用作参数和返回值类型。

②抽象类的构造函数只能由派生类对象的构造函数或成员函数调用,用来创建并初始化当中的基类部分的成员。

注意:

① 因为抽象类的构造函数一般不能被使用,所以最好把它声明为protected

② 如果调用纯虚函数,结果是不确定的。

③ 如果抽象基类的纯虚函数没有在派生类中被重定义,那么这个纯虚函数就会被原封不动地继承下来,使派生类也变成一个抽象类。

十一、虚析构函数

默认情况下,析构函数的调用也是静态绑定的。比如定了基类Vessel,派生类BoxCan,类Box的派生类CartonToughpack,结构层次如图:

再定义一个基类指针数组,用于存储派生类对象的地址,采用动态分配内存的方式:

  Vessel* pVessels[]={new Box(40,30,20),new Can(10,3),new Carton(40,30,20)};

当在循环中删除这些动态分配内存时(delete pVessels[i]),它们都会默认地静态调用~Vessel(),而不是正确地调用各自的析构函数。为了解决这个问题,必须在基类Vessel中将析构函数声明为“虚析构函数”。

虚析构函数的声明方式只需在析构函数前加关键字virtual即可,例如:

virtual ~Vessel();

这样声明之后,以类Vessel为基类的所有派生类中的析构函数都会变成虚析构函数,哪怕它们的函数名并不相同。这下再删除动态分配的内存时,就会正确调用每个对象的析构函数了。

注意:

虚析构函数只需要把基类析构函数声明为虚函数,此后所有派生类中的析构函数都会变成虚函数。因为派生类的析构函数不可能与基类析构函数同名,这是实现虚析构函数的唯一方式。

十二、typeid()运算符

由于基类指针可以包含任何派生类型的对象的地址,当需要知道当前该指针或引用的动态类型时,可以使用typeid()运算符。

typeid()运算符用来确定基类指针或引用的当前动态类型。要使用该运算符,必须包含头文件<typeinfo>typeid()运算符返回type_info类型的对象,并且它实现了“==”运算符,允许比较两个type_info对象。

typeid()的使用方式为:

typeid(表达式);

表达式可以是某类类型,基类指针或基类引用。

由于返回的是一个type_info类型的值,不能直接知道它的数据类型,所以常用if语句测试它的具体类型。比如:

if(typeid(*pVessel)==typeid(Carton))      ——③

            cout<<”Pointer is type Carton”<<endl;

        else

            cout<<”Pointer is not type Carton”<<endl;

③如果表达式是指针,必须解除它的引用,即使用“*指针”的形式。

十三、类的成员指针

可以定义指向类的成员的指针,这种指针可以类的数据成员或成员函数。这里要特别强调:类的成员指针它指向的是类的成员,而不是类对象的成员。它只与类有关,与类的对象无关,这有点像类的静态成员。如图:

类成员指针指向的是类当中的成员,不是一个具体的数据项,只有当类成员指针与类对象连用时,才具体表示一个确切的内存地址。

㈠数据成员指针

指向类数据成员的指针称为数据成员指针。它的定义方式为:

数据类型 类类型::* 指针名;

例如:

double Box::* pData;

pData=&Box::width;

可以认为数据成员指针不包含具体的内存地址,也可以认为它包含了一个半截地址,即某一个成员的地址。但是,引用某一个成员必须要一个完整方式——“对象名+成员名”,即使是使用指针引用,也是完整引用,指针中包含了完整的对象地址。所以这里的pData是不能单独使用的。

    要使用成员指针,必须使用成员指针运算符“.*”。它是成员运算符“.”和指针运算符“*”的组合。使用方式为:

对象.*成员指针;

例如,myBox.*pData就等价于myBox.width

当然,还有供指针使用的指向指针运算符“->*”。使用方式为:

指针->*成员指针;

例如,pBox->*pData就等价于pBox->width

注意:

① 数据成员指针的类型是“数据类型 类类型::*”,与普通指针有很大区别。

② 可以用typedef运算符将数据成员指针的类型定义一个别名,采用:

                  typedef double Box::* pBoxMember;的样式。

③ 成员指针同样可以包含派生类成员(非对象)的地址。

㈡成员函数指针

用于存储成员函数地址的指针称为成员函数指针。定义形式为:

返回值类型 (类名::*指针名)(形参表);

如果成员函数声明为const,成员函数指针也必须在末尾用关键字const。例如:

virtual double volume() const;

double (Box::*pGet)() const;   //相应的函数指针

可以使用typedef定义一个别名,方便成员函数指针的使用,方式为:

typedef 返回值类型 (类名::*别名)(形参表);

这里需要注意的是,别名是在括号中函数指针的位置,要注意区分。

成员函数指针的调用方式也有点特别,例如:

(myBox.*pGet)();

(pBox->*pGet)();

函数调用运算符()的优先级比.*->*高,如果不加括号,函数调用运算符()就会加到pGet上,使之变成一个函数调用——myBox.*(pGet())

可以这样对这种调用方式作出理解:

因为pGet存储的是函数名,在这里是pGet=getLength,那么myBox.*pGet等价于myBox.getLength,再加上函数的标识括号,就成了myBox.getLength()。过程如下:

pGetgetLength

myBox.*pGetmyBox.getLength

(myBox.*pGet)()(myBox.getLength)()myBox.getLength()

最后举一个把成员函数指针作为函数形参的例子。

      typedef double (Box::*pBoxFun)() const;

   class Box{

      public:

      double sideArea(pBoxFun pGetside1,pBoxFun pGetside2){

      return (this->*pGetside1)()*(this->*pGetside2)();

};

使用时,实参的赋值方式为:

     myBox.sideArea(&Box::getHeight,&Box::getLength);

补充:

继承方式对动态调用有影响,但影响改为不明确;

原创粉丝点击