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)子类有虚函数父类无:这种情况下,指针是什么类型就调用什么类型的方法。
- OOP之多态 【C#】
- objective-c之多态
- objective-c之多态
- 【C#】面向对象之多态
- C#——面向对象之多态
- Linux C之多线程
- Objective-c 特性之多态、动态类型和动态绑定
- Objective-c 特性之多态、动态类型和动态绑定
- Objective-c 特性之多态、动态类型和动态绑定
- C+学习之多态篇(虚函数)
- linux c sockset之多播
- linux c sockset之多播
- java之多态,
- 面向对象之多态
- 面向对象之多态
- c++规范之多态
- C++学习之多态
- 面向对象之多态
- 坚持每天学习嵌入式、每天总结、发表学到的东西!
- CDP协议
- LeetCode - 12. Integer to Roman
- 原型对象 原型相关问题
- [2016/7/14]一天不写东西就难受
- C++之多态
- instance of,isInstance,isAssignableFrom
- 字符串组合问题
- UVA 573 The Snail
- kali搜集工具之CDPSnarf
- 7月13号面试小结
- Poj - 1845 - Sumdiv
- Android线程池使用详解
- ul li 实例