C++多态机制浅析
2016-04-02 14:10
323 查看
最近突然想弄懂C++的多态机制到底是怎么实现的,找了挺多资料,发现大多数博客有一点没讲好——多继承的时候,是怎么让每个父类的指针恰好指向它所在的虚函数表的。不过这一点[1]里面有分析了,所以综合起来,打算整理一下,分享给米娜桑(有错误的麻烦指出o,蟹蟹~)
运行结果是:
sizeof(WithoutVP)=1
sizeof(WithVP)=4
结果分析:首先,为什么空的对象的size居然不是0呢?据说没什么特别的原因,就是编译器需要有一个数据来确定这个东西而已(有兴趣可以试试在一个程序里声明129个不同的空的类会怎么样,2333)。重点是为什么声明了虚函数的类WithVP的对象的大小是4而不是一样为1???
提示一下,我用的是32位的编译器,4 Bytes刚好是一个指针的大小。
不卖关子了,直接看下图:
图里可以很清楚看出,WithVP这个类的对象多了一个_vfptr的指针,里面有3个元素,分别对应声明的3个虚函数!
结论:其实编译器会为每个具有虚函数(不管是自己类里声明的,还是从父类继承下来的)的类添加一个指针,这个指针指向一个属于该类的一个线性表(可能是数组也可能是链表等),称之为“虚函数表”。
运行结果是:
I am Base_A in Child1
I am Base_B
结合下面的对象内存(VS的图有点乱。。。):
Child1类继承了Base类,并重写了Base_A函数。那么Base类的虚函数表的内容是:{Base::Base_A(), Base::Base_B()},而Child1类的虚函数表的内容为:{Child1::Base_A(), Base::Base_B(), Child1::Derive_C()}(注意最后一个在vs中没有显示出来,但其实是有的,据说)。相信规律已经很显然了~即,子类的虚函数表=父类的虚函数表+子类新增的虚函数+如果有重载的虚函数,则替换成重载后的虚函数(可能不同编译器会有不同的实现,主要原则是这样)。
首先得看一下类的内存结构是怎样的,大概是——虚函数表的指针(如果有的话),父类的内存,自己非static数据成员等等,详情参考[3][4],我觉得它们都讲得很好。
所以,(单继承时)指针赋值的时候(即执行Base* base = &child;),注意指针赋值的真实含义,其实是这个指针指向你所指定的那块内存而已,所以现在Base类的这个指针base,它所代表的那个对象的虚函数表指针指向的是真正的子类的虚函数表,而不是Base类的虚函数表!!!这样就够了,因为在用Base类指针调用Base_A时,由于虚函数表指针是指向Child1类的虚函数表,所以自然执行的结果就是Child1::Base_A()的结果!!!多态的效果就是这样实现的。
咳咳,答案是肯定滴。多继承(并不是指多次继承,而是同时从多个父类继承)的例子如下:
运行结果是:
I am Base_A in Test
I am Base_A in Test
I am Base_B in Base1
I am Base_B i Base2
sizeof(Test)=16
sizeof(*b1)=8
sizeof(*b2)=8
问题来了,现在有多个父类了,那么在指针赋值(即Base1* b1 = &test;和Base2* b2 = &test;)的时候,会发生什么事呢?照例还是得先看一下最本质的类的内存分布(下面的图来自博文[1]):
(p是指Test类对象的指针,p1指向父类Base1被Test继承过来的部分,p=p1,而p2则是指父类Base2被Test继承过来的部分,p2=p1+sizeof(Base1_to_Test))这里的话,意思是说,多继承的类,它会有多个虚表指针,分别对应所继承的父类。然后在赋值给父类指针的时候,比如Base2* b2 = &test,编译器会给根据类的大小给一个偏移;同样Base1* b1 = &test也会有一个偏移量,只不过由于它是第一个,所以偏移量是0罢了~
这就是多继承和单继承的区别,关键在于理解一点就够了——将子类对象的地址复制给父类指针的时候,父类指针是指向哪里。
此外还有很有趣的一个东西,比如我们再写一句delete b2的时候,被释放的是哪一部分内存?只是p2过后的那部分吗?(肯定不是啊,这样的话,前面那部分内存就泄露了)那怎么做到能够删除整块内存呢?(在运行时并没有足够的信息来指示这块内存是真实的Base2的对象还是子类的对象),有兴趣可以看一下博文[1]的介绍——thunk技术。
[1] IBM技术博客:https://www.ibm.com/developerworks/cn/java/j-lo-polymorph/
[2] 陈皓的博客:http://blog.csdn.net/haoel/article/details/1948051
[3] C++类对应的内存结构,http://blog.csdn.net/guogangj/article/details/2036785
[4] C++类、结构对象内存布局浅析,http://blog.csdn.net/fanfank/article/details/12175585
A. 初探
先看一下声明了虚函数和没声明虚函数的类的大小的区别:#include <stdio.h> class WithoutVP { public: void func() {} }; class WithVP { public: void func() {} virtual void vfunc1() {} virtual void vfunc2() {} virtual void vfunc3() {} }; int main() { WithoutVP no; WithVP yes; printf("sizeof(WithoutVP)=%d\n", sizeof(no)); printf("sizeof(WithVP)=%d\n", sizeof(yes)); return 0; }
运行结果是:
sizeof(WithoutVP)=1
sizeof(WithVP)=4
结果分析:首先,为什么空的对象的size居然不是0呢?据说没什么特别的原因,就是编译器需要有一个数据来确定这个东西而已(有兴趣可以试试在一个程序里声明129个不同的空的类会怎么样,2333)。重点是为什么声明了虚函数的类WithVP的对象的大小是4而不是一样为1???
提示一下,我用的是32位的编译器,4 Bytes刚好是一个指针的大小。
不卖关子了,直接看下图:
图里可以很清楚看出,WithVP这个类的对象多了一个_vfptr的指针,里面有3个元素,分别对应声明的3个虚函数!
结论:其实编译器会为每个具有虚函数(不管是自己类里声明的,还是从父类继承下来的)的类添加一个指针,这个指针指向一个属于该类的一个线性表(可能是数组也可能是链表等),称之为“虚函数表”。
B. 单继承
正如很多相关博客所介绍的,C++的多态机制的实现,是用了virtual function加上virtual function table来实现的(正如A中所见到的)。为什么这样就能够实现呢?用一个单继承的简单例子来说明:#include <stdio.h> class Base { public: virtual void Base_A() { printf("I am Base_A\n"); } virtual void Base_B() { printf("I am Base_B\n"); } }; class Child1 : public Base { public: virtual void Base_A() { printf("I am Base_A in Child1\n"); } virtual void Derive_C() { printf("I am Derive_C in Child1\n"); } }; int main() { Child1 child; Base* base = &child; base->Base_A(); base->Base_B(); return 0; }
运行结果是:
I am Base_A in Child1
I am Base_B
结合下面的对象内存(VS的图有点乱。。。):
Child1类继承了Base类,并重写了Base_A函数。那么Base类的虚函数表的内容是:{Base::Base_A(), Base::Base_B()},而Child1类的虚函数表的内容为:{Child1::Base_A(), Base::Base_B(), Child1::Derive_C()}(注意最后一个在vs中没有显示出来,但其实是有的,据说)。相信规律已经很显然了~即,子类的虚函数表=父类的虚函数表+子类新增的虚函数+如果有重载的虚函数,则替换成重载后的虚函数(可能不同编译器会有不同的实现,主要原则是这样)。
回到“为什么这样就能够实现多态”的问题上来
关键在于——当我们将子类对象的指针赋值给父类的指针变量时,发生了什么事呢?首先得看一下类的内存结构是怎样的,大概是——虚函数表的指针(如果有的话),父类的内存,自己非static数据成员等等,详情参考[3][4],我觉得它们都讲得很好。
所以,(单继承时)指针赋值的时候(即执行Base* base = &child;),注意指针赋值的真实含义,其实是这个指针指向你所指定的那块内存而已,所以现在Base类的这个指针base,它所代表的那个对象的虚函数表指针指向的是真正的子类的虚函数表,而不是Base类的虚函数表!!!这样就够了,因为在用Base类指针调用Base_A时,由于虚函数表指针是指向Child1类的虚函数表,所以自然执行的结果就是Child1::Base_A()的结果!!!多态的效果就是这样实现的。
C. 多继承
哎,为什么要分单继承和多继承呢?有区别?咳咳,答案是肯定滴。多继承(并不是指多次继承,而是同时从多个父类继承)的例子如下:
#include <stdio.h> class Base1 { public: virtual void Base_A() { printf("I am Base_A in Base1\n"); } virtual void Base_B() { printf("I am Base_B in Base1\n"); } }; class Base2 { public: virtual void Base_A() { printf("I am Base_A in Base2\n"); } virtual void Base_B() { printf("I am Base_B i Base2\n"); } }; class Test : public Base1, public Base2 { public: virtual void Base_A() { printf("I am Base_A in Test\n"); } }; int main() { Test test; Base1* b1 = &test; Base2* b2 = &test; b1->Base_A(); b2->Base_A(); b1->Base_B(); b2->Base_B(); printf("sizeof(Test)=%d\n", sizeof(Test)); printf("sizeof(*b1)=%d\n", sizeof(*b1)); printf("sizeof(*b2)=%d\n", sizeof(*b2)); return 0; }
运行结果是:
I am Base_A in Test
I am Base_A in Test
I am Base_B in Base1
I am Base_B i Base2
sizeof(Test)=16
sizeof(*b1)=8
sizeof(*b2)=8
问题来了,现在有多个父类了,那么在指针赋值(即Base1* b1 = &test;和Base2* b2 = &test;)的时候,会发生什么事呢?照例还是得先看一下最本质的类的内存分布(下面的图来自博文[1]):
(p是指Test类对象的指针,p1指向父类Base1被Test继承过来的部分,p=p1,而p2则是指父类Base2被Test继承过来的部分,p2=p1+sizeof(Base1_to_Test))这里的话,意思是说,多继承的类,它会有多个虚表指针,分别对应所继承的父类。然后在赋值给父类指针的时候,比如Base2* b2 = &test,编译器会给根据类的大小给一个偏移;同样Base1* b1 = &test也会有一个偏移量,只不过由于它是第一个,所以偏移量是0罢了~
这就是多继承和单继承的区别,关键在于理解一点就够了——将子类对象的地址复制给父类指针的时候,父类指针是指向哪里。
此外还有很有趣的一个东西,比如我们再写一句delete b2的时候,被释放的是哪一部分内存?只是p2过后的那部分吗?(肯定不是啊,这样的话,前面那部分内存就泄露了)那怎么做到能够删除整块内存呢?(在运行时并没有足够的信息来指示这块内存是真实的Base2的对象还是子类的对象),有兴趣可以看一下博文[1]的介绍——thunk技术。
[1] IBM技术博客:https://www.ibm.com/developerworks/cn/java/j-lo-polymorph/
[2] 陈皓的博客:http://blog.csdn.net/haoel/article/details/1948051
[3] C++类对应的内存结构,http://blog.csdn.net/guogangj/article/details/2036785
[4] C++类、结构对象内存布局浅析,http://blog.csdn.net/fanfank/article/details/12175585
相关文章推荐
- sim900GPRS使用C语言打电话发短信
- C++指针理解《一》
- [leetcode ]221. Maximal Square c语言
- 多态在 Java 和 C++ 编程语言中的实现比较
- C++-优先级队列
- 【C语言】编写一个函数实现n^k,使用递归实现
- C语言(自加自减)
- C\C++ 获取当前路径
- c++中的构造函数和拷贝构造函数
- C语言中Union的用法
- c++学习(2.2)变量
- C语言函数和汇编函数相互调用
- 几个有关iOS的几个常见问题-----为什么选择OC语言
- 设计类CDate以满足:输出年月日日期格式;输入的日期加1;设置日期(参考清华版李春葆C++书籍)
- C++中的输出格式 八进制 十进制 十六进制
- c++断言设置
- C++中构造函数或析构函数定义为private
- 第1周 C语言及程序设计初步 例程-7 问题求解方法——迭代
- C语言18个经典问题答录
- C++ 判断double变量是否为0