C++ 类的内存结构

代码与可执行文件

代码段,数据段,BSS段,堆、栈

数据段、代码段、堆栈段、BSS段的区别
程序内存空间(代码段、数据段、堆栈段)

  • 代码段:简单说就是存储函数与常量的地方。C/C++写的成员函数,非成员函数都是在这里。
  • 数据段:初始化的全局变量,初始化的静态变量 被编译器放在这里
  • BSS 段: 这里存放未初始化的全局变量,未初始化的静态变量。BSS 部分并不占据存储空间,编译器只是把这些为初始化的全局/静态变量记录在这里。内存空间要等到执行阶段由系统分配,并完成初始化!这也是为什么内存结构上BSS 部分在栈空间下面的原因。
  • 堆、栈:这个比较好理解,不做介绍了

一个小c 程序demo

#include <stdio.h>

int global_init = 1;
int global_uinit;
static int global_static_init = 1;
static int global_static_uinit;
int main()
{
    int local_init = 1;
    int local_uinit;
    static int local_static_init = 1;
    static int local_static_uinit;

    return 0;
}

用 nm 查看结果:其中 D、d 都是表示 data 段; B、b 表示BSS 段。详细介绍参考 man nm

C++ 对象与存储

C++成员函数在内存中的存储方式

虚指针理解为一个 int64_t* 的数组,每个数组成员都是函数指针

先说明几点:

  • 空类大小不为 0
  • 静态类成员函数与类绑定,并不会被注入 this 指针
    因此静态成员函数可以通过类名直接调用,不需要创建类对象。同样因为没有 this 指针,所以静态成员函数也不能调用普通成员函数,只能访问静态成员变量。
  • 普通成员函数为类全局共享,不与类实例绑定
    但是普通成员函数的调用需要绑定实例,这是因为普通成员函数 this 指针的存在; 也因为普通成员函数绑定的不是类实例,所以普通继承关系不具有多态,而是由指针决定。
  • 虚表为类全局共享,不与类实例绑定; 虚指针与类实例绑定
    虚表是全局存在的,相当于一个全局变量,而不是每个类实例都创建一个虚表,虚表只能通过虚指针来访问。
    虚指针在Linux G++/Clang++ 实现是放在类的最前面。须指针指向虚表的操作由构造函数初始化。
  • 成员函数不占用类的内存空间
    即 new 一个对象只是创建了对象的数据部分,并不包含函数部分

类的实际内存结构如下:

虚表与虚指针

说明:

  1. 虚表指针总是存在在类的头部,并按类的继承顺序排放。一个子类可以有多个虚表指针,且虚指针个数和具有虚函数的基类个数相同。

  2. 虚成员函数总是按照声明顺序存在于虚表中。

  3. 如果存在同名函数,子类虚函数会覆盖每一个父类的每一个同名虚函数。

  4. 子类独有的虚函数填入第一个虚函数表中,且用父类指针是不能调用。

  5. 父类独有的虚函数不会被覆盖覆盖。仅子类和该父类指针能调用。

如下图类的内存结构图


图中代码参考链接

无虚函数

class Drive
{
public:
    void f() {}
};

int main()
{
    Drive d;

    cout << sizeof(d) << endl;
    return 0;
}

如下,类中没有虚函数,只有一个成员函数,以及其他默认构造析构函数。类似于空类,类的大小为1(注意空类大小不为0,因为为0的话,实例化后没法区分)。因此可以的出结论类的非虚成员函数信息不存在于对象实例中!

无继承

代码:

class Drive
{
public:
    virtual void vf() {}
    void f() {}
};

int main()
{
    Drive d;

    return 0;
}

如下,子类经过强制类型转换,得到虚表指针,并提取虚表指针的内容,经过转换可以得到第一个虚函数。虚表中只有一个虚函数。

单继承

class Base1
{
public:
    virtual void vb1f() {}
    virtual void vf() {}
};

class Drive : public Base1
{
public:
    virtual void vdf() {}
    virtual void vf() {}
    void f() {}
};

int main()
{
    Drive d;

    return 0;
}

虚表中只有多个虚函数。顺序是父类,子类的顺序。其中注意到双方共有的虚函数 “vf”, 在虚表中子类的虚函数覆盖了父类的需函数。

多继承

class Base1
{
public:
    virtual void vb1f() {}
    virtual void vf() {}
};

class Base2
{
public:
    virtual void vb2f() {}
    virtual void vf() {}
};

class Drive : public Base1, Base2
{
public:
    virtual void vdf() {}
    virtual void vf() {}
    void f() {}
};

int main()
{
    Drive d;

    return 0;
}

虚表中只有多个虚函数。顺序是父类Base1, 父类Base2,子类。

查看第一个虚表:其中注意到双方共有的虚函数 “vf”, 在虚表中子类的虚函数覆盖了父类的需函数。另外子类的虚函数 ”vdf“ 被放在了第一个虚表的后面。

查看第二个虚表:第二个虚表指针在地一个虚表指针后面。同样方式可以看到第二个虚表只有父类 Base2 的虚成员函数,而且共有的虚函数被子类的虚函数 vf 覆盖。

虚继承(菱形继承)

单虚继承情况和单继承完全一样,这里忽略,直接描述虚继承的菱形继承情况
注:虚继承在虚根基类初始化不一样,这也是为什么“用虚继承实现不能继承的类”的原理

class Base
{
public:
   virtual void vbbf() {}
   virtual void vbf() {}
};

class Base1 : virtual public Base
{
public:
   virtual void vb1f() {}
   virtual void vf() {}
};

class Base2 : virtual public Base
{
public:
   virtual void vb2f() {}
   virtual void vf() {}
};

class Drive : virtual public Base1, virtual public Base2
{
public:
   virtual void vdf() {}
   virtual void vf() {}
   void f() {}
};

int main()
{
   Base1 b1;
   Base2 b2;
   Drive d;

   return 0;
}

如上可以看到地一个虚表和预期完全一样,按照继承的顺序,虚函数的顺序存在虚表中。
但是第二个虚表就不一样了,第二个虚表的前两个成员都是空的,并不是指向 Base 的虚函数,所以可以知道编译器在这里做了处理,避免了菱形继承中尴尬的情况。

posted @ 2020-06-18 09:55  sinpo828  阅读(1591)  评论(0编辑  收藏  举报