C++之多态

来源:互联网 发布:网络电视不更新怎么办 编辑:程序博客网 时间:2024/04/27 18:30

一、多态定义

        多态是每种面向对象语言的重要概念。它理解起立就是父类指针指向了子类的实例,然后通过父类指针调用实际成员函数的过程.我们知道虚函数是实现多态的重要机制。假如一个类中有虚函数,那么在类实例的首部会保存该类虚函数表的指针。注意这里针对g++编译时,虚表虚函数表还不太一样。从前面的2篇博文大家知道,在存在虚继承的情况下,虚表开始处会记录基类数据成员的偏移。虚表是类所有,虚指针是实例所有,同一类所有实例公用该类虚表。

二、源代码调试分析过程

1)子父类都不包含虚函数

示例1:中我定义了2个类,father和son类,它们都不包含虚函数,我分别定义了父类的对象b,引用对象b2,指针b3,然后分别让他们指向子类对象er。如代码中的main函数部分。我们很清楚能看到打印结果居然都是父类的Test方法。也许你知道本来就是这样的啊,下面我们反汇编看看 ,编译器到底干了什么。

#include <stdio.h> #include <iostream>using namespace std;class father {public:int a;father():a(88){}void Test(){cout << "father:test" <<endl;}};class son:public father {public:int b;son():b(22){}void Test(){cout << "son:test" <<endl;}};int main(){son er;er.a = 44;er.b = 55;father b1 = er;b1.Test();father &b2 = er;b2.Test();father *b3 = &er;b3->Test();return 1;}
运行结果:

father:test
father:test
father:test

分析:这里我就不像之前博文那样写出调试步骤了,只列出一些关键的数据。下面是这个main含函数经过反汇编得到一段代码。这里在代码中我们直接使用了类定义了一个子类对象er,而没有使用new关键子,所以er对象也是放在在栈空间而不是在堆空间(对堆栈不理解可看这里)。这里所以er安放到了rbp-0x20处,大小是16字节。下面可以看到我们每次通过我们定义的b1,b2,b3,去访问Test方法,它都会取子类对象的地址,然后就直接就调用了子类的Test()函数了。所有就有了,在没有虚函数的情况下,指针类型是什么类型,它就调用对应类的成员方法。

   father b1 = er;       400884:   8b 45 e0                mov    -0x20(%rbp),%eax  400887:   89 45 f0                mov    %eax,-0x10(%rbp)    b1.Test();  40088a:   48 8d 45 f0             lea    -0x10(%rbp),%rax "取出保存到0x10偏移处子类对象地址  40088e:   48 89 c7                mov    %rax,%rdi  400891:   e8 98 00 00 00          callq  40092e <_ZN6father4TestEv> "这之前编译器,也没做具体运算,就这样直接就调了基类test方法。    father &b2 = er;      400896:   48 8d 45 e0             lea    -0x20(%rbp),%rax  40089a:   48 89 45 d8             mov    %rax,-0x28(%rbp)    b2.Test();  40089e:   48 8b 45 d8             mov    -0x28(%rbp),%rax "取出保存到0x28偏移处子类对象地址  4008a2:   48 89 c7                mov    %rax,%rdi  4008a5:   e8 84 00 00 00          callq  40092e <_ZN6father4TestEv> “如上同理    father *b3 = &er;     4008aa:   48 8d 45 e0             lea    -0x20(%rbp),%rax  4008ae:   48 89 45 d0             mov    %rax,-0x30(%rbp)

2)父类有虚函数子类没有虚函数

    这种情况我们在之前说过,一旦有虚函数的话就会给该类生成一个虚函数表。该表的虚函数表指针存放到每个该类实例对象的内存首地址处,针对g++编译情况,实例首地址存放不是虚表首地址,而是虚函数表首地址(也就是虚表首地址+偏移)。下面的例子同示例1的区别有两个地方。

 1.将father类的test方法改成添加virtual修饰符

 2.main函数中采用new方式为son对象er分配空间(可以和之前示例1对比一下,这里是er分配到堆中)

#include <stdio.h>#include <iostream>using namespace std;class father{public:    int a;    father():a(88){    }    virtual void Test()    {        cout << "father:test" <<endl;    }};class son:public father{                                                                                                                                                                                       public:    int b;    son():b(22){}    void Test()    {        cout << "son:test" <<endl;    }};int main(){    son *er=new son();    er->a = 44;    er->b = 55;    father b1 = *er;    b1.Test();    father &b2 = *er;    b2.Test();    father *b3 = er;    b3->Test();    return 1;}
运行结果:

father:test
son:test
son:test
总结:经过上面两个2修改,打印结果发生了很大变化。这里由于父类中存在虚函数,那么子类和父类都会对应一张虚表,第一个输出的结果是父类的test函数,原因是这里重新给父类对象b1分配了栈空间,不是简单的指针赋值了,而是将子类中对应父类那部分数据域拷贝到父类当中,而虚函数表使用的确是父类的自己的,除此之外在用father b1定义父类对象时,也没调用父类构造函数(如果采用new的方式分配的话会调用),其实在new子类对象的时候已经调用过了,下面的调试环节我们可以看到父类的数据域a的值,已经赋成子类的数据了吧。下面我有分析父类的拷贝函数,可以很清晰的看到复制过程和虚函数表赋值。下面是主要的main函数反汇编代码。

    son *er=new son();  40096b:   bf 10 00 00 00          mov    $0x10,%edi                                                                                                                                      400970:   e8 1b ff ff ff          callq  400890 <_Znwm@plt>  400975:   48 89 c3                mov    %rax,%rbx  400978:   48 89 d8                mov    %rbx,%rax  40097b:   48 89 c7                mov    %rax,%rdi  40097e:   e8 1b 01 00 00          callq  400a9e <_ZN3sonC1Ev>  400983:   48 89 5d e8             mov    %rbx,-0x18(%rbp)  ”将er对象保存到rbp-0x18处    er->a = 44;  400987:   48 8b 45 e8             mov    -0x18(%rbp),%rax  40098b:   c7 40 08 2c 00 00 00    movl   $0x2c,0x8(%rax)   “注意这里在示例1处时是直接放到栈中,这里是放到堆里面。    er->b = 55;  400992:   48 8b 45 e8             mov    -0x18(%rbp),%rax  400996:   c7 40 0c 37 00 00 00    movl   $0x37,0xc(%rax)    father b1 = *er;  40099d:   48 8b 55 e8             mov    -0x18(%rbp),%rdx “取到er对象地址  4009a1:   48 8d 45 c0             lea    -0x40(%rbp),%rax "这里取到rbp-0x40处的地址,它想干嘛,奇怪下面也没调用father构造函数,怪  4009a5:   48 89 d6                mov    %rdx,%rsi        ”将er对象首地址放到源寄存器中  4009a8:   48 89 c7                mov    %rax,%rdi        ”将b1对象首地址放到目的寄存器中  4009ab:   e8 48 01 00 00          callq  400af8 <_ZN6fatherC1ERKS_> “这里不是构造函数,而是一个拷贝函数。我们下面进去看看。    b1.Test();  4009b0:   48 8d 45 c0             lea    -0x40(%rbp),%rax ”下面都是简单的将er指针赋值给对应指针的操作,虚指针还是子类的。  4009b4:   48 89 c7                mov    %rax,%rdi  4009b7:   e8 b8 00 00 00          callq  400a74 <_ZN6father4TestEv>    father &b2 = *er;  4009bc:   48 8b 45 e8             mov    -0x18(%rbp),%rax  4009c0:   48 89 45 e0             mov    %rax,-0x20(%rbp) “b2入栈    b2.Test();  4009c4:   48 8b 45 e0             mov    -0x20(%rbp),%rax  4009c8:   48 8b 00                mov    (%rax),%rax  4009cb:   48 8b 10                mov    (%rax),%rdx  4009ce:   48 8b 45 e0             mov    -0x20(%rbp),%rax  4009d2:   48 89 c7                mov    %rax,%rdi  4009d5:   ff d2                   callq  *%rdx    father *b3 = er;  4009d7:   48 8b 45 e8             mov    -0x18(%rbp),%rax  4009db:   48 89 45 d8             mov    %rax,-0x28(%rbp) ”<span style="font-family: Arial, Helvetica, sans-serif;">b3入栈</span>

father类的拷贝函数:

这个拷贝函数,先将父类的虚指针设置为自己的,然后从子类中拷贝了属于父类的那部分数据,下面反汇编代码中有详细介绍,这里就不打字了。

0000000000400af8 <_ZN6fatherC1ERKS_>:#include <stdio.h>#include <iostream>using namespace std;class father{  400af8:   55                      push   %rbp  400af9:   48 89 e5                mov    %rsp,%rbp  400afc:   48 89 7d f8             mov    %rdi,-0x8(%rbp)  ”从上面可以看到这里将b1对象地址放到当前栈的0x08处  400b00:   48 89 75 f0             mov    %rsi,-0x10(%rbp) “将er对象的地址放到0x10偏移处  400b04:   48 8b 45 f8             mov    -0x8(%rbp),%rax  ”取出b1对象地址  400b08:   48 c7 00 40 0c 40 00    movq   $0x400c40,(%rax) “将0x400c40(父类虚表)放到b1对象所在的首地址上。  400b0f:   48 8b 45 f0             mov    -0x10(%rbp),%rax ”取出er对象地址  400b13:   8b 50 08                mov    0x8(%rax),%edx   “取出er对象首地址偏移8的内容,即er->a的数据  400b16:   48 8b 45 f8             mov    -0x8(%rbp),%rax  "再次取出b1地址放到rax通用寄存器中  400b1a:   89 50 08                mov    %edx,0x8(%rax)   “这里将er->a的数据拷贝到b1对象所在内存偏移0x08地上。  400b1d:   c9                      leaveq   400b1e:   c3                      retq     400b1f:   90                      nop

下图就是简单的拷贝过程,只要记住这里vptr使用的是父类自己的就行了。



下面是一些调试结果,可以清晰的看到er对象中vptr.father 保存的是子类的虚指针0x400c20.在打印b1时 vptr.father=0x400c40,而a=44 不是初始化时的88,所以这里可以看出子类将a赋值过去了。其它的指针类型除了b1 vptr使用父类,其它都是子类,所以他们会调用子类的方法。

(gdb) p *er

$1 = {<father> = {_vptr.father = 0x400c20 <vtable for son+16>, a = 44}, b = 55}

(gdb) p b1
$2 = {_vptr.father = 0x400c40 <vtable for father+16>, a = 44}  注意父类vptr值

(gdb) p b2
$3 = (father &) @0x603010: {_vptr.father = 0x400c20 <vtable for son+16>, a = 44}

(gdb) p *b3
$5 = {_vptr.father = 0x400c20 <vtable for son+16>, a = 44}

下面是虚表展示区,由于基类中含有虚函数,所以子类中被迫有了虚函数,而且都有一张虚表。这里在强调一下,放在对象首地址的是虚函数表指针(它是虚表的一个偏移)而不是虚表指针,虚表指针的开始处,被g++放了其它数据。在菱形继承,虚继承的情况下,开始处会存放公用基类数据的偏移。

Vtable for father
father::_ZTV6father: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI6father)
16    father::Test

Class father
   size=16 align=8
   base size=12 base align=8
father (0x7f2e30aad8c0) 0
    vptr=((& father::_ZTV6father) + 16u)

Vtable for son
son::_ZTV3son: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI3son)
16    son::Test

Class son
   size=16 align=8
   base size=16 base align=8
son (0x7f2e30af60e0) 0
    vptr=((& son::_ZTV3son) + 16u)
  father (0x7f2e30af6150) 0
      primary-for son (0x7f2e30af60e0)

3)父类无虚函数子类有虚函数

        这种情况和示例2的区别无非就是去掉了father类 test方法前面的修饰符virtual,所以子类会有一张虚表,而父类就没有了。那么这样情况下,又会表现成什么情况呢?会是和上面的过程一样吗?带着疑问我们开始吧,代码如下。

#include <stdio.h>                                                                                                                                                                      #include <iostream>using namespace std;class father{public:    int a;    father():a(88){    }    void Test()    {        cout << "father:test" <<endl;    }};class son:public father{public:    int b;    son():b(22){}    virtual void Test()    {        cout << "son:test" <<endl;    }};int main(){    son *er=new son();    er->a = 44;    er->b = 55;    father b1 = *er;    b1.Test();    father &b2 = *er;    b2.Test();    father *b3 = er;    b3->Test();    return 1;}
运行结果:

father:test
father:test
father:test

总结:打印结果都是父类的test方法,过程和示例2肯定不一样,我们来看看汇编代码压压惊。

   father b1 = *er;  40097f:    48 8b 45 d8              mov    -0x28(%rbp),%rax “取出子类对象er的内存地址  400983:    8b 40 08                 mov    0x8(%rax),%eax   ”取出偏移8的数据,我去,这里取的就是er->a的数据啊  400986:    89 45 e0                 mov    %eax,-0x20(%rbp) “将a的数据放到栈中,也就是放到b1所在地,这里father没有虚表,不用偏移^_^    b1.Test();  400989:    48 8d 45 e0              lea    -0x20(%rbp),%rax  40098d:    48 89 c7                 mov    %rax,%rdi  400990:    e8 c1 00 00 00           callq  400a56 <_ZN6father4TestEv>    father &b2 = *er;  400995:    48 83 7d d8 00           cmpq   $0x0,-0x28(%rbp)   "比较er对象是否是空,这么操蛋  40099a:    74 0a                    je     4009a6 <main+0x62> “如果是0就跳到4009a6处执行,也就是左下方第4行  40099c:    48 8b 45 d8              mov    -0x28(%rbp),%rax   ”取出er对象内存地址  4009a0:    48 83 c0 08              add    $0x8,%rax          "rax指向er->a,这想干嘛  4009a4:    eb 05                    jmp    4009ab <main+0x67> ”跳到下面执行  4009a6:    b8 00 00 00 00           mov    $0x0,%eax  4009ab:    48 89 45 d0              mov    %rax,-0x30(%rbp)   “这里直接将er->a,赋值给了b2    b2.Test();  4009af:    48 8b 45 d0              mov    -0x30(%rbp),%rax  4009b3:    48 89 c7                 mov    %rax,%rdi  4009b6:    e8 9b 00 00 00           callq  400a56 <_ZN6father4TestEv>    father *b3 = er;  4009bb:    48 83 7d d8 00           cmpq   $0x0,-0x28(%rbp)   ”这个过程和上面的一样,就这样吧  4009c0:    74 0a                    je     4009cc <main+0x88>  4009c2:    48 8b 45 d8              mov    -0x28(%rbp),%rax  4009c6:    48 83 c0 08              add    $0x8,%rax  4009ca:    eb 05                    jmp    4009d1 <main+0x8d>  4009cc:    b8 00 00 00 00           mov    $0x0,%eax  4009d1:    48 89 45 c8              mov    %rax,-0x38(%rbp)    b3->Test();  4009d5:    48 8b 45 c8              mov    -0x38(%rbp),%rax  4009d9:    48 89 c7                 mov    %rax,%rdi  4009dc:    e8 75 00 00 00           callq  400a56 <_ZN6father4TestEv>
        上面可以看到 执行 father b1 = *er这行代码时,直接将er->a的值赋给了父类a,这里由于父类没有虚函数,也没有加偏移8了。同样下面的b2,b3都是简单的赋值操作。这里我们可以看出了眉头了,父类没有虚函数的情况下,指针类型是什么类型,就调用对应类型的方法。这里都是将子类赋给了父类,所以调用的就是父类的方法了。
     下面在上一道小菜,可以看到父类没有虚表吧,子类有虚函数(仿佛我这里说了废话)。虚表开始的不是虚函数表

Class father   size=4 align=4   base size=4 base align=4father (0x7f63443658c0) 0Vtable for sonson::_ZTV3son: 3u entries0     (int (*)(...))08     (int (*)(...))(& _ZTI3son)16    son::TestClass son   size=16 align=8   base size=16 base align=8son (0x7f63443655b0) 0    vptr=((& son::_ZTV3son) + 16u)  father (0x7f63443ae000) 8

下面是调试时的一些记录:分别打印的是b1,b2,b3的内存,可以发现子类实例中存在虚指针,而父类不存在vptr。由于是简单的拷贝操作,所以b1,b2,b3的数据域都是44,而不是初始化时的88.
(gdb) p *er
$1 = {<father> = {a = 44}, _vptr.son = 0x400c10, b = 55}
(gdb) p b1
$2 = {a = 44}
(gdb) p b2
$3 = (father &) @0x603018: {a = 44}
(gdb) p *b3

$4 = {a = 44}

三、大结局

下是的一些结论都是根据前面的几个简单例子验证得到的,针对于复杂的菱形继承,多继承的多态情况,我们后面在分析吧。

1)父子类都没有虚函数时:指针类型是什么类型,就调用对应类型的方法.

2)父类有虚函数子类没有:这时候由于父类有虚函数,所以子类也被动有了虚函数。这时候如果父类对象分配了实际的内存空间,而且父类也不是采用new的方式分配的。那么如果子类直接赋值给父类对象的话,使用基类类型访问的依然是父类方法。除非是子类指针赋值给父类指针的话,那就访问的依然是子类的方法。

小例:

-1.father b;  //直接定义分配了内存

     b  = *er;    //直接是赋值操作。

     b.test();    //这种情况就是访问父类

-2. father *b=new father();

      b = er;

      b->test() ; //这里依然访问的是子类方法,之前的基类对象就找不到了

3)子类有虚函数父类无:这种情况下,指针是什么类型就调用什么类型的方法。



0 0
原创粉丝点击