博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
探索虚函数(二)
阅读量:6225 次
发布时间:2019-06-21

本文共 7052 字,大约阅读时间需要 23 分钟。


感觉网上真的是牛人多多,没想到一个虚函数可以有这么多内容,菜鸟的我看着很久以前的大神们在实践中学习的脚印,观看他们深究其一也深究其二,实在让我这个偷懒的渣渣无地自容,无奈沿着大神么的脚步小心翼翼的学习一番.

前面总结了虚函数的一些基本知识,除了知道应用场景,怎么用,还要知道原理是什么,以及原理是怎么实现的.好的,总结出方法论四点,应用场景,使用方法,原理,原理实现.这篇文章打算带着自己,边实践中边思考虚函数的原理.

真的是简单而又明了. 虚继承知道这几种情况应该就差不多了吧. 对原理的解释和实践比较有名的.另外这位作者补充了两篇和用实际的例程展示了对象在内存中的布局情况. 教会了我如何用gdb去查看内存的布局,在实践中如果有不明白的还是要自己动手实践,不迷信博客,教程,书本, 要相信cpu内存与编译器.另外对于虚拟继承来说内存布局又有它的特殊性清晰的阐述了vbptr的原理.

本文打算利用gdb调试的方式学习一下中关于单继承,多继承,菱形继承关系中相应实例对象的内存分布情况.

特别注明: 文章中的代码全部来自于


如何获得虚表

如果存在继承关系,在响应的类中就会有一个_vptr的的虚表指针,位置在对象内存的开始部分,指向某一个虚表,这个虚表指针的大小和每个虚表项的大小和计算机系统息息相关,如果是32位系统,那么大小是4B,如果是64位系统那么大小是8B.测试主机 ubuntu 16.04 64bits系统.

在gdb调试的时候为了让打印的信息更加容易阅读,可以设置几个选项

set p obj on

set p array on
set p pretty on

假如在某个类A a;的开头有个虚表指针,我们如何得到呢. 可以借助指针的运算,因为本机64位环境,所以指针用long *,那么就是;

p  /a ((long*)*(long*)&a)[0]@5

本句话的意思是打印虚表位置开始往后的5项.也包括不是虚表的部分.

单继承

class Base{public:    virtual void fun1()    {        cout << "Base::func1()" << endl;    }    virtual void fun2()    {        cout << "Base::func2()" << endl;    }private:    int b;};class Derive :public Base{public:    virtual void fun1()           //重写基类虚函数,实现多态    {        cout << "Derive::func1()" << endl;    }    virtual void fun3()    {        cout << "Derive::func3()" << endl;    }    void fun4()    {        cout << "Derive::func4()" << endl;    }private:    int d;};int main(){   Base b;   Derive d;     return 0;}

b的内存结构

(gdb) p b$3 = (Base) {  _vptr.Base = 0x400c18 
, b = 0}

d的内存结构:

(gdb) p d$4 = (Derive) {   = {    _vptr.Base = 0x400bf0 
, b = 4196496 }, members of Derive: d = 0}

可以发现子类的实例直接继承了父类中的虚函数表的指针,那么虚函数表的内容有什么区别呢

我们分别打印一下两个变量的虚函数的

(gdb) p /a ((long *)(*(long *)&b))[0]@4$10 =   {0x400a24 
, 0x400a50
, 0x602200 <_ZTVN10__cxxabiv120__si_class_type_infoE@@CXXABI_1.3+16>, 0x400c40 <_ZTS6Derive>}(gdb) p /a ((long *)(*(long *)&d))[0]@4$11 = {0x400a7c
, 0x400a50
, 0x400aa8
, 0x0}

继承之后子类覆盖了父类中的虚函数.在虚表的表现上将父类相应的函数替换成子类的.而且吧子类特有的虚函数直接放道虚表的最后.

多继承

class Base1   //基类{public:    virtual void fun1()    {        cout << "Base1::fun1" << endl;    }    virtual void fun2()    {        cout << "Base1::fun2" << endl;    }private:    int b1;};class Base2  //基类{public:    virtual void fun1()    {        cout << "Base2::fun1" << endl;    }    virtual void fun2()    {        cout << "Base2::fun2" << endl;    }private:    int b2;};class Derive : public Base1, public Base2  //派生类{public:    virtual void fun1()    {        cout << "Derive::fun1" << endl;    }    virtual void fun3()    {        cout << "Derive::fun3" << endl;    }private:    int d1;};int main(){   Base1 b1;   Base2 b2;   Derive d;     return 0;}

b1的内存结构

(gdb) p b1$2 = (Base1) {  _vptr.Base1 = 0x400d08 
, b1 = 4196909}

b2的内存结构

(gdb) p b2$3 = (Base2) {  _vptr.Base2 = 0x400ce8 
, b2 = 4197389}

d的内存结构

(gdb) p d

$4 = (Derive) {  
= { _vptr.Base1 = 0x400ca0
, b1 = 0 },
= { _vptr.Base2 = 0x400cc8
, b2 = 4196496 }, members of Derive: d1 = 0}

可以发现子类从父类中各继承一张虚表,那么这两张在子类中的表与双父类表之间有什么区别呢.我们用命令分别查看b1,b2和d中的虚表

直接看一下d中两张虚表吧:

$8 =   {0x400ae0 
, 0x400a5c
, 0x400b12
, 0xfffffffffffffff0, 0x400d18 <_ZTI6Derive>}(gdb) p /a ((long *)*((long *)&d+sizeof(b1)/8))[0]@5$9 = {0x400b0b <_ZThn16_N6Derive4fun1Ev>, 0x400ab4
, 0x0, 0x400d58 <_ZTI5Base2>, 0x400a88
}

我们可以看到子类特有虚函数填充到第一个虚表当中,但是继承自两个父类的虚表中的函数都被替换成了子类中覆盖的方法.

菱形继承(非虚拟继承)

class Base          //Derive的间接基类{public:    virtual void func1()    {        cout << "Base::func1()" << endl;    }    virtual void func2()    {        cout << "Base::func2()" << endl;    }private:    int b;};class Base1 :public Base  //Derive的直接基类{public:    virtual void func1()          //重写Base的func1()    {        cout << "Base1::func1()" << endl;    }    virtual void func3()    {        cout << "Base1::func3()" << endl;    }private:    int b1;};class Base2 :public Base    //Derive的直接基类{public:    virtual void func1()       //重写Base的func1()    {        cout << "Base2::func2()" << endl;    }    virtual void func4()    {        cout << "Base2::func4()" << endl;    }private:    int b2;};class Derive :public Base1, public Base2{public:    virtual void func1()          //重写Base1的func1()    {        cout << "Derive::func1()" << endl;    }    virtual void func5()    {        cout << "Derive::func5()" << endl;    }private:    int d;};

我们可以打印一下这个菱形继承的内存结构·

$2 = (Derive) {  
= {
= { _vptr.Base = 0x400db0
, b = 0 }, members of Base1: b1 = 0 },
= {
= { _vptr.Base = 0x400de0
, b = 4196608 }, members of Base2: b2 = 0 }, members of Derive: d = -8912}

注意d到依然按照继承两张虚表的方式,但是这里注意以下d中有两个b,继承自Base,因此在指名的时候需要指定作用域,否则会有歧义.

那么虚表中的内容有什么区别呢?
我们打印以下虚表

(gdb) p /a ((long *)(*((long*)&d)))[0]@5$29 =   {0x400b90 
, 0x400ab4
, 0x400b0c
, 0x400bc2
, 0xfffffffffffffff0}(gdb) p /a ((long*)(*(long *)((int*)&d + sizeof(Base1)/4)))[0]@5 $30 = {0x400bbb <_ZThn16_N6Derive5func1Ev>, 0x400ab4
, 0x400b64
, 0x0, 0x400ea8 <_ZTI5Base2>}

可以看到与上一种情况类似,只不过这里面有两个func2,是两个不同的函数,调用的时候依据具体情况分析.

菱形继承(虚继承)

上面提到菱形继承可能导致的问题是变量歧义.解决的方法是采用虚拟继承的方式.所谓虚拟继承在使用的时候,虚基类中的数据在子类当中只有一个副本.是通过vbptr来实现的,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量详细叙述了相关的原理.

class Base{public:    virtual void fun1()    {        cout << "Base::fun1()" << endl;    }    virtual void fun2()    {        cout << "Base::fun2()" << endl;    }private:    int b;};class Base1 :virtual public Base  虚继承{public:    virtual void fun1()          //重写Base的func1()    {        cout << "Base1::fun1()" << endl;    }    virtual void fun3()    {        cout << "Base1::fun3()" << endl;    }private:    int b1;};class Base2 :virtual public Base  //虚继承{public:    virtual void fun1()       //重写Base的func1()    {        cout << "Base2::fun1()" << endl;    }    virtual void fun4()    {        cout << "Base2::fun4()" << endl;    }private:    int b2;};class Derive :public Base1, public Base2{public:    virtual void fun1()          //重写Base1的func1()    {        cout << "Derive::fun1()" << endl;    }    virtual void fun5()    {        cout << "Derive::fun5()" << endl;    }private:    int d;};

看一下d的内存布局

$1 = (Derive) {  
= {
= { _vptr.Base = 0x400e20
, b = 4196496 }, members of Base1: _vptr.Base1 = 0x400dc0
, b1 = 4197629 },
= { members of Base2: _vptr.Base2 = 0x400df0
, b2 = 0 }, members of Derive: d = 0}

可以体现出来这里面的Base的b变量只有一份拷贝,这里面其实隐藏了vbptr的虚基类表,如果有虚表的话,它就在虚表的下面.如果没有他就在类实例的开头.

关于vbptr的验证留到后面的文章,今天就到这儿吧.

转载于:https://www.cnblogs.com/xiaozhi007/p/7249532.html

你可能感兴趣的文章
《HTML CSS JavaScript 网页制作从入门到精通 第3版》—— 2.6 段落标记
查看>>
《响应式Web设计实践》一1.6 本书包含哪些内容
查看>>
《Java和Android开发实战详解》——导读
查看>>
《Netty 实战》Netty In Action中文版 第2章——你的第一款Netty应用程序(三)
查看>>
从学界到业界:关于数据科学的误解与事实
查看>>
3.6 HyperLogLog
查看>>
游戏玩家的福音:在 Ubuntu 上安装开源 VoIP 应用 Mumble
查看>>
《Web性能实践日志》一第1章 WebPageTest内部原理1.1 函数拦截
查看>>
《Android Studio应用开发实战详解》——第1章,第1.4节Android和Linux的关系
查看>>
《多核与GPU编程:工具、方法及实践》----3.4 信号量
查看>>
用机器学习的经验指导人生:如何实现学习效率最大化
查看>>
《Hack与HHVM权威指南》——1.6.1 没有类型的变量
查看>>
一次马失前蹄的SQL优化:递归查询引发的血案
查看>>
《HBase实战》一第一部分 HBase基础
查看>>
《触摸屏游戏设计》——导读
查看>>
《OpenGL超级宝典(第5版)》——第1章,第1.2节3D图形技术和术语
查看>>
如何让你的机器学习玩超级玛丽
查看>>
阿里NASA计划“亮剑”:谢崇进和他追求的科学极限
查看>>
docker 基本命令 (CentOs7 Docker 17.03.1-ce)
查看>>
Apache Spark源码走读(八)Graphx实现剖析&spark repl实现详解
查看>>