Skip to content

类的内存布局

从简单到复杂吧,多态,虚继承、菱形继承……

成员变量

成员变量是顺序存储的,在内存(看实例化的时候被开辟在堆区还是栈区,只是位置不同,但是内存布局一致)空间会开辟一段连续内存,里面只放非静态数据成员,同时,也会进行一些内存对齐来使得 CPU 读取更快(Cache Line 那边已经说过

那么,静态成员变量 —— 自然是存放在全局 / 静态数据段

另外的,权限修饰的关键字并不会影响内存布局,只会影响编译期的检查。

C++
class Simple {
public:
    int a;      // 4 字节
private:
    char b;     // 1 字节
    double c;   // 8 字节
};

(以 64为操作系统为例 )那么看似应当占用 13 个字节,实际上会被对齐到 16 个字节

成员函数

如果把所有成员函数都存储在存变量的那段连续内存中,显然太笨了 —— 成员函数和其他普通的函数一样,都是存放在代码段里,然后在调用的时候 ——

C++
Logger lg;
lg.log();

---------

Logger_log(&lg);

也就是,实际上每个非静态的成员方法在本质上都会被隐式的传入一个this指针。

综上所述,成员方法并不会占用对象的内存

静态成员函数也是在代码段

C++
class MethodClass {
public:
    int a;
    static int s_count;     // 静态变量

    void doSomething();     // 普通方法
    static void print();    // 静态方法
};

这样一个什么都有的类,实际上被实例化后也只会占用 4 个字节

单继承

比较简单,先把父类的基础上,先把父类的内存布局抄一份下来,接着在尾部接上自己的内存布局

C++
class Base {
    int a;
};
class Derived : public Base {
    int b;
};

那么它的内存分布图 ——

+-------------------+ <--- Derived 对象起始地址
| Base::a (4 bytes) | \__ 属于父类 Base 的部分
+-------------------+ /
| Derived::b(4 bytes| \__ 属于子类 Derived 自己的部分
+-------------------+ /

多态

首先,因为多态函数调用的时候,其到底要调用哪个具体的方法,是无法在编译器确定,而是要在运行时确定的。

C++
Base *p = getObj();
p->foo();

由于 getObj 返回的对象只能在运行期直到,所以调用的 foo 是哪个也只能在运行时直到,所以需要用一种方式来对这些方法进行定位 ——

对于出现虚函数的类,会编译出对应的 vptr / vtable,来进行虚方法的定位

NOTE

The C++ standard does not specify how virtual functions should be implemented (this detail is left up to the implementation).

先说虚函数表,它记录了所有被 virtual 标记的虚函数的存放地址(函数指针地址,也就是虚函数的具体实现在代码段的位置

一个类的虚函数表的布局显然是在编译器就可以确定下来的,如果给每一个实例都分配一个显然是对内存空间的不尊重(严重浪费),所以它是放在只读段的一个函数指针数组。

接着只要类有虚函数(或继承自有虚函数的类),编译器就会在对象内存中加一个隐藏指针成员:vptr,用于指向自己这个类所对应的虚函数表。

所以每次在访问的时候,先读取到 vptr,然后再通过偏移量确定具体的实现。

举个例子 —— 假设 Base* p = new Derived(); p->func1();

  1. 取出 p 对象头部的 vptr 值(一个内存地址)。
  2. 跳转到该地址,即进入了静态区中的 Derived 虚函数表。
  3. 根据编译器确定的偏移量(func1 如果是第一个虚方法,通常在索引 0),然后取出表中的函数指针。
  4. 跳转到该指针指向的代码区,执行 Derived::func1 的指令。

综上所述,虚函数本质上就是一个普通函数,只是调用方式不同(通过 vtable 间接调用)。

那么性能损耗也是必然的,普通函数可以直接跳地址,但是虚函数要先读 vptr,再读 vtable,多出两次内存寻址。而且这两个地址一定是非连续的,容易导致 CPU 的分支预测失败,清空 pipeline

很聪明的查表方法(编译期生成),但是也是有一定的性能损耗

Released under the MIT License.