C++之多态

一、多态定义

        多态是每种面向对象语言的重要概念。它理解起立就是父类指针指向了子类的实例,然后通过父类指针调用实际成员函数的过程.我们知道虚函数是实现多态的重要机制。假如一个类中有虚函数,那么在类实例的首部会保存该类虚函数表的指针。注意这里针对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=4
father (0x7f63443658c0) 0

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 (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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值