虚函数与虚表再探


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

前面总结了虚函数的一些基本知识,除了知道应用场景,怎么用,还要知道原理是什么,以及原理是怎么实现的.好的,总结出方法论四点,应用场景,使用方法,原理,原理实现.这篇文章打算带着自己,边实践中边思考虚函数的原理.
这篇博客真的是简单而又明了. 虚继承知道这几种情况应该就差不多了吧. 这篇博客对原理的解释和实践比较有名的.另外这位作者补充了两篇c++内存对象布局(上)c++内存对象布局(下)用实际的例程展示了对象在内存中的布局情况. 这篇文章教会了我如何用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 <vtable for Base+16>, 
  b = 0
}

d的内存结构:

(gdb) p d
$4 = (Derive) {
  <Base> = {
    _vptr.Base = 0x400bf0 <vtable for Derive+16>, 
    b = 4196496
  }, 
  members of Derive: 
  d = 0
}

可以发现子类的实例直接继承了父类中的虚函数表的指针,那么虚函数表的内容有什么区别呢
我们分别打印一下两个变量的虚函数的

(gdb) p /a ((long *)(*(long *)&b))[0]@4
$10 =   {0x400a24 <Base::fun1()>,
  0x400a50 <Base::fun2()>,
  0x602200 <_ZTVN10__cxxabiv120__si_class_type_infoE@@CXXABI_1.3+16>,
  0x400c40 <_ZTS6Derive>}
(gdb) p /a ((long *)(*(long *)&d))[0]@4
$11 =   {0x400a7c <Derive::fun1()>,
  0x400a50 <Base::fun2()>,
  0x400aa8 <Derive::fun3()>,
  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 <vtable for Base1+16>, 
  b1 = 4196909
}

b2的内存结构

(gdb) p b2
$3 = (Base2) {
  _vptr.Base2 = 0x400ce8 <vtable for Base2+16>, 
  b2 = 4197389
}

d的内存结构
(gdb) p d

$4 = (Derive) {
  <Base1> = {
    _vptr.Base1 = 0x400ca0 <vtable for Derive+16>, 
    b1 = 0
  }, 
  <Base2> = {
    _vptr.Base2 = 0x400cc8 <vtable for Derive+56>, 
    b2 = 4196496
  }, 
  members of Derive: 
  d1 = 0
}

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

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

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

菱形继承(非虚拟继承)

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) {
  <Base1> = {
    <Base> = {
      _vptr.Base = 0x400db0 <vtable for Derive+16>, 
      b = 0
    }, 
    members of Base1: 
    b1 = 0
  }, 
  <Base2> = {
    <Base> = {
      _vptr.Base = 0x400de0 <vtable for Derive+64>, 
      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 <Derive::func1()>,
  0x400ab4 <Base::func2()>,
  0x400b0c <Base1::func3()>,
  0x400bc2 <Derive::func5()>,
  0xfffffffffffffff0}
(gdb) p /a  ((long*)(*(long *)((int*)&d + sizeof(Base1)/4)))[0]@5 
$30 =   {0x400bbb <_ZThn16_N6Derive5func1Ev>,
  0x400ab4 <Base::func2()>,
  0x400b64 <Base2::func4()>,
  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) {
  <Base1> = {
    <Base> = {
      _vptr.Base = 0x400e20 <vtable for Derive+120>, 
      b = 4196496
    }, 
    members of Base1: 
    _vptr.Base1 = 0x400dc0 <vtable for Derive+24>, 
    b1 = 4197629
  }, 
  <Base2> = {
    members of Base2: 
    _vptr.Base2 = 0x400df0 <vtable for Derive+72>, 
    b2 = 0
  }, 
  members of Derive: 
  d = 0
}

可以体现出来这里面的Base的b变量只有一份拷贝,这里面其实隐藏了vbptr的虚基类表,如果有虚表的话,它就在虚表的下面.如果没有他就在类实例的开头.
关于vbptr的验证留到后面的文章,今天就到这儿吧.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值