C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

C++ Primer 学习笔记_37_面向对象编程(8)--虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)
C++ Primer 学习笔记_37_面向对象编程(8)--虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小) 

回顾

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

        32位      64位
char      1       1
int       4      大多数4,少数8
long      4       8
float     4       4
double    8       8
指针       4       8(和long一样)
注意:指针不管是任何类型都是和long一样,double *也是4。


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

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

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

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

win32 可选的有1, 2, 4, 8, 16
linux 32 可选的有1, 2, 4
类的大小与数据成员有关与成员函数无关
类的大小与静态数据成员无关
虚继承对类的大小的影响
虚函数对类的大小的影响


1、含静态变量、虚函数的类的空间计算

    sizeof应用在类和结构的处理情况是相同的。但需要注意结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与单个对象的地址无关。


【例1】

下述代码中,sizeof(Test)=(),sizeof(s)=(),sizeof(test1)=()? 

#include <iostream>
using namespace std;
class Test
{
    int a;
    static double c;
};
Test *s;
class test1 {};
int main()
{
    cout << sizeof(Test) << endl;
    cout << sizeof(s) << endl;
    cout << sizeof(test1) << endl;
}

解答:4 4 1。


【例2】

下列表达式在32位及其编译环境下的值为()。

#include <iostream>
using namespace std;
class A{};
class B
{
public:
    B();
    virtual ~B();
};
class C
{
private:
#pragma pack(4)
    int i;
    short j;
    float k;
    char l[64];
    long m;
    char *p;
#pragma pack()
};
class D
{
private:
#pragma pack(1)
    int i;
    short j;
    float k;
    char l[64];
    long m;
    char *p;
#pragma pack()
};
int main()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    cout << sizeof(C) << endl;
    cout << sizeof(D) << endl;
    return 0;
}

解答:1  4  84  82。B的大小为4因为B中有指针vptr。D的计算是4+2+4+64+4+4=82。

注意:但是如果在64位的编译环境下,short都是4bit,因此输出的结果是1  4  96   96。



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


一、只出现虚继承的情况

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

运行结果(在VS2012中运行,其他编译器输出结果有所不同)

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

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

virtual base table 

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

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

virtual base table pointer(vbptr)

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)


从程序可以看出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;
}
运行结果:
8
12
Base::Fun1 ...
Base::Fun2 ...

Base::Fun1 ...
Derived::Fun2 ...
Derived::Fun3 ...

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

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

vptr:虚函数表指针

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

    从输出可以看出,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 **)ⅆ
    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;
}

运行结果(在VS2012中运行,其他编译器输出结果有所不同)

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

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

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)
C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

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 **)ⅆ
      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;
}

运行结果(在VS2012中运行,其他编译器输出结果有所不同)

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

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

C++ Primer 学习笔记_37_面向对象编程(八)-虚函数与多态(五):虚继承和虚函数对C++对象内存模型造成的影响(类/对象的大小)

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

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





参考:

C++ primer 第四版

C++ primer 第五版

版权声明:本文为博主原创文章,未经博主允许不得转载。