(十六)多态----核心机密,小心应付+

来源:互联网 发布:淘宝网店转让安全吗 编辑:程序博客网 时间:2024/05/09 10:26

1、多态性常用“虚函数”来描述。它们总是在调用函数时使用对象的指针或对象的引用。

     而多态性仅用于共享一个基类的类层次结构,因此:“从一个类派生另一个类”是多态性的基本条件。


2、基类指针指向派生类指针。

      因为派生类包含一个基类的子对象,即派生类对象也是基类对象。因此,基类指针可以指向(存储)派生类对象的地址。

     甚至可以使用任何直接或间接基类的指针存储派生类对象的地址。

#include <iostream>using namespace std;class B0{public:void display(){cout<<"B0::display()"<<endl;}//virtual void display(){cout<<"B0::display()"<<endl;}};class B1: public B0{public:void display(){cout<<"B1::display()"<<endl;}};class D1: public B1{public:void display(){cout<<"D1::display()"<<endl;}};void fun(B0 *ptr){ptr->display();}int main(int argc, char *argv[]){B0 b0;//声明B0类对象B1 b1;//声明B1类对象D1 d1;//声明D1类对象B0 *p;//声明B0类指针p=&b0;//B0类指针指向B0类对象fun(p);p=&b1;//B0类指针指向B1类对象fun(p);p=&d1;//B0类指针指向D1类对象fun(p);return 0;}

      从上可以看出基类指向了派生类,或者说,基类指针存储了派生类的指针。

       反过来,就不一定成立了,因为派生类的指针可能指向了一些不属于基类的成员,因为如果用派生类指针存储基类的地址,那这个指针

      因为是由基类的(不包含有派生类的成员),这时如果用这个派生类指针指向派生类新增的成员就会出现错误。


       至于为什么说基类指针指向是派生类?是因为,派生类对象包含了基类的成员,那么把地址赋值给基类,这时这个基类指针就相当于指针

       指向了派生类,这是永远成员的。



3、动态类型,和动态绑定相关,只有在程序运行时才最终决定它的类型,而静态类型是在编译时就决定了其类型相当于静态绑定,它的类型

     是一直固定不变的。这种在程序执行前就固定下来的也叫”静态解析“、”静态绑定“、或”早期绑定“。

      动态绑定是一个强大的机制,我们常不能事先确定要处理哪种类型的对象,只能在运行时才知道,用多态性(动态绑定)就可以轻松来解决

      这个问题,多态性一般用于交互式应用程序,输入的类型取决于用户一时的兴致。

       因此:

       通过静态解析的基类指针来调用函数,都只会调用基类的函数。

        但是通过动态绑定的基本指针来调用函数,这取决于当时指针指向的对象类型,指向谁就调用谁。

       函数通过指针的静态调用仅决定于指针的类型,而不取决于它指向的对象。如果是动态的,则取决于它指向的对象。


4、虚基类: 这是为了解决二义性,防止出现两个基类子对象而出的定义。


5、虚函数:为了实现动态绑定,虚函数在声明时用virtual,表示这个函数是动态的。它是实现多态性的关键。

      因此,想把类描述成多态性,意味着它是一个至少包含一个虚函数的派生类。

      虚函数在内存中只表示为一个地址(指针),它指向一个虚表(vtable),虚表把每个派生类中的虚函数地址列入其中。当调用虚函数时,

       一由这个指针引导到虚表中;二在虚表中根据偏移量(派生类决定)查找是哪一个表项(表项就是一个虚函数的地址);三由这个地址

      去调用对应的虚函数。

      因此,如果两个类A,B成员完全一样,但A中有虚函数,那么A对象的大小必定大于B对象(多4个),多出的4就是那个指针(地址,4个字节)

      注意无论一个类有多少虚函数,它有且只有一个虚表。

      

       虚函数的版本可以各自相同也可不同,基类中须用virtual指明,派生类中可以不指明,但为了阅读最好全部都加上。

       因此,在基类中声明了virtual,派生类中同名都是虚函数


       注意:

        一、要求虚函数的情形

        在任意派生类和基类中定义时,必须有相同的名称和参数,且如果基类函数声明为const时,派生类函数也必须声明为const,否则就会被认为是

        两个不同的函数。

        而且,一般情况下,虚函数在派生类中的返回值类型也必须与基类的相同,但当基类中返回类型是类类型指针可引用时例外(此时称“协变性”)。

        否则,派生类函数就不会编译。

        另外一个限制是:虚函数不能是模板函数。


         2、虚函数和类层次结构。

         虚函数不是只有基本中才有,在派生类中也会出现,对于后继派生的就会出现多个虚函数的情况,因此虚函数在层次中对下面所有派生有影响。

         当出现断层时,就寻找上层最近可访问的来替代。如:

          A->B->C->D   箭头表示公有派生方向

          其中A,B,D有公有虚函数fun,
          那调用C中的虚函数,应该是A中版本还是B中版本?
          答案是:B    因为向上寻找最近可访问的,向上最近的就是B

          当然可以用访问符来限制它比如: c.A::fun();那么它就使用A中的版本。


          用virtual来声明虚函数的本质:就是告诉编译器,这个函数不是静态绑定,它是一个虚函数,它会动态绑定。


          3、访问指定符和虚函数

            只要虚函数在基类声明中为public,那么:无论在派生类中的访问指定符是什么,都可通过基类的指针(或引用)为所有的派生类进行调用。

#include <iostream>using namespace std;class A{public:virtual void fun(){cout<<"A"<<endl;}};class B:public A{private:virtual void fun(){cout<<"B"<<endl;}};class C:public B{protected:virtual void fun(){cout<<"C"<<endl;}};int main(int argc, char *argv[]){A a,*p=&a;B b;C c;p=&b;p->fun();//显示B p=&c;p->fun();//显示C return 0;}

            难道是因为基类中的访问指定符,说明了它在虚表中都是公有的?

            修改一下A中为protected或private,果然下面无论是什么属性,都会提示出错。说明基类虚函数的访问指定符决定了虚表的访问。

           正确的答案是:多态情况下,基类虚函数的“访问指定符”会被所有派生类继承。因此与派生类的访问指定符无关,派生类中显示的

              访问指定符只是影响静态限定的作用。                               


          假定A,B中全是公有成员,

class A{public:virtual void fun(){cout<<"A"<<endl;}};class B:public A{public:virtual void fun(){cout<<"B"<<endl;}};int main(int argc, char *argv[]){B b;A *pa=&b;pa->fun();//正确 Bpa->A::fun();//正确 A pa->B::fun();//错误 return 0;}

         pa->fun();通过动态联编,由指向的对象b来决定调用哪一个函数。

         pa->A::fun();特定指明是调用的A中的fun函数,所以结果是A

         pa->B::fun() ;  出错。因为B中的fun并不是A的成员,那你会说为什么前面的fun也不是A的成员怎么正确呢?

         原因:通过指针进行函数的调用,要么是所指向的类函数成员的静态调用,要么是对虚函数的动态调用。

                    只有这两个种情况,首先特定了B::因此肯定不是动态调用,因此它只是能是静态调用,而静态调用只能由

                    指针的类型来决定,指针是A类,而A类并没有B::fun()的成员,所以会出错,程序不会编译。


 6、虚函数的默认值。很奇特的一个现象!!!

       按上面的道理 ,不同派生对象对应不同版本的虚函数,处理不同的默认值。

       但是,因为默认值是在编译期间处理,所以在虚函数中参数使用默认值会出现奇怪的现象。

       如果虚函数在基类的声明时带有默认值,而该函数在派生类中的默认参数就不起作用。

       当然如果是本类对象调用,就会起作用,因为这个时候是用的静态绑定。

        尽管虚函数是动态的,但它的默认参数却是静态的,因此其在派生类中的默认参数将被忽略。


7、引用来调用虚函数。

      void show(Box&  aBox){   aBox.volume();  }

      不同的对象传引用,由此来调用虚函数,实现多态性。

      

8、调用虚函数的基类版本

      前面是基类来调用下面派生的。现在反过来有两种方法实现:假定A公有派生B

      一是派生类对象转换为基类对象,再来调用基类的虚函数。

           B  b;

           A   a=b;//或 A a;   a=static_cast<A>(b);

          a.fun();// 静态绑定,所以调用的是a,注意与下面中的区别,下面是动态,必须加限定A::,否则将调用B。

           因为对象调用函数永远是静态绑定,不论这个函数是不是虚函数

      二是派生类对象的指针转为基类指针再来调用基类虚函数。

B  b;A *p=&b;p->A::fun();//正确,若p->fun则调用的是B(多态) p->B::fun();//错误,B::fun()不是A中成员 

          因为一旦用类名进行标识函数,那么这就是静态绑定,所以出错。前面也说过,指针调用函数有两种,一种是动态调用虚

          函数,这里显然不是(因为加了标识符),另一种是指针所指向函数的静态绑定,这里是这种情况,静态绑定时,而A中无引成员,出错。


9、基类与派生类对象与指针的转换:

      派生类的对象可以给基类的对象赋值,即对象向上赋值。因为派生类包含基类子对象,反过来不行,因为派生类中的新增成员,基类对象是无法提供。

      派生类的指针可以给基类的指针赋值,即指针向上赋值。同上。

      特别注意:指针有时还可以向下赋值,即基类指针转为派生类指针,它有限制:

                          基类指针静态强制转为派生类指针,基类指针必须指向派生类的基类子对象。什么意思呢?

                          


10、纯虚函数:没有代码的虚函数,称为纯虚函数。

       为什么会有没有代码呢?因为,为了实现大量的派生类中的虚函数,特别地为它设置一个基类,这个基类中的虚函数是没有代码的,这给下面派生的

                                                  虚函数指代多态特性。例如:作图有画圆、圆矩形等,都会有draw()函数,因为把它设置为虚函数,并设置一个基数。在基类中

                                                  draw是没有什么实现代码的,只有在画圆、画矩形中才有代码实现。这个draw()就是一个纯虚函数。

       声明:在基类中虚函数后用=0;来表示是没有代码的纯虚函数。如:     virtual void draw()=0;

      

11、抽象类:只要含有纯虚函数,那这个类就是抽象类。

        为什么取名为抽象类?抽象之意就是没有实际实现作用的,因为抽象类是不能定义自己的对象,它存在的唯一原因是为定义派生它的其它类。

                           所以是不能创建抽象类的对象,不能用把它作为函数的参数类型或返回类型。因为它并没有“完整描述”对象。

                          注意:抽象类的指针或引用或以作为参数或返回类型的

        为什么抽象类还有构造函数呢?既然不能创建对象为啥还有构造,抽象类构造的目的是用于初始化它的数据成员,并不是创建对象。因此,

                          它的构造函数一般是不能被使用的,所以一般被定义为protected保护成员,它一般是在派生类的构造函数中的初始化列表中调用。

                          注意:抽象类的构造函数不能调用纯虚函数,因为它调用的结果 是不定的(随机)

        如果派生类中仍然有纯虚函数它本身也是抽象类,两种情况,一是继承于基类来的抽象类没有实现代码;二是派生类自己本身新增了纯虚函数。

       也就是说纯虚函数具有继承性。


        抽象类除了为派生类打好基础的作用外,还有一个作用就是定义标准类接口:

                派生于这种抽象基类的类必须为每个虚函数定义实现代码,这个虚函数的实现代码由实现派生类的人来决定。所以抽象类固定了接口,但

        实现代码(通过派生类)是灵活的。


12、通过指针来释放对象

       由于虚函数的原因,处理派生类对象常使用基类的指针,而析构时因为没有具体的对象名(只有基类的指针)对于动态分配的各自派生类的对象

       析构时就会出问题,因为用指针析构只能调用基类的对象。怎么办呢?


13、虚析构函数

         接12,这时只要把基类中的析构函数定义为虚函数,这样释放指针delete pa;时就会自动跟踪每个派生类,这就是虚类析构函数。

        声明:在基类析构函数前加virtual,这样就告诉编译器,通过指针或引用参数调用的析构函数是动态绑定的,应在运行期间自动跟踪析构。

       注意:一根据虚函数定义,只须在基类定义析构函数前加virtual,后面派生的析构自动就是虚函数了。

                   二虚析构函数是特例,因为前面的虚函数要求函数名与参数及返回值相同,析构函数则不同。

       只要类中有虚函数,就应尽量把析构函数设置为虚函数。


14、类型比较:如何判断一个对象、指针、引用的类型呢?

        typeid()    测定运行期间的类型。 

        包含在头文件<typeinfo>中

        返回值:type_info   严格的说是const   std::type_info

if(typeid(*pVessel)==typeid(Carton))cout<<"yes"<<endl;else    cout<<"no"<<endl;//下面是引用比较void show(Vessel& rVessel)....if(typeid(rVessel)==typeid(Carton))cout<<"Yes"<<endl;elsecout<<"No"<<endl; 

       注意:在使用typeid确定动态类型,时通过需要解除基类指针的引用,以测试该指针所指向的对象类型。

                   对于引用参数,则使用参数名即可进行测定。


       一般typeid()会使程序僵化,因为不断地判断类型,分支运行相关代码,显得繁芜。

       所以一般用虚函数来简单,因为它更灵活多变,代码简化。

           

15、类成员指针:它可以指向数据成员或成员函数。

       为什么要有类成员的指针?因为类成员的指针可以指向任意几个兼容成员(多个对象),也可以为类的对象访问它指向的成员。

                                                   当然类成员的指针只有和具体的对象结合才能指向内存中某个具体的位置。

       指针可以存储类对象的数据成员的地址(假定可访问)。但不能用函数指针存储成员函数的址址,即使函数签名与成员函数签名一样也不行。

       这是因为:成员函数把类类型作为其类型的一部分,所以它不同于一般的函数类型。


        一、数据成员指针

               类数据成员的指针,它包含类数据成员的地址,且仅在与(具体的)类对象组合时,才能指向内存的某个位置。

#include <iostream>using namespace std;class A{public:A(int m):a(m){}int a;};//typedef int A::* A_datap;int main(int argc, char *argv[]){int A::*p=&A::a;//A_datap p=&A::a;A c=A(3);cout<<c.*p<<endl;return 0;}

         因为int A::*这种类型很长,常用typedef来代替。注意取的地址也有限定A:: ,这里不能用&A.a(这个是int型显示不符)。


       二、成员函数指针

               因为“类的成员函数指针”的类型涉及到类类型、函数的参数列表和返回类型,意味着这种指针是专用于类的,不能用于其它类的函数成员地址。

        同时还遵循函数指针的规则。

class A{public:A(int m,int n):a(m),b(n){}int a;int b;void show(){cout<<"OK"<<endl;}};int main(){int A::*p=&A::a;int A::*p1=&A::b;void (A::*p2)()=&A::show;A c=A(3,4);cout<<c.*p<<endl;cout<<c.*p1<<endl;(c.*p2)();return 0;}

          定义p2是类的成员函数指针类型,并赋值。

          注意括号的写法,c.(*p2)()、c.*p2()   两种写法都是错误的

           c.*p2();  括号优先,按语法,*p2()返回一个指针,指针与对象结合,无法解释。

           c.(*p2)();同样返回一个函数指针再与对象结合 。。。。无法解释

           (c.*p)();  对象c中的函数名与对象类型结合,返回类中的函数名,结合()即调用函数。

         声明类的成员函数语句:

               return_type    (Class_type::*Pointer_name)(Parameter_type_list)

         下面是有参数列表情况:

class A{public:A(int m,int n):a(m),b(n){}int a;int b;void show(int i){cout<<i<<" OK"<<endl;}};int main(){int A::*p=&A::a;int A::*p1=&A::b;void (A::*p2)(int)=&A::show;A c=A(3,4);cout<<c.*p<<endl;cout<<c.*p1<<endl;(c.*p2)(4);return 0;}

           当然成员函数指针也可以作为另一个函数的参数进行传送:

class A{public:double fun(double (A::*pfun1)()const,double (A::*pfun2)()const{return (this->*pfun1)()*(this->*pfun2)();}double fun1()const{...}double fun2()const{...}...};...//调用:cout<<a.fun(&A::fun1(),&A::fun2())<<endl; 

        可以声明类成员的指针,它可以是数据成员的指针,也可以是函数成员的指针。但是:它只能与对象、引用或指针结合进行使用,来引用成员指针定义

        的对象的类成员。





原创粉丝点击