c++类对象的内存分布

转载自:https://blog.twofei.com/496/要想知道c++类对象的内存布局, 可以有多种方式,比如:1)输出成员变量的偏移, 通过offsetof宏来得到2)通过调试器查看, 比如常用的VS1.只有数据成员的对象class Base1{publ...

转载自:https://blog.twofei.com/496/

  要想知道c++类对象的内存布局, 可以有多种方式,比如:

1)输出成员变量的偏移, 通过offsetof宏来得到

2)通过调试器查看, 比如常用的VS

1.只有数据成员的对象

class Base1
{
public:
    int base1_1;
    int base1_2;
};

对象大小及偏移:

可知对象布局:

  可以看到, 成员变量是按照定义的顺序来保存的, 最先声明的在最上边, 然后依次保存,类对象的大小就是所有成员变量大小之和.

2.没有虚函数的对象

class Base1
{
public:
    int base1_1;
    int base1_2;

    void foo(){}
};

结果如下:

  和前面的结果是一样的,因为成员函数可以被看作是类作用域的全局函数, 不在对象分配的空间里, 只有含有虚函数的类对象里才会有一个指针, 指向虚函数表。成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对应的对象上。对象调用成员函数时,编译器可以确定这些函数的地址,并通过传入this指针和其他参数,完成函数的调用。

3.拥有仅一个虚函数的类对象

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
};

Base1 b1;

 

结果如下:

  咦? 多了4个字节? 且 base1_1 和 base1_2 的偏移都各自向后多了4个字节! 说明类对象的最前面被多加了4个字节的"东东". 我们通过VS2013来瞧瞧类Base1的变量b1的内存布局情况:(由于没有写构造函数, 所以变量的数据没有初始化, Debug模式下, 未初始化的变量值为0xCCCCCCCC, 即:-858983460)

 

  base1_1前面多了一个变量 __vfptr(这就是虚函数表vtable指针,简称虚表指针), 其类型为void**, 这说明它是一个指向void*的指针,再看高亮部分,说明它是一个指向指针数组的指针. 再看看[0]元素, 其类型为void*, 其值为 ConsoleApplication2.exe!Base1::base1_fun1(void), 这是什么意思呢? 如果对WinDbg比较熟悉, 那么应该知道这是一种惯用表示手法, 它就是指 Base1::base1_fun1() 函数的地址.

所以,该类的对象大小为12个字节, 大小及偏移信息如下:

现在的类对象布局如下:

 

  注意到__vfptr前面的const修饰. 它修饰的是那个虚函数表, 而不是__vfptr. 

4.拥有多个虚函数的类对象

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

Base1 b1;

 

大小以及偏移信息如下:

  多了一个虚函数, 类对象大小却依然是12个字节! 再来看看VS形象的表现:

 

现在__vfptr所指向的指针数组中出现了第2个元素, 其值为Base1类的第2个虚函数base1_fun2()的函数地址. 通过上面两张图表, 我们可以得到如下结论:

  1. 更加肯定前面我们所描述的: __vfptr只是一个指针, 它指向一个函数指针数组(即: 虚函数表)
  2. 增加一个虚函数, 只是简单地向该类对应的虚函数表中增加一项而已, 并不会影响到类对象的大小以及布局情况

不妨, 我们再定义一个类的变量b2, 现在再来看看__vfptr的指向:

 

通过Watch 1窗口我们看到:

  1. b1和b2是类的两个变量, 理所当然, 它们的地址是不同的(见 &b1 和 &b2)
  2. 虽然b1和b2是类的两个变量, 但是: 它们的__vfptr的指向却是同一个虚函数表

由此我们可以总结出:

  同一个类的不同对象共用同一份虚函数表, 它们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表.

是时候该展示一下类对象的内存布局情况了:

 

结论:

  1. 虚函数表是编译器在编译时期为我们创建好的, 只存在一份,保存在.rodata只读数据段
  2. 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表

5.单继承且本身不存在虚函数的继承类的内存布局

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;
};

Derive1 d1;

来看看现在的内存布局:

基类在上边, 继承类的成员在下边依次定义! 展开来看看:

现在类的布局情况应该是下面这样:

6.覆盖的基类虚函数的单继承类的内存布局

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;

    // 覆盖基类虚函数
    virtual void base1_fun1() {}
};

Derivel d1;

现在的布局:

  注意高亮的那一行: 原本是Base1::base1_fun1(), 但由于继承类重写了基类Base1的此方法, 所以现在变成了Derive1::base1_fun1()!

7.定义了基类没有的虚函数的单继承的类对象布局

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;

    virtual void derive1_fun1() {}
};

Derive1 d1;

现在的布局:

  表面上看来几乎和第5种情况完全一样, 现在继承类明明定义了自身的虚函数, 但不见了, 那么, 来看看类对象的大小, 以及成员偏移情况吧:

  也没有变化, 现在我们只能从汇编入手了, 来看看调用derive1_fun1()时的代码:

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();

 

本文标题为:c++类对象的内存分布

基础教程推荐