C++——多态总结
来源:互联网 发布:欧芹和芹菜的区别 知乎 编辑:程序博客网 时间:2024/05/18 00:15
在博客多态&虚函数中主要对多态的一些基本概念和虚函数做了介绍,下面,我们来探究一下【虚表】。
含有虚函数的类
- 先来看看含有虚函数的类的大小吧!
class B{public: virtual void Show() { cout << _b << endl; }public: int _b;};
一眼看过去,这个类中只有一个int类型的变量_b,那么他的大小是不是只有4个字节呢?我运行了之后发现并不是,它的大小是8个字节。运行结果我这就不看了,下面我们用内存和监视来分析一下对象b的对象模型。
可以看到,当执行完
b._b = 1;
时,对象b的成员_b与对象b的地址偏移了4个字节。而这4个字节里存放的是一个地址,我们把这个地址叫做虚表指针,它指向一个存放虚函数地址的内存块。这个内存块就是虚表。由此,我们可得到b的对象模型
有虚函数的类相比普通类多了4个字节,用来存放虚表指针。在对象模型中,类中的成员变量存放在虚表指针之后。
当类中有多个对象时,这些对象共用同一虚表。(可同时创建两个对象,在内存里查看其虚表指针是否相同来验证)
当类中有多个成员变量时,对象模型中各变量的存放顺序按其在类中的声明顺序存放。
当类中有多个虚函数时,虚表中各虚函数存放顺序按照其在类中的声明顺序存放。
直接来看例子吧:
class B{public: virtual void Fun3() {} virtual void Fun1() {} virtual void Fun2() {}public: int _b3; int _b1; int _b2;};
下图是我根据监视内存画出来的b的存储结构
再来看看它的对象模型是怎样的?
- 需要注意一点
- 类的构造函数在这里起填充虚表指针的作用
下面我们来看一下继承(参见博客【继承】)体系中虚函数的结构如何?
单继承中的虚函数
- 虚函数无覆盖
class B{public: B() :_b(1) {} virtual void Fun1() { cout << "B::Fun1()"; } virtual void Fun2() { cout << "B::Fun2()"; } virtual void Fun3() { cout << "B::Fun3()"; }public: int _b;};class D :public B{public: D() :_d(2) {} virtual void Fun4() { cout << "D::Fun4()"; } virtual void Fun5() { cout << "D::Fun5()"; } virtual void Fun6() { cout << "D::Fun6()"; }public: int _d;};typedef void(*FUN_TEST)();void FunTest(){ B b; cout<<sizeof(b)<<endl; cout << "B vfptr:" << endl; for (int iIdx = 0; iIdx < 3; ++iIdx) { FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&b + iIdx)); funTest(); cout << ": " << (int *)funTest << endl; } cout << endl; D d; cout << sizeof(d) << endl; cout << "D vfptr:" << endl; for (int iIdx = 0; iIdx < 6; ++iIdx) { FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&d + iIdx)); funTest(); cout << ": " << (int *)funTest << endl; }}
其中FunTest()函数用于打印各对象中的函数。
先来看看结果吧
d中有两个成员变量以及一个虚表指针,所以大小为12.
可以发现,在派生类中,也完全继承了基类的虚函数,且遵循继承的存储结构。
派生类中继承的基类的虚函数地址与基类中的相同。
- 虚函数有覆盖
同样拿上面例子来说,我把派生类中的函数名Fun5改为Fun3,把Fun6改为Fun2,并且派生类中的循环次数改为4,其余保持不变。再次运行代码得到如下结果:
基类无变化,但是在派生类中,只打印了派生类的函数Fun2和Fun3.但是我的定义顺序明明是Fun3在Fun2前面的呀。怎么打印结果却是反的?这是怎么一回事呢?
当对象d被创建的时候,其虚表指针已经形成,但此时,虚表中存放的是从基类继承来的虚函数,当系统检测到派生类中已经对基类的虚函数进行重写的函数时,就拿该函数去替换虚表中基类对应的虚函数。所以虽然,Fun3定义在Fun2之前,但是替换时,系统从基类的Fun1开始,依次向下检测,当检测到Fun2被重写时,直接拿派生类中的Fun2去替换当前位置上的Fun2。如下图:
由此,可得出单继承的对象模型:
虚表的形成:
菱形继承
前面在继承中,为了解决二义性问题,我们引入了虚拟继承。在虚拟继承中,派生类中前4字节是偏移量的地址。但引入虚函数之后,我们看到虚表指针也存放于派生类的前4字节。那么,我们来看看,在菱形继承中,派生类的对象模型如何?
- 菱形继承
class B{public: virtual void FunTest1() { cout << "B::FunTest1()" << endl; } int _b;};class C1 :public B{public: void FunTest1() { cout << "C1::FunTest1()" << endl; } virtual void FunTest2() { cout << "C1::FunTest2()" << endl; } int _c1;};class C2 :public B{public: virtual void FunTest1() { cout << "C2::FunTest1()" << endl; } virtual void FunTest3() { cout << "C2::FunTest3()" << endl; } int _c2;};class D :public C1, public C2{public: virtual void FunTest1() { cout << "D::FunTest1()" << endl; } virtual void FunTest2() { cout << "D::FunTest2()" << endl; } virtual void FunTest3() { cout << "D::FunTest3()" << endl; } virtual void FunTest4() { cout << "D::FunTest4()" << endl; } int _d;};typedef void(*Fun)();void Printvpf(){ D d; cout << sizeof(d) << endl; d.C1::_b = 1; d.C2::_b = 2; d._c1 = 3; d._c2 = 4; d._d = 5; C1& c1 = d; int* vpfAddr = (int*)*(int*)&c1; Fun* pfun = (Fun*)vpfAddr; while (*pfun) { (*pfun)(); pfun = (Fun*)++vpfAddr; } cout << endl; C2& c2 = d; vpfAddr = (int*)*(int*)&c2; pfun = (Fun*)vpfAddr; while (*pfun) { (*pfun)(); pfun = (Fun*)++vpfAddr; }}
来看看结果吧!
结果可见,系统将派生类自己的虚函数放在了其第一个基类C1后面,并且派生类中重写的虚函数覆盖了基类的虚函数。
其对象模型如下:
- 菱形虚拟继承
class B{public: virtual void FunTest1() { cout << "B::FunTest1()" << endl; } int _b;};class C1 :virtual public B{public: void FunTest1() { cout << "C1::FunTest1()" << endl; } virtual void FunTest2() { cout << "C1::FunTest2()" << endl; } int _c1;};class C2 :virtual public B{public: virtual void FunTest1() { cout << "C2::FunTest1()" << endl; } virtual void FunTest3() { cout << "C2::FunTest3()" << endl; } int _c2;};class D :public C1, public C2{public: virtual void FunTest1() { cout << "D::FunTest1()" << endl; } virtual void FunTest2() { cout << "D::FunTest2()" << endl; } virtual void FunTest3() { cout << "D::FunTest3()" << endl; } virtual void FunTest4() { cout << "D::FunTest4()" << endl; } int _d;};typedef void(*Fun)();void Printvpf(){ D d; cout << sizeof(d) << endl; d._b = 1; d._c1 = 2; d._c2 = 3; d._d = 4; C1& c1 = d; int* vpfAddr = (int*)*(int*)&c1; Fun* pfun = (Fun*)vpfAddr; while (*pfun) { (*pfun)(); pfun = (Fun*)++vpfAddr; } cout << endl; C2& c2 = d; vpfAddr = (int*)*(int*)&c2; pfun = (Fun*)vpfAddr; while (*pfun) { (*pfun)(); pfun = (Fun*)++vpfAddr; } B& b = d; vpfAddr = (int*)*(int*)&b; pfun = (Fun*)vpfAddr; while (*pfun) { (*pfun)(); pfun = (Fun*)++vpfAddr; }}int main(){ Printvpf(); return 0;}
结果如图:
表示偏移量的地址紧随虚表指针之后,其次才是成员变量。各类成员存放遵循继承规则。
上图中表示偏移量的第一个数可能有人无法理解为什么几乎是一串f,其实它是负数在内存中的存储形式。
由此可得对象模型为:
- C++——多态总结
- 【c++】多态总结
- 【C++】多态总结
- C基础——C语言总结
- C——指针总结
- C#——字符串总结
- C++——Const总结
- c——类型总结
- C++——【继承】总结
- C——数组总结
- C——指针总结
- Objective—C NSNumber 总结
- C———位运算总结
- C++primer——const总结
- C语言基础——大总结
- 黑马程序员——C语言总结
- 【备战C++】——宏观总结
- Objective-C基础——语法总结
- php进阶
- 报错JedisConnectionException: Could not get a resource from the pool
- 蓝牙BLE 广播数据
- Async/Await,最佳做法..netframework4.5
- Hibernate中使用Criteria查询及注解——(DeptTest.java)
- C++——多态总结
- mybatis——【持久化框架】Mybatis简介与原理
- PTA 朋友圈
- UEditor在JavaWeb中的应用
- 产业互联网公司,做产品决策的三个步骤
- leetcode_53. Maximum Subarray-子数组最大和
- NYOJ 325 zb的生日
- hdu 3829 Cat VS Dog(最大独立集)
- 16. 3Sum Closest