C++语言中的类在内存中的分布是怎样的?也是内存对齐的吗?对象的虚表指针存放在哪里?C++中类的内存模型,在内存中是如何存储的?虚函数是如何存储的

C/C++语言特别适合用于开发压榨机器性能的程序,在资源有限的情况下,C/C++程序员不能放过机器的每一点计算力以及每一字节内存。当然了,要做到这一点,编程语言本身要提供精准的控制能力,C/C++语言作为一种强类型的编程语言,“精准控制”自然不在话下。这样看来,将机器性能发挥到极限的责任最后还是落在程序员身上了,要做到这一点,我们至少需要对各种数据类型使用的内存情况了然于胸。

类的内存模型

一般来说,我们在刚开始接触C/C++语言时,就会知道不同的数据类型占用内存空间通常不同的概念,比如 char 类型占用 1 个字节内存空间,int 类型常常占用 4 字节内存空间,double 类型常常占用 8 字节内存空间,有经验的程序员还会明白指针以及结构体占用内存空间的情况,等等。

请注意“常常”一词,C语言标准暂时还没有明确定义 int 等数据类型占用内存空间的情况。

事实上,我之前也在讨论C语言特性的时候专门写文章分析过数据类型与内存使用之间的关系,虽说也涉及到稍稍复杂一些的内存对齐概念,但是对于C++语言中的类,倒是完全没有提及,而类是C++语言中非常重要的概念,要是弄不清楚它的内存分布,“精准控制”就完全是吹牛了。

查阅了不少资料,没有找到直接讨论C++语言中类的内存分布的,我倒也不想再花时间在搜索引擎上,倒不如通过几条线索自己分析,直观的理解C++语言中类的内存分布了。

空类

请看下面这段C++语言代码:

class A {
};
cout << sizeof(A) << endl; // 输出 1

看来即使是空类,编译器在处理它时,也会隐含的添加 1 个字节。

类的成员变量

现在为类 A 增加几个成员变量,并且分别将此时类 A 的 size,实例化对象的地址,以及成员变量的地址打印出来:

class A {
public:
    int pub_i1;
    int pub_i2;
};

A a;
cout << "sizeof A: " << sizeof(A) << endl;
cout << "a addr: " << &a << endl;
cout << "A::pub_i1 addr: " << &a.pub_i1 << endl;
cout << "A::pub_i2 addr: " << &a.pub_i2 << endl;

这段C++语言代码的输出如下,请看:

sizeof A: 8
a addr: 0x7fff98567040
A::pub_i1 addr: 0x7fff98567040
A::pub_i2 addr: 0x7fff98567044

可以看出,此时类 A 的的 size 恰好等于两个成员变量占用的内存之和(我的机器上sizeof(int)等于 4),并且对象 a 的地址和第一个成员变量 pub_i1 的地址相等,第二个成员变量 pub_i2 的地址则紧跟在 pub_i1 之后,由此可以推测类 A 的内存分布如下图:

类 A 的内存分布

此时的类 A 倒有些类似于C语言中的结构体:

struct A {
    int pub_i1;
    int pub_i2;
};

的确如此,事实上,类 A 中成员变量的在内存中的存储方式也会涉及到内存对齐,这一点也和C语言中的结构体类似,不过这不是本文的重点,感到陌生的读者可以再回头看看我之前的文章。

类的成员函数

为类 A 添加两个成员函数,并且将其地址打印出来:

class A {
public:
    int pub_i1;
    int pub_i2;

    void pub_foo1() {}
    void pub_foo2() {}
};

A a;
cout << "sizeof A: " << sizeof(A) << endl;
cout << "a addr: " << &a << endl;
cout << "A::pub_i1 addr: " << &a.pub_i1 << endl;
cout << "A::pub_i2 addr: " << &a.pub_i2 << endl;

printf("A::pub_foo1() addr: %p\n", (void *)&A::pub_foo1);
printf("A::pub_foo2() addr: %p\n", (void *)&A::pub_foo2);

这段C++语言代码的输出如下:

sizeof A: 8
a addr: 0x7ffe2dbc3120
A::pub_i1 addr: 0x7ffe2dbc3128
A::pub_i2 addr: 0x7ffe2dbc312c
A::pub_foo1() addr: 0x400b28
A::pub_foo2() addr: 0x400bc2

从输出可以看出,成员函数的加入并未增加类 A 的 size,而且两个成员函数 pub_foo1() 和 pub_foo2() 在地址上离类 A 的成员变量非常远,有理由推测此时类 A 在内存中的分布如下,请看:

类A的成员函数独立分布

即,类 A 的(非虚)成员函数其实在内存中是独立分布的,并且彼此之间不毗邻,它们都远离类 A 对象的地址,并不占据类 A 的 size。

类的私有成员

前面讨论的都是类 A 的公有成员,现在为其增加几个私有成员,并通过公有函数 pub_foo1() 将它们的地址打印出来,相关的C++语言代码如下,请看:

class A {
...
private:
    int prv_i1;
    int prv_i2;

    void pub_foo1() {
        cout << "A::prv_i1 addr: " << &prv_i1 << endl;
        cout << "A::prv_i2 addr: " << &prv_i2 << endl;

        printf("A::prv_foo1() addr: %p\n", (void *)&A::prv_foo1);
        printf("A::prv_foo2() addr: %p\n", (void *)&A::prv_foo2);
    }
    void prv_foo2() {}
};
...
a.pub_foo1();

修改后的C++语言编译执行后输出如下,请看:

sizeof A: 16
a addr: 0x7ffdbbfe6980
A::pub_i1 addr: 0x7ffdbbfe6980
A::pub_i2 addr: 0x7ffdbbfe6984
A::pub_foo1() addr: 0x400ace
A::pub_foo2() addr: 0x400bb0
A::prv_i1 addr: 0x7ffdbbfe6988
A::prv_i2 addr: 0x7ffdbbfe698c
A::prv_foo1() addr: 0x400bba
A::prv_foo2() addr: 0x400bc4

此时 sizeof(A) 变为 16 了,这恰好等于 4 个 int 型成员变量占用的内存之和,并且仔细观察还能发现这 4 个 int 型成员在内存中是连续分布的,并且顺序是它们在类中被定义的顺序(这一点读者可自行验证)。private 成员并无特别之处,私有函数也是独立于类 A 对象 a 独立存储的,因此推测此时类 A 的内存模型如下,请看:

private 成员并无特别之处

虚函数

我们已经知道C++语言中类的常规非虚成员函数并不占用类的 size 了,那么作为目前唯一已知动态绑定的虚函数是否也如此呢?我们在类 A 中添加两个虚函数:

class A {
public:
    ...
    void pub_foo2() {}
    virtual void pub_vfoo1() {}
    virtual void pub_vfoo2() {}
private:
    ...
};
...
printf("A::pub_vfoo1() addr: %p\n", (void *)&A::pub_vfoo1);
printf("A::pub_vfoo2() addr: %p\n", (void *)&A::pub_vfoo2);

a.pub_foo1();

编译并执行这段C++语言代码,可以得到如下输出,请看:

sizeof A: 24
a addr: 0x7fffb26a22a0
A::pub_i1 addr: 0x7fffb26a22a8
A::pub_i2 addr: 0x7fffb26a22ac
A::pub_foo1() addr: 0x400b28
A::pub_foo2() addr: 0x400bc2
A::pub_vfoo1() addr: 0x400bcc
A::pub_vfoo2() addr: 0x400bd6
A::prv_i1 addr: 0x7fffb26a22b0
A::prv_i2 addr: 0x7fffb26a22b4
A::prv_foo1() addr: 0x400be0
A::prv_foo2() addr: 0x400bea

此时类 A 的 size 为 24,增加了 8 个字节,看来虚函数的确很特别(常规函数并不增加类的 size)。还不止于此,在我的机器上,一个函数指针占用的内存空间为 8 字节,这里我们添加了 2 个虚函数,却只增加 1 个函数指针的大小,为什么呢?

还记再前面一节中我们曾提到C++语言编译器会为含有虚函数的类添加虚表存放虚函数指针吗?这里多出一个指针正是虚表指针__vptr:只需要一个指针就能够找到虚表,进而在虚表中找到所有的虚函数。请注意,含有虚函数的类 A 的对象 a 的地址已经不等于第一个成员变量的地址,而是多出了 8 个字节的内存偏移,这个偏移量恰好能够存放一个指针,因此我们推测含有虚函数的类 A 在内存中的分布如下图,请看:

含有虚函数的类 A 在内存中的分布

这种推测对不对呢?我们可以再做实验确认下,编写下面的C++语言代码:

...
a.pub_foo1();

void **__vptr = (void **)&a;
void **virtual_table = (void **)(*__vptr);

printf("virtual table[0]: %p\n", virtual_table[0]);
printf("virtual table[1]: %p\n", virtual_table[1]);

对上述C++语言代码稍作解释:我们已经分析虚表指针__vptr恰好等于类 A 对象 a 的地址,因此可以根据&a获得实际的虚表指针地址。因为__vptr指向的正是虚表(virtual_table),所以将得到的地址中的值取出就得到了 virtual_table 的地址,依次将 virtual_table 中的前 2 个(类A只有 2 个虚函数)元素的值打印出来,应该分别等于类 A 的两个虚函数地址,对不对呢?实际编译并执行这段C++语言代码,得到的输出如下,请看:

sizeof A: 24
...
A::pub_vfoo1() addr: 0x400c16
A::pub_vfoo2() addr: 0x400c20
...
virtual table[0]: 0x400c16
virtual table[1]: 0x400c20

我省去了一些信息,这样便可以清晰的看出输出其实与我们的推测一致。

小结

本文主要通过简单的C++语言代码示例分析和讨论了类在内存中的布局,最后我们还分析了所谓的“虚表”以及“虚表指针”在内存中的分布,当然了,本文并无实际的理论支撑,一切都是从实践推导出来的,但是这样多多少少可以提供一些参考,事实上,本文更多的是介绍一种分析方法,下一节将使用同样的方法分析类在继承的过程中的内存变化,敬请关注。

全部代码请点这里

阅读更多:   C++虚函数
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif