从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

首先重新回顾一下关于类/对象大小的计算原则:

类大小计算遵循结构体对齐原则

第一个数据成员放在offset为0的位置

其它成员对齐至min(sizeof(member),#pragma pack(n)所指定的值)的整数倍。

整个结构体也要对齐,结构体总大小对齐至各个min中最大值的整数倍。

win32 可选的有1, 2, 4, 8, 16

linux 32 可选的有1, 2, 4

类的大小与数据成员有关与成员函数无关 类的大小与静态数据成员无关 虚继承对类的大小的影响 虚函数对类的大小的影响

下面通过实例来展示虚继承和虚函数对类大小造成的影响。

测试环境为:Win32 + Vs2008

一、只出现虚继承的情况

#include <iostream>
using namespace std;

class BB
{
public :
      int bb_ ;
};

class B1 : virtual public BB
{
public :
      int b1_ ;
};

class B2 : virtual public BB
{
public :
      int b2_ ;
};

class DD : public B1, public B2
{
public :
      int dd_ ;
};

int main (void)
{
      cout<<sizeof (BB)<< endl;
      cout<<sizeof (B1)<< endl;
      cout<<sizeof (DD)<< endl;

      B1 b1 ;
      int** p ;

      cout<<&b1 <<endl;
      cout<<&b1 .bb_<< endl;
      cout<<&b1 .b1_<< endl;

      p = (int **)&b1;
      cout<<p [0][0]<<endl;
      cout<<p [0][1]<<endl;

      DD dd ;
      cout<<&dd <<endl;
      cout<<&dd .bb_<< endl;
      cout<<&dd .b1_<< endl;
      cout<<&dd .b2_<< endl;
      cout<<&dd .dd_<< endl;
      p = (int **)&dd;
      cout<<p [0][0]<<endl;
      cout<<p [0][1]<<endl;
      cout<<endl ;
      cout<<p [2][0]<<endl;
      cout<<p [2][1]<<endl;

      BB* pp ;

      pp = &dd ;
      dd.bb_ = 10; //对象的内存模型在编译时就已经确定了,否则无法定义类的对象,因为要开辟内存
      int base = pp-> bb_;     // 通过间接访问 (其实pp 已经偏移了20 ),这需要运行时的支持
      cout<<"dd.bb_=" <<base<< endl;

      return 0;
}

从输出的地址和虚基类表成员数据可以画出对象内存模型图:

virtual base table 

本类地址与虚基类表指针地址的差 虚基类地址与虚基类表指针地址的差

virtual base table pointer(vbptr)

从程序可以看出pp是BB* 指针,通过打印pp 的值与&dd 比较可知,

cout<<(void*)&dd<<endl; cout<<(void*)pp<<endl;

pp实际上已经偏移了20个字节,如何实现的呢?先找到首个vbptr,找到虚基类BB地址与虚基类表指针地址的差,也即是20,接着pp偏移20个字节指向了dd对象中的BB部分,然后就访问到了bb_,这是在运行时才做的转换。记住:C++标准规定对对象取地址将始终为对应类型的首地址。

二、只出现虚函数的情况

(一):一般继承

#include <iostream>
using namespace std;

class Base
{
public :
    virtual void Fun1()
    {
        cout << "Base::Fun1 ..." << endl;
    }

    virtual void Fun2()
    {
        cout << "Base::Fun2 ..." << endl;
    }
    int data1_ ;
};

class Derived : public Base
{
public :
    void Fun2 ()
    {
        cout << "Derived::Fun2 ..." << endl;
    }
    virtual void Fun3()
    {
        cout << "Derived::Fun3 ..." << endl;
    }
    int data2_ ;
};

typedef void (* FUNC)(void );

int main (void)
{
    cout << sizeof (Base) << endl;
    cout << sizeof (Derived) << endl;
    Base b ;
    int **p = (int **)& b;
    FUNC fun = (FUNC) p[0][0];
    fun();
    fun = (FUNC )p[0][1];
    fun();
    cout << endl ;

    Derived d ;
    p = (int **)&d;
    fun = (FUNC )p[0][0];
    fun();
    fun = (FUNC )p[0][1];
    fun();
    fun = (FUNC )p[0][2];
    fun();

    return 0;
}

从输出的函数体可以画出对象内存模型图:

vtbl:虚函数表(存放虚函数的函数指针)

vptr:虚函数表指针

从输出可以看出,Derived类继承了Base::Fun1,而覆盖了Fun2,此外还有自己的Fun3。注意,因为Fun3是虚函数,才会出现在虚函数表,如果是一般函数是不会的,因为不用通过vptr间接访问。

(二)、钻石继承

#include <iostream>
using namespace std;

class BB
{
public:
    virtual void vpbb()
    {
        cout << "BB:vpbb().." << endl;
    }
    int bb_;
};
class B1 : public BB
{
public:
    virtual void vpb1()
    {
        cout << "B1:vpb1().." << endl;
    }
    int b1_;
};
class B2 : public BB
{
public:
    virtual void vpb2()
    {
        cout << "B2:vpb2().." << endl;
    }
    int b2_;
};
class DD : public B1, public B2
{
public:
    virtual void vpdd()
    {
        cout << "DD:vpdd().." << endl;
    }
    int dd_;
};

typedef void (* FUNC)(void );

int main()
{
    cout << sizeof(BB) << endl;
    cout << sizeof(B1) << endl;
    cout << sizeof(DD) << endl;
    cout << endl;

    DD dd ;
    cout << &dd << endl;
    cout << &dd.B1::bb_ << endl;
    cout << &dd.B2::bb_ << endl;
    cout << &dd .b1_ << endl;
    cout << &dd .b2_ << endl;
    cout << &dd .dd_ << endl;
    cout << endl;

    B1 b ;
    int **p = (int **)& b;
    FUNC fun = (FUNC) p[0][0];
    fun();
    fun = (FUNC )p[0][1];
    fun();
    cout << endl ;

    p = (int **)&dd
    fun = (FUNC)p[0][0];
    fun();
    fun = (FUNC)p[0][1];
    fun();
    fun = (FUNC)p[0][2];
    fun();

    fun = (FUNC)p[3][0];
    fun();
    fun = (FUNC)p[3][1];
    fun();

    cout << endl;

    return 0;
}

从成员输出的地址和通过虚函数表指针访问到的函数可以画出模型:

DD::vfdd 的位置跟继承的顺序有关,如果DD先继承的是B2, 那么它将跟在B2::vfb2 的下面。

如果派生类是从多个基类继承或者有多个继承分支(从所有根类开始算起),而其中若干个继承分支上出现了多态类,则派生类将从这些分支中的每个分支上继承一个vptr,编译器也将为它生成多个vtable,有几个vptr就生成几个vtable(每个vptr分别指向其中一个),分别与它的多态基类对应。

三、虚继承与虚函数同时出现的情况:

#include <iostream>
using namespace std;

class BB
{
public :
      virtual void vfbb()
     {
           cout<<"BB::vfbb" <<endl;
     }
      virtual void vfbb2()
     {
           cout<<"BB::vfbb2" <<endl;
     }
      int bb_ ;
};

class B1 : virtual public BB
{
public :
      virtual void vfb1()
     {
           cout<<"B1::vfb1" <<endl;
     }
      int b1_ ;
};

class B2 : virtual public BB
{
public :
      virtual void vfb2()
     {
           cout<<"B2::vfb2" <<endl;
     }
      int b2_ ;
};

class DD : public B1, public B2
{
public :
      virtual void vfdd()
     {
           cout<<"DD::vfdd" <<endl;
     }
      int dd_ ;
};

typedef void (* FUNC)(void);

int main (void)
{
      cout<<sizeof (BB)<< endl;
      cout<<sizeof (B1)<< endl;
      cout<<sizeof (DD)<< endl;

      BB bb ;
      int** p ;
      p = (int **)&bb;
      FUNC fun ;
      fun = (FUNC )p[0][0];
      fun();
      fun = (FUNC )p[0][1];
      fun();
      cout<<endl ;


      B1 b1 ;
     
      p = (int **)&b1;
      fun = (FUNC )p[0][0];
      fun();
      fun = (FUNC )p[3][0];
      fun();
      fun = (FUNC )p[3][1];
      fun();

      cout<<p [1][0]<<endl;
      cout<<p [1][1]<<endl;
      cout<<endl ;



      DD dd ;
      p = (int **)&dd;
      fun = (FUNC )p[0][0];
      fun();
      fun = (FUNC )p[0][1]; // DD::vfdd 挂在 B1::vfb1的下面
      fun();
      fun = (FUNC )p[3][0];
      fun();
      fun = (FUNC )p[7][0];
      fun();
      fun = (FUNC )p[7][1];
      fun();
     
      cout<<p [1][0]<<endl;
      cout<<p [1][1]<<endl;
      cout<<p [4][0]<<endl;
      cout<<p [4][1]<<endl;


      return 0;
}

从输出的虚基类表成员数据和虚函数体可以画出对象内存模型图:

上图中vfdd 出现的位置跟继承的顺序有关,如果DD先继承的是B2,那么它将跟在vfb2 的下面。

注意:如果没有虚继承,则虚函数表会合并,一个类只会存在一个虚函数表和一个虚函数表指针(同个类的对象共享),当然也不会有虚基类表和虚基类表指针的存在。

但如果是钻石继承,那么是会存在两份虚函数表和两份虚函数表指针的。

参考:

《深入探索C++对象模型》

C++ primer 第四版 Effective C++ 3rd C++编程规范

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CDA数据分析师

提升R代码运算效率的11个实用方法

众所周知,当我们利用R语言处理大型数据集时,for循环语句的运算效率非常低。有许多种方法可以提升你的代码运算效率,但或许你更想了解运算效率能得到多大的提升。本文...

1838
来自专栏python学习之旅

算法学习笔记(四):堆排序

1123
来自专栏noteless

[五]java函数式编程归约reduce概念原理 stream reduce方法详解 reduce三个参数的reduce方法如何使用

java8 流相关的操作中,我们把它理解 "累加器",之所以加引号是因为他并不仅仅是加法

1093
来自专栏编程

这是新手自学编程的开始,程序员入门的诀窍

虽然学习IT编程技术已经是越来越热了,但是如果作为一个新手程序员想要入门还是很困难的,这里有一些前端程序员总结的程序员入门的好技巧。相对java,C,Pytho...

2177
来自专栏菩提树下的杨过

恶心的0.5四舍五入问题

四舍五入是财务类应用中常见的需求,按中国人的财务习惯,遇到0.5统一向上进位,但是c#与java中默认的却不是这样。 见c#代码: 1 stat...

18010
来自专栏小白的技术客栈

Python之递归函数

Python之递归函数 好久没有更新内容了,也好久没有给大家打个招呼了,小白想死你们了。今天跟大家说说Python中的递归函数。 Python是支持递归函数的...

3336
来自专栏Java3y

十道算法题[二]

前言 清明不小心就拖了两天没更了~~ 这是十道算法题的第二篇了~上一篇回顾:十道简单算法题 最近在回顾以前使用C写过的数据结构和算法的东西,发现自己的算法和数据...

3019
来自专栏云霄雨霁

删数问题

1880
来自专栏C/C++基础

多益网络2016春季实习校招笔试回顾(C++游戏后台开发)

2016.04.16晚中山大学大学城校区(东校区)参加了多益网络的C++游戏后台开发的笔试。有几道笔试题还是值得斟酌和记录的,特记录如下。比较可惜,因为回老家了...

642
来自专栏从流域到海域

Python 切片(Slice)

在实际开发中,经常遇到下面的需求:在线性表(数组)中提取若干个元素的操作,提取规则有很多,比如说提取前5个、提取后5个、提取奇数/偶数位元素等等。 ...

18210

扫码关注云+社区