精华内容
下载资源
问答
  • 虚函数表在类中占用多大
    千次阅读
    2017-09-27 14:16:59

    总结一下VPTR 和 VTABLE 和类对象的关系:
    每一个具有虚函数的类都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个VTABLE虚函数表只有一个。
    在每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。
    2、虚继承
    这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先要通过加入一个虚指针来指向父类,因此有可能会有两个虚指针。
    二、(虚)继承类的内存占用大小
    首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。
    计算一个类对象的大小时的规律:
    1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
    2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
    3、因此一个对象的大小≥所有非静态成员大小的总和;
    4、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;
    5、虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);
    6、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;
    7、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。

    更多相关内容
  • 字节对齐的原理见链接https://www.cnblogs.com/0201zcr/p/4789332.html(注意64位系统虚函数指针占8字节,32位占4字节) 计算一个对象的大小时的规律: 1、空、单一继承的空、多重继承的空...

    前半部分转自https://www.cnblogs.com/SeekHit/p/7570247.html

    其中为32位字节对齐,后半部分给出自己的理解。

    字节对齐的原理见链接https://www.cnblogs.com/0201zcr/p/4789332.html(注意64位系统虚函数指针占8字节,32位占4字节)

    计算一个类对象的大小时的规律:

    • 1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
    • 2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
    • 3、因此一个对象的大小≥所有非静态成员大小的总和;
    • 4、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;
    • 5、虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者总共所占的空间大小为:8(或8乘以多继承时父类的个数);理解为虚函数表和虚基表分别占4字节。即基类的虚函数表总共占4个字节,子类的虚基表每个基类地址占4个字节
    • 6、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;
    • 7、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。
    • 例子一

      class A     
      {     
      };    
        
      class B     
      {  
          char ch;     
          virtual void func0()  {  }   
      };   
        
      class C    
      {  
          char ch1;  
          char ch2;  
          virtual void func()  {  }    
          virtual void func1()  {  }   
      };  
        
      class D: public A, public C  
      {     
          int d;     
          virtual void func()  {  }   
          virtual void func1()  {  }  
      };     
        
      class E: public B, public C  
      {     
          int e;     
          virtual void func0()  {  }   
          virtual void func1()  {  }  
      };  
        
      int main(void)  
      {  
          cout<<"A="<<sizeof(A)<<endl;    //result=1 //64位为1
          cout<<"B="<<sizeof(B)<<endl;    //result=8 //64位为16 
          cout<<"C="<<sizeof(C)<<endl;    //result=8 //64位为16
          cout<<"D="<<sizeof(D)<<endl;    //result=12//64位为24
          cout<<"E="<<sizeof(E)<<endl;    //result=20//64位为40
          return 0;  
      }  

    原帖中写到“求sizeof(D)的时候,需要明白,首先VPTR指向的虚函数表中保存的是类D中的两个虚函数的地址(只有一个虚基类地址指针),然后存放基类C中的两个数据成员ch1、ch2,注意内存对齐,然后存放数据成员d,这样4+4+4=12。”32位系统虚函数表指针内存是4字节,而64位系统虚函数表指针内存是8字节。所以在64位系统中结果不同于32位系统,过程为8+2+4对齐为8+8+4(8+8是满足父类的对齐要求)继续对齐为8+8+8满足(对齐为最大类型内存整数倍的对齐要求),故结果为24.
    32位系统中求sizeof(E)的时候,首先是类B的虚函数地址,然后类B中的数据成员,再然后是类C的虚函数地址,然后类C中的数据成员,最后是类E中的数据成员e,同样注意内存对齐,这样4+4+4+4+4=20。

    64位系统中求sizeof(E)的时候,首先是类B的虚函数地址(8字节),然后类B中的数据成员(并对齐为8+8=16字节),再然后是类C的虚函数地址(8字节),然后类C中的数据成员(8+1+1对齐为8+8=16字节),最后是类E中的数据成员e,同样注意内存对齐(8字节),这样16+16+8=40。

    展开全文
  • 2、虚继承 这个是比较不好理解的,对于虚继承,若派生有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚 2、一个类中虚函数本身、成员函数(包括
  • 最近被问到一个关于继承虚函数表的问题,当时回答是可能存在虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了个...

    前言

    最近被问到一个关于多继承虚函数表的问题,当时回答是可能存在多个虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了多个含有虚函数基类的子类来说,指向虚函数表的指针应该不止一个。

    问题

    虚函数表的问题是从C++多态的概念引出的,要想实现多态有3个条件:

    1. 存在继承:没有继承就没有多态(运行时),在多态中必须存在有继承关系的父类和子类。
    2. 重写函数:父类中需要定义带有 virtual 关键字的函数,而在子类中重写一个名字和参数与父类中定义完全相同的函数。
    3. 向上转型:将父类的指针和引用指向子类的对象。

    满足以上三个条件,当使用父类的指针调用带有 virtual 关键字的函数时,就会产生多态行为。

    实现这种多态表现的核心内容就是虚函数表,对于带有 virtual 关键字的函数地址会被放入一个表格,而在类中会有一个指向虚函数表的指针指向这个表格,表明这个表格属于类的一部分。

    对于父类来说,这个表格中都是自己类的虚函数,而对于子类来说,首先这个虚函数表包含父类中所有的虚函数,当子类重写某个虚函数时就会用子类重写后的函数地址替换原来父类中定义的函数地址,同时在子类的虚函数表中还会包含子类独有的虚函数。

    由此可见虚函数表的不同和复杂性还是体现在子类上,所以之后会分别测试单继承、多继承、菱形继承三种情况下虚函数表的不同,主要看一下虚函数表的个数和内存布局情况。

    测试环境

    首先来说明一下测试环境,测试工具是VS2013,对于int *p; sizeof(p)的结果是4,说明编译环境是32位的,这个对后面查看内存结构非常关键。

    开始测试

    使用VS2013查看类的内存布局非常方便,因为类的大小在编译期间就已经确定了,不用运行就可以通过添加编译选项知道类的大小和布局,而指向虚函数表的指针也会占用类的大小,如果说编译的时候确定了类的大小,那从侧面也说明了在编译期间虚函数表实际上也确定了。

    使用VS2013查看类的布局时,可以在项目的属性页:“配置属性”–>“C/C++”–>“命令行”中输入以下任意一个命令,

    • /d1reportAllClassLayout :这个选项可以在VS的输出窗口显示所有相关联的类结构,因为一些外部类也会显示,最终的内容会非常多,需要自己辨别有用的信息。
    • /d1reportSingleClassLayoutXXX :这个选项只会在输出窗口显示指定的类结构,只需要将XXX替换成想显示的类的名字即可,缺点就是无法同时显示多个想查看的类。

    无虚函数简单类结构

    在查看虚函数表的结构之前,先使用之前的编译参数来查看一下简单的类结构,排除虚函数的干扰,能更清楚的了解类成员在类中的布局情况,有一点需要提一下,成员变量会占用类的大小,但是成员函数不会,如果有虚函数,所有的虚函数会被放入一个表格,而在类中放置一个指向虚函数表的指针,来看一下简单代码:

    class CBase
    {
    public:
        void func() {}
    public:
        int m_var1;
    };
    
    class CDerived : public CBase
    {
    public:
        void func() {}
    public:
        int m_var2;
    };
    

    编译输出的类的内存布局为:

    1>  class CBase    size(4):
    1>      +---
    1>   0  | m_var1
    1>      +---
    1>
    1>  class CDerived  size(8):
    1>      +---
    1>      | +--- (base class CBase)
    1>   0  | | m_var1
    1>      | +---
    1>   4  | m_var2
    1>      +---
    

    从上面的输出内容来看,很清楚的可以看到基类 CBase 的大小 size(4) 占用4个字节,只有一个成员变量 m_var1,在类中偏移量为0的位置,而派生类 CDerived 占用8个字节大小,第一个成员继承自基类 CBasem_var1,在类中偏移量为0的位置,还有一个子类独有的成员变量 m_var2,在类中偏移量为4的位置。

    掌握着这种简单类的查看类结构的方法,接下来开始看一下包含虚函数的类的内存布局。

    包含虚函数的类结构

    查看包含虚函数的类结构相对来说麻烦一点,先来说两个符号,免得一会看见结构发懵,vfptr 表示类中指向虚函数表的指针,通常放在类的起始位置,比成员变量的位置都要靠前, vftable 表示类中引用的虚函数表,在具体分析是还有有一些修饰符,用来表明是谁的虚函数表。

    单继承

    这种情况的下的子类的虚函数表很简单,在该子类的内存布局上,最开始的位置保存了一个指向虚函数表的指针,虚函数表中包含了从父类继承的虚函数,当子类中重写父类虚函数时会将虚函数表中对应的函数地址替换,最后添加上自己独有的虚函数地址,下面上代码分析一下:

    class CBase
    {
    public:
        void func0() {}
        virtual void func1() {}
        virtual void func2() {}
    public:
        int m_var1;
    };
    
    class CDerived : public CBase
    {
    public:
        virtual void func2() {}
        virtual void func3() {}
        void func4() {}
    public:
        int m_var2;
    };
    

    上面这两个类的内存布局情况如下:

    1>  class CBase size(8):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | m_var1
    1>      +---
    1>
    1>  CBase::$vftable@:
    1>      | &CBase_meta
    1>      |  0
    1>   0  | &CBase::func1
    1>   1  | &CBase::func2
    1>
    1>  CBase::func1 this adjustor: 0
    1>  CBase::func2 this adjustor: 0
    1>
    1>
    1>  class CDerived  size(12):
    1>      +---
    1>      | +--- (base class CBase)
    1>   0  | | {vfptr}
    1>   4  | | m_var1
    1>      | +---
    1>   8  | m_var2
    1>      +---
    1>
    1>  CDerived::$vftable@:
    1>      | &CDerived_meta
    1>      |  0
    1>   0  | &CBase::func1
    1>   1  | &CDerived::func2
    1>   2  | &CDerived::func3
    1>
    1>  CDerived::func2 this adjustor: 0
    1>  CDerived::func3 this adjustor: 0
    

    看起来是不是比没有虚函数时复杂多了,不过不要着急,从上到下慢慢分析就好了,这次的基类 CBase 大小是8个字节,首先是{vfptr}这个指向虚函数表的指针,在类中的偏移量是0,接下来是成员变量 m_var1,在类中偏移量是4。

    然后是 CBase::$vftable@ 表示基类 CBase 的虚函数表,其中第一行 &CBase_meta 看起来怪怪的,这里我们不展开(因为我也没弄太懂),应该是和虚函数表相关的元数据,第二行是一个0,看起来是一个偏移量,这里没有偏移,当出现偏移时我们再试着分析(相信我,马上就会出现),第三行内容 &CBase::func1 是自己类的虚函数,前面有一个0,应该是指该虚函数在虚函数表中索引,第四行也是相同的情况。

    接下来出现了两行非常相似的内容,看一下CBase::func1 this adjustor: 0,这句代码中的关键是 adjustor,其实有是一个偏移量,据说涉及到thunk技术,据说“thunk其实就是一条汇编指令,操作码是0xe9,就是jmp,后面紧跟操作数”,这里我们就不展开了,如果后面弄明白了可以单独写一篇总结,到此为止基类的内存结构就分析完了。

    继续看派生类 CDerived,它的大小是12个字节,内部结构首先是 {vfptr} 一个指向虚函数表的指针,偏移量为0,m_var1 是从父类继承的成员变量,偏移量为4,而 m_var2 是自己类独有的成员变量,偏移量是8。

    然后看派生类对应的虚函数表 CDerived::$vftable@,跳过前两行直接看一下后面几个函数,发现只有 func1 是基类的,而函数 func2func3 都是派生类的,出现这种情况的原因是子类重写了函数 func2func3 ,所以用重写后的函数地址替换了从基类继承的虚函数,造成了目前看到的状况。

    最后又出现了两行 adjustor,很奇怪为什么 func1 函数没有 adjustor,貌似这个 adjustor 只对当前类有效,先留个疑问,接下来看一下多继承。

    多继承

    当多个父类中都包含虚函数的时候,和子类关联的虚函数表就不止一个了,这个情况是可以通过使用sizeof(子类)来简单验证的:

    这一部分是在没有VS的情况下预先写下的,本来考虑使用VS展开布局后,这一段就没有什么必要了,但是后来想想还是留着吧,因为这一段使用的g++编译器,64位环境,每个指针占用8个字节,通过不同的环境调试,更加可以证明,多继承下的多个虚函数表的存在性:

    class W
    {
    public:
        long n;
    public:
        void func(){}
    };
    

    对于这样的一个简单类,sizeof(W) = 8,类的大小等于成员变量的大小。

    class W1
    {
    public:
        long n1;
    public:
        virtual void func1(){}
    };
    
    class W2
    {
    public:
        long n2;
    public:
        virtual void func2(){}
    };
    

    对于上面这两个简单的包含虚函数的类,sizeof(W1) = 16,sizeof(W2) = 16,因为每个类都除了一个 long 类型的成员变量以外,还包含了指向虚函数的一个指针,所以类的大小是16个字节。

    class WW : public W1, public W2
    {
    public:
        long nn;
    public:
        virtual void func(){}
    };
    

    而继承了 W1W2 这两个父类的子类 WW 在继承了两个成员变量 n1n2 之外,还有自己的成员变量 nn,三个变量占用字节24个,而计算类 WW 的的大小 sizeof(W1) = 40,也就是说除了成员变量24个字节,还剩余了16个字节的空间没有着落,我们知道它至少包含一个指向虚函数表的指针,占用8个字节的大小,还剩8个字节没有找到用处,从此处分析应该还有一个指向虚函数表的指针,具体的情况可以看一下内存分布。

    接下来和单继承的分析方法一样,写代码编译查看布局:

    class CBase0
    {
    public:
        void func0() {}
        virtual void func1() {}
        virtual void func2() {}
        virtual void func3() {}
    public:
        int m_var0;
    };
    
    class CBase1
    {
    public:
        void func0() {}
        virtual void func2() {}
        virtual void func3() {}
        virtual void func4() {}
    public:
        int m_var1;
    };
    
    class CDerived : public CBase0, public CBase1
    {
    public:
        virtual void func1() {}
        virtual void func2() {}
        virtual void func4() {}
        virtual void func5() {}
        void func6() {}
    public:
        int m_var2;
    };
    

    上面3个类描述了一个简单的多继承的情况,之所以写这么多函数就是构建一种,既有虚函数覆盖,又有单独不被覆盖的情况,下面展示了这段代码的内存布局。

    1>  class CBase0    size(8):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | m_var0
    1>      +---
    1>
    1>  CBase0::$vftable@:
    1>      | &CBase0_meta
    1>      |  0
    1>   0  | &CBase0::func1
    1>   1  | &CBase0::func2
    1>   2  | &CBase0::func3
    1>
    1>  CBase0::func1 this adjustor: 0
    1>  CBase0::func2 this adjustor: 0
    1>  CBase0::func3 this adjustor: 0
    1>
    1>
    1>  class CBase1    size(8):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | m_var1
    1>      +---
    1>
    1>  CBase1::$vftable@:
    1>      | &CBase1_meta
    1>      |  0
    1>   0  | &CBase1::func2
    1>   1  | &CBase1::func3
    1>   2  | &CBase1::func4
    1>
    1>  CBase1::func2 this adjustor: 0
    1>  CBase1::func3 this adjustor: 0
    1>  CBase1::func4 this adjustor: 0
    1>
    1>
    1>  class CDerived  size(20):
    1>      +---
    1>      | +--- (base class CBase0)
    1>   0  | | {vfptr}
    1>   4  | | m_var0
    1>      | +---
    1>      | +--- (base class CBase1)
    1>   8  | | {vfptr}
    1>  12  | | m_var1
    1>      | +---
    1>  16  | m_var2
    1>      +---
    1>
    1>  CDerived::$vftable@CBase0@:
    1>      | &CDerived_meta
    1>      |  0
    1>   0  | &CDerived::func1
    1>   1  | &CDerived::func2
    1>   2  | &CBase0::func3
    1>   3  | &CDerived::func5
    1>
    1>  CDerived::$vftable@CBase1@:
    1>      | -8
    1>   0  | &thunk: this-=8; goto CDerived::func2
    1>   1  | &CBase1::func3
    1>   2  | &CDerived::func4
    1>
    1>  CDerived::func1 this adjustor: 0
    1>  CDerived::func2 this adjustor: 0
    1>  CDerived::func4 this adjustor: 8
    1>  CDerived::func5 this adjustor: 0
    

    内容很多,前面两个基类 CBase0CBase1 的布局很简单,参照之前的分析很容易看懂,直接从派生类看起吧。

    我们发现派生类 CDerived 中确实有两个指向虚函数表的指针,接下来看一下这两个虚函数表,这个虚函数表和前面遇到的格式一样,除了第一行的元数据,第二行的诡异偏移量0,剩下的虚函数指针有的是从基类继承来的,有的是被当前派生类覆盖的,还有派生类自己独有的。

    而第二个虚函数表就有点意思了,首先是少了 &CDerived_meta 这一行,然后偏移量终于不是0了,而是-8,从派生类 CDerived 的内存布局上来看,以下开始大胆假设,至于小心求证的部分放到以后来做(看自己的进步状态了)。

    第二个指向虚函数表的指针是不是距离类的起始偏移量是8,我猜这个-8的意思就是指的这个偏移量,这个值有可能被后面使用,第二行出现了 &thunk: this-=8; goto CDerived::func2,其中包含 thunk 字样,表示这个 func2 不归我管,你去-8偏移量的那个虚函数表里找一找。

    还有一点你有没有发现 func5 这个函数只在第一个虚函数表中出现,而没有出现在第二个虚函数表中,这也是一个规则,自己独有的虚函数放到第一个虚函数表中,这可能也是为什么只有第一个虚函数表包含元数据行。

    最后一点,我们发现对于函数 func4 来说 adjustor 终于不是0了,而值变成了8,仿佛在说这个虚函数只在偏移量的为8的位置。

    菱形继承

    对于这一部分,并没有太多新的内容,只是简单的菱形继承中,最初的基类在最终的子类中会包含两份,而虚函数的样子并没有太大的不同,接下来简单看一下代码和对应的内存布局即可,因为菱形继承并不被提倡,所以也不用花太多时间来分析这个问题。

    class CSuper
    {
    public:
        virtual void func0() {}
        virtual void func1() {}
    public:
        int m_var;
    };
    
    class CBase0 : public CSuper
    {
    public:
        virtual void func1() {}
        virtual void func2() {}
    public:
        int m_var0;
    };
    
    class CBase1 : public CSuper
    {
    public:
        virtual void func1() {}
        virtual void func3() {}
    public:
        int m_var1;
    };
    
    class CDerived : public CBase0, public CBase1
    {
    public:
        virtual void func1() {}
        virtual void func3() {}
        virtual void func4() {}
    public:
        int m_var2;
    };
    
    1>  class CSuper    size(8):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | m_var
    1>      +---
    1>
    1>  CSuper::$vftable@:
    1>      | &CSuper_meta
    1>      |  0
    1>   0  | &CSuper::func0
    1>   1  | &CSuper::func1
    1>
    1>  CSuper::func0 this adjustor: 0
    1>  CSuper::func1 this adjustor: 0
    1>
    1>
    1>  class CBase0    size(12):
    1>      +---
    1>      | +--- (base class CSuper)
    1>   0  | | {vfptr}
    1>   4  | | m_var
    1>      | +---
    1>   8  | m_var0
    1>      +---
    1>
    1>  CBase0::$vftable@:
    1>      | &CBase0_meta
    1>      |  0
    1>   0  | &CSuper::func0
    1>   1  | &CBase0::func1
    1>   2  | &CBase0::func2
    1>
    1>  CBase0::func1 this adjustor: 0
    1>  CBase0::func2 this adjustor: 0
    1>
    1>
    1>  class CBase1    size(12):
    1>      +---
    1>      | +--- (base class CSuper)
    1>   0  | | {vfptr}
    1>   4  | | m_var
    1>      | +---
    1>   8  | m_var1
    1>      +---
    1>
    1>  CBase1::$vftable@:
    1>      | &CBase1_meta
    1>      |  0
    1>   0  | &CSuper::func0
    1>   1  | &CBase1::func1
    1>   2  | &CBase1::func3
    1>
    1>  CBase1::func1 this adjustor: 0
    1>  CBase1::func3 this adjustor: 0
    1>
    1>
    1>  class CDerived  size(28):
    1>      +---
    1>      | +--- (base class CBase0)
    1>      | | +--- (base class CSuper)
    1>   0  | | | {vfptr}
    1>   4  | | | m_var
    1>      | | +---
    1>   8  | | m_var0
    1>      | +---
    1>      | +--- (base class CBase1)
    1>      | | +--- (base class CSuper)
    1>  12  | | | {vfptr}
    1>  16  | | | m_var
    1>      | | +---
    1>  20  | | m_var1
    1>      | +---
    1>  24  | m_var2
    1>      +---
    1>
    1>  CDerived::$vftable@CBase0@:
    1>      | &CDerived_meta
    1>      |  0
    1>   0  | &CSuper::func0
    1>   1  | &CDerived::func1
    1>   2  | &CBase0::func2
    1>   3  | &CDerived::func4
    1>
    1>  CDerived::$vftable@CBase1@:
    1>      | -12
    1>   0  | &CSuper::func0
    1>   1  | &thunk: this-=12; goto CDerived::func1
    1>   2  | &CDerived::func3
    1>
    1>  CDerived::func1 this adjustor: 0
    1>  CDerived::func3 this adjustor: 12
    1>  CDerived::func4 this adjustor: 0
    

    虚继承

    解决菱形继承的一个常用的办法就是改为虚继承,实际上虚继承中就是将从最基类中继承的公共部分提取出来放在最子类的末尾,然后在提取之前的位置用一个叫做vbptr的指针指向这里。

    之前看到过一种说法:

    虚继承内部实现也相当复杂,似乎破坏了OO的纯洁性

    至于复杂不复杂,看看后面的内存布局就很清楚了,那是相当复杂,其中出现了各种偏移,简单了解下就行了,如果不是维护老代码,谁现在还写这样的结构。

    class CSuper
    {
    public:
        virtual void func0() {}
        virtual void func1() {}
    public:
        int m_var;
    };
    
    class CBase0 : virtual public CSuper
    {
    public:
        virtual void func1() {}
        virtual void func2() {}
    public:
        int m_var0;
    };
    
    class CBase1 : virtual public CSuper
    {
    public:
        virtual void func1() {}
        virtual void func3() {}
    public:
        int m_var1;
    };
    
    class CDerived : public CBase0, public CBase1
    {
    public:
        virtual void func1() {}
        virtual void func3() {}
        virtual void func4() {}
    public:
        int m_var2;
    };
    
    1>  class CSuper    size(8):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | m_var
    1>      +---
    1>
    1>  CSuper::$vftable@:
    1>      | &CSuper_meta
    1>      |  0
    1>   0  | &CSuper::func0
    1>   1  | &CSuper::func1
    1>
    1>  CSuper::func0 this adjustor: 0
    1>  CSuper::func1 this adjustor: 0
    1>
    1>
    1>  class CBase0    size(20):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | {vbptr}
    1>   8  | m_var0
    1>      +---
    1>      +--- (virtual base CSuper)
    1>  12  | {vfptr}
    1>  16  | m_var
    1>      +---
    1>
    1>  CBase0::$vftable@CBase0@:
    1>      | &CBase0_meta
    1>      |  0
    1>   0  | &CBase0::func2
    1>
    1>  CBase0::$vbtable@:
    1>   0  | -4
    1>   1  | 8 (CBase0d(CBase0+4)CSuper)
    1>
    1>  CBase0::$vftable@CSuper@:
    1>      | -12
    1>   0  | &CSuper::func0
    1>   1  | &CBase0::func1
    1>
    1>  CBase0::func1 this adjustor: 12
    1>  CBase0::func2 this adjustor: 0
    1>
    1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
    1>            CSuper      12       4       4 0
    1>
    1>
    1>  class CBase1    size(20):
    1>      +---
    1>   0  | {vfptr}
    1>   4  | {vbptr}
    1>   8  | m_var1
    1>      +---
    1>      +--- (virtual base CSuper)
    1>  12  | {vfptr}
    1>  16  | m_var
    1>      +---
    1>
    1>  CBase1::$vftable@CBase1@:
    1>      | &CBase1_meta
    1>      |  0
    1>   0  | &CBase1::func3
    1>
    1>  CBase1::$vbtable@:
    1>   0  | -4
    1>   1  | 8 (CBase1d(CBase1+4)CSuper)
    1>
    1>  CBase1::$vftable@CSuper@:
    1>      | -12
    1>   0  | &CSuper::func0
    1>   1  | &CBase1::func1
    1>
    1>  CBase1::func1 this adjustor: 12
    1>  CBase1::func3 this adjustor: 0
    1>
    1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
    1>            CSuper      12       4       4 0
    1>
    1>
    1>  class CDerived  size(36):
    1>      +---
    1>      | +--- (base class CBase0)
    1>   0  | | {vfptr}
    1>   4  | | {vbptr}
    1>   8  | | m_var0
    1>      | +---
    1>      | +--- (base class CBase1)
    1>  12  | | {vfptr}
    1>  16  | | {vbptr}
    1>  20  | | m_var1
    1>      | +---
    1>  24  | m_var2
    1>      +---
    1>      +--- (virtual base CSuper)
    1>  28  | {vfptr}
    1>  32  | m_var
    1>      +---
    1>
    1>  CDerived::$vftable@CBase0@:
    1>      | &CDerived_meta
    1>      |  0
    1>   0  | &CBase0::func2
    1>   1  | &CDerived::func4
    1>
    1>  CDerived::$vftable@CBase1@:
    1>      | -12
    1>   0  | &CDerived::func3
    1>
    1>  CDerived::$vbtable@CBase0@:
    1>   0  | -4
    1>   1  | 24 (CDerivedd(CBase0+4)CSuper)
    1>
    1>  CDerived::$vbtable@CBase1@:
    1>   0  | -4
    1>   1  | 12 (CDerivedd(CBase1+4)CSuper)
    1>
    1>  CDerived::$vftable@CSuper@:
    1>      | -28
    1>   0  | &CSuper::func0
    1>   1  | &CDerived::func1
    1>
    1>  CDerived::func1 this adjustor: 28
    1>  CDerived::func3 this adjustor: 12
    1>  CDerived::func4 this adjustor: 0
    1>
    1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
    1>            CSuper      28       4       4 0
    

    总结

    1. 虚函数表是用来实现多态的核心内容。
    2. 多继承很强大但是不要滥用,当多个基类都含有虚函数时,派生类会有多个指向虚函数表的指针。
    3. 忘记菱形继承吧,为了取消二义性引入虚继承,结果造成内存分布复杂而又难以理解,大道至简,回归本质吧!
    展开全文
  • C++的大小及虚函数表

    千次阅读 2017-04-20 23:46:16
    之前用C的时候,停挺经常计算结构体空间的,现在突然用C++了,好像还不知道C++大小怎么算的。 1....C++类中,数据成员的初始化是按照类中声明的先后顺序初始化,而与函数参数的传入顺序没有关系

    之前用C的时候,停挺经常计算结构体空间的,现在突然用C++了,好像还不知道C++类大小怎么算的。

    1. 类的组成

    我也不知道这么说是否正确,我觉得类主要有两类成员:数据成员,成员函数,并且
    * 数据成员有静态和非静态之分
    * 函数成员有静态,非静态和虚函数(virtual)之分

    数据成员
    在C++类中,数据成员的初始化是按照类中声明的先后顺序初始化,而与函数参数的传入顺序没有关系
    其中,静态函数为所有类成员所共有,不计入某个类的大小,静态成员的初始化一般在类的外部实现
    如:

    xxx.h

    class base{
      private:
          static int i;
    }

    xxx.cpp

    int base::i=1;

    注意:不要在头文件定义(初始化静态数据成员),大多数情况下会引起重复定义错误。
    成员的大小计算应该和结构体一样,下面我们来验证一下

    class basic{
        static int a;
        char b[10];
        int c;
    };
    
    int basic::a=1;
    
    
    int main ()
    {
        basic test;
        cout<<"size:"<<sizeof(test)<<endl;
        return 0;
    }

    输出为16,可以看出,a不占此类内存,b占10个字节,c四个字节,64位系统8字节对其,故为16

    * 函数成员 *
    类与结构体相比,多了属于自己的函数,类的非静态和静态函数都不占用类的内存,但是虚函数占用一个地址字节的内存。
    而且无论定义多少个虚函数,都只占用一个地址字节的内存。

    class basic{
        private:
            static int a;
            char b[10];
            int c;
    
        public:
            static void fun1(){cout<<"fun1"<<endl;}
            void fun2(){cout<<"fun2"<<endl;}
            virtual void fun3(){cout<<"fun3"<<endl;}
            virtual void fun4(){cout<<"fun4"<<endl;}
    
    
    };
    int basic::a=1;
    
    int main ()
    {
        basic test;
        cout<<"size:"<<sizeof(test)<<endl;
        return 0;
    }

    输出为24,结论正确

    既然扯到虚函数了,就说说虚函数吧
    如果一个类中存在虚函数,编译器会做以下三件事
    * 为该类分配一个虚函数表,它存有虚函数在执行器的地址
    * 在该类中安插一个虚指针,指向该类的虚表
    * 将每一个虚函数的入口地址存放在虚函数表中相应的slot

    所以在类的内存中储存的,就是指向虚函数表的虚指针
    那接下来,我们来看看,类里面的数据成员和虚函数是怎样储存的,在这里,我先输出了类成员地址,为了方便,我直接将成员替换为了公有成员

    #include <iostream>
    using namespace std;
    class basic{
        public:
            static int a;
            char b[10];
            int c;
    
            static void fun1(){cout<<"fun1"<<endl;}
            void fun2(){cout<<"fun2"<<endl;}
            virtual void fun3(){cout<<"fun3"<<endl;}
            virtual void fun4(){cout<<"fun4"<<endl;}
    };
    
    int basic::a=1;
    
    int main ()
    {
        basic test;
        cout<<"size:"<<sizeof(test)<<endl;
    
        cout<<"addr of test:"<<&test<<endl;
        cout<<"addr of test.a:"<<&test.a<<endl;
        for(int i=0;i<10;++i)
        {
            cout<<"addr of test.b["<<i<<"]:"<<(void *)&(test.b[i])<<endl;
            //这里得强制转换一下,不然坑爹的cout会把他当做字符串处理的
        }
        cout<<"addr of of test.c:"<<&test.c<<endl;
    
        return 0;
    }

    输出:

    size:24
    addr of test:0x7ffca3bd1980
    addr of test.a:0x602080
    addr of test.b[0]:0x7ffca3bd1988
    addr of test.b[1]:0x7ffca3bd1989
    addr of test.b[2]:0x7ffca3bd198a
    addr of test.b[3]:0x7ffca3bd198b
    addr of test.b[4]:0x7ffca3bd198c
    addr of test.b[5]:0x7ffca3bd198d
    addr of test.b[6]:0x7ffca3bd198e
    addr of test.b[7]:0x7ffca3bd198f
    addr of test.b[8]:0x7ffca3bd1990
    addr of test.b[9]:0x7ffca3bd1991
    addr of of test.c:0x7ffca3bd1994

    可以看出来,a的地址明显不在类的地址范围内,类的地址与第一个元素地址相差8个字节,即64位机的地址字节,这8个字节中储存虚函数指针,下面验证这一点

    #include <iostream>
    using namespace std;
    
    
    class basic{
        public:
            static int a;
            char b[10];
            int c;
    
            static void fun1(){cout<<"fun1"<<endl;}
            void fun2(){cout<<"fun2"<<endl;}
            virtual void fun3(){cout<<"fun3"<<endl;}
            virtual void fun4(){cout<<"fun4"<<endl;}
    };
    
    int basic::a=1;
    
    
    int main ()
    {
        basic test;
        cout<<"size:"<<sizeof(test)<<endl;
    
        cout<<"addr of test:"<<&test<<endl;
        cout<<"addr of test.a:"<<&test.a<<endl;
        for(int i=0;i<10;++i)
        {
            cout<<"addr of test.b["<<i<<"]:"<<(void *)&(test.b[i])<<endl;
            //这里得强制转换一下,不然坑爹的cout会把他当做字符串处理的
        }
        cout<<"addr of of test.c:"<<&test.c<<endl;
    
        void (*pfun3)(void); //函数指针
        void (*pfun4)(void); //函数指针
    
        pfun3 = (void(*)(void))*(long *)(*(long*)(&test));
        // 这里应该用typedef void(*FUN)(void)处理下会比较好,我只是想这样写写 long 刚好是64bit
        pfun3();
        pfun4 = (void(*)(void))*((long *)(*(long*)(&test))+1);
        // (long *)(&test) 将test的地址转化为系统字节长度地址再取出来  在这个地址上存放这虚函数表的地址
        // *(long *)(&test) 将虚函数指针所指向的对象取出来  得出的值应该是虚函数表的地址 同时也是第一个虚函数的地址
        // *(long *)*(long *)(&test) 从虚函数表的地址中取出内容  即第一个虚函数
        // (long *)*(long *)(&test)+1  虚函数表地址+1 即第二个虚函数地址  取出来即是fun4
        pfun4();
        return 0;
    }
    

    输出

    size:24
    addr of test:0x7ffe4ed2da70
    addr of test.a:0x602080
    addr of test.b[0]:0x7ffe4ed2da78
    addr of test.b[1]:0x7ffe4ed2da79
    addr of test.b[2]:0x7ffe4ed2da7a
    addr of test.b[3]:0x7ffe4ed2da7b
    addr of test.b[4]:0x7ffe4ed2da7c
    addr of test.b[5]:0x7ffe4ed2da7d
    addr of test.b[6]:0x7ffe4ed2da7e
    addr of test.b[7]:0x7ffe4ed2da7f
    addr of test.b[8]:0x7ffe4ed2da80
    addr of test.b[9]:0x7ffe4ed2da81
    addr of of test.c:0x7ffe4ed2da84
    fun3
    fun4

    从上面实验可以看出,类的内存结构如下图所示

    1

    fun3和fun4多写了一个括号。。。

    刚入门c++找对象,如果有出错的地方还请大神指教

    展开全文
  • 所有的虚函数都必须有定义,因为编译器直到运行前也不知道到底要调用哪个版本的虚函数。 只有通过指针或引用调用虚函数才会发生动态绑定,因为只有这种情况,引用或指针的静态类型与对象本身的动态类型才会不同。
  • 【C++】虚函数表存放哪里?

    千次阅读 2021-03-22 22:04:33
    3.虚函数表存储虚函数的地址,即虚函数表的元素是指向成员函数的指针,而类中虚函数的个数编译时期可以确定,即虚函数表的大小可以确定,即大小是编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆. ...
  • 多态成立的三个条件:继承、徐函数重写、父类指针指向子类对象。 多态的C++实现方法:virtual关键字,告诉编译器要执行多态;不要根据指针类型来判断如何调用对应的函数,而是要根据指针所实际指向的对象的类型来...
  • 1.多重继承可能产生虚函数表 代码示例:会产生虚函数表 #include <iostream> #include <string> using namespace std; class BaseA { public: virtual void funcA() { cout <<...
  •  GCC, 无论是虚函数还是虚继承, 都需要将指针存储在虚函数表(virtual function table), 占用4个字节.  继承会继承基类的数据, 和虚函数表, 即继承基类的空间.  代码: /* * test.cpp * * Created ...
  • 下面小编就为大家带来一篇C++对象内存分布详解(包括字节对齐和虚函数表)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 虚函数表存放哪里

    千次阅读 2018-09-24 17:28:07
    3.虚函数表存储虚函数的地址,即虚函数表的元素是指向成员函数的指针,而类中虚函数的个数编译时期可以确定,即虚函数表的大小可以确定,即大小是编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆. ...
  • 首先如果自己去设计内存占用时,从节省内存的角度去考虑的话,我觉得这些实例化后的应该是共用一张虚函数表的,因为每个内的虚函数都是一样的。因此,我们可以如下进行一些验证: class classA { virtual ...
  • C++的虚函数表指针vptr

    千次阅读 2019-03-22 19:54:20
    剑指Offer开篇有这样讲过: 定义一个空,对该求sizeof结果为1,因为声明该... 如果该类型添加一个构造函数和析构函数,再对其求sizeof还是1,因为你生命一个空本身就存在默认构造函数和析构函数。 ...
  • 一、虚函数的工作原理  虚函数的实现要求对象携带额外的信息,这些信息用于运行时...典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl
  • 一、C++成员函数在内存的存储方式  用去定义对象时,系统会为每一个对象分配存储空间。如果一个包括了数据和函数,要分别为数据和函数的代码分配存储空间。按理说,如果用同一个定义了10个对象,那么就...
  • 虚函数 空间占用大小

    千次阅读 2014-09-23 10:07:16
     每一个具有虚函数的都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个的所有对象所共有的,也就是说无论用户声明了多少个对象,但是这个VTABLE虚函数表...
  • 探究C++:虚函数表究竟怎么回事?

    千次阅读 2020-09-12 21:55:25
    我们知道,一个Class,如果有定义虚函数,那么这个对象所占用的存储空间,会保存一个指向虚函数表的指针,结果是这个的大小会增加4,即一个指针的大小。 那么这个指针存储在类的什么地方?虚函数表里是...
  • 静态多态和动态多态–虚函数、纯虚函数 ...2、虚函数一般用继承个子类继承同一基类,若某种行为上不同的派生有着自己的实现方式。这种情况我们就会用到多态。采用基类中将此函数定义成虚函数,派生...
  • 虚函数 在类的定义,前面有 virtual 关键字的成员函数就是虚函数。 注:派生类中的成员函数 与 基类虚函数同名且同参数的函数,不加...虚函数因为用了虚函数表机制,调用的时候会增加内存开销,具体见下文。
  • 虚函数占用大小Sizeof

    千次阅读 2014-04-30 11:14:08
    参考:... ...   根据原始定义,知道: 一旦存在虚函数,中将存在虚函数表,变量会多一个虚函数表的指针,从而sizeof时,空间会增大4字节(32位编译情况,64位编译时指针占
  • 关于虚函数表,可见博客 传送 ,但是要注意,一般被继承的第一个基类的虚函数表合并至子类的虚函数表,(基类的虚函数前,除非有虚函数覆盖,子类的虚函数后) 主要讲了无虚继承情况下各种情况的虚函数表的...
  • 对象内存布局计算l 空、单一继承的空、多重继承的空所占...l 当类中声明了虚函数(不管是1个还是个),那么实例化对象时,编译器会自动对象里安插一个指针vPtr指向虚函数表VTable;l 虚承继的情况:由
  • 虚函数虚函数相当于函数指针,占用四个字节(对于32位),在类中虚函数占用四个字节,其成员函数不占的内存。基类定义虚函数,优先调用子类的同名函数,覆盖虚函数。基类指针访问不同派生对象,调用不同方法。...
  • vtable c++ 内存布局 查看内存布局 虚函数表 多重继承
  • C++虚函数表原理浅析

    2021-04-24 00:43:58
    C++虚函数表和对象存储C++的虚函数实现了多态的机制,也就是用父类型指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数,这种技术可以让父类的指针有“多种形态”,这也是一种泛...
  • 下面小编就为大家带来一篇C++的多态与虚函数的内部实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 37,555
精华内容 15,022
关键字:

虚函数表在类中占用多大