C/C++语言特别适合用于开发压榨机器性能的程序,在资源有限的情况下,C/C++程序员不能放过机器的每一点计算力以及每一字节内存。当然了,要做到这一点,编程语言本身要提供精准的控制能力,C/C++语言作为一种强类型的编程语言,“精准控制”自然不在话下。这样看来,将机器性能发挥到极限的责任最后还是落在程序员身上了,要做到这一点,我们至少需要对各种数据类型使用的内存情况了然于胸。
使用的内存情况了然于胸类的内存模型
一般来说,我们在刚开始接触C/C++语言时,就会知道不同的数据类型占用内存空间通常不同的概念,比如char类型占用1个字节内存空间,int类型常常占用4字节内存空间,double类型常常占用8字节内存空间,有经验的程序员还会明白指针以及结构体占用内存空间的情况,等等。
请注意“常常”一词,C语言标准暂时还没有明确定义int等数据类型占用内存空间的情况。
事实上,我之前也在讨论C语言特性的时候专门写文章分析过数据类型与内存使用之间的关系,虽说也涉及到稍稍复杂一些的内存对齐概念,但是对于C++语言中的类,倒是完全没有提及,而类是C++语言中非常重要的概念,要是弄不清楚它的内存分布,“精准控制”就完全是吹牛了。
查阅了不少资料,没有找到直接讨论C++语言中类的内存分布的,我倒也不想再花时间在搜索引擎上,倒不如通过几条线索自己分析,直观的理解C++语言中类的内存分布了。
空类
请看下面这段C++语言代码:
classA{};coutsizeof(A)endl;//输出1
看来即使是空类,编译器在处理它时,也会隐含的添加1个字节。
类的成员变量
现在为类A增加几个成员变量,并且分别将此时类A的size,实例化对象的地址,以及成员变量的地址打印出来:
类的成员变量这段C++语言代码的输出如下,请看:
sizeofA:8
aaddr:0x7fff
A::pub_i1addr:0x7fff
A::pub_i2addr:0x7fff
可以看出,此时类A的的size恰好等于两个成员变量占用的内存之和(我的机器上sizeof(int)等于4),并且对象a的地址和第一个成员变量pub_i1的地址相等,第二个成员变量pub_i2的地址则紧跟在pub_i1之后,由此可以推测类A的内存分布如下图:
类A的内存分布此时的类A倒有些类似于C语言中的结构体:
structA{
intpub_i1;
intpub_i2;
};
的确如此,事实上,类A中成员变量的在内存中的存储方式也会涉及到内存对齐,这一点也和C语言中的结构体类似,不过这不是本文的重点,感到陌生的读者可以再回头看看我之前的文章。
类的成员函数
为类A添加两个成员函数,并且将其地址打印出来:
添加两个成员函数这段C++语言代码的输出如下:
sizeofA:8
aaddr:0x7ffe2dbc
A::pub_i1addr:0x7ffe2dbc
A::pub_i2addr:0x7ffe2dbcc
A::pub_foo1()addr:0xb28
A::pub_foo2()addr:0xbc2
从输出可以看出,成员函数的加入并未增加类A的size,而且两个成员函数pub_foo1()和pub_foo2()在地址上离类A的成员变量非常远,有理由推测此时类A在内存中的分布如下,请看:
类A的成员函数独立分布即,类A的(非虚)成员函数其实在内存中是独立分布的,并且彼此之间不毗邻,它们都远离类A对象的地址,并不占据类A的size。
类的私有成员
前面讨论的都是类A的公有成员,现在为其增加几个私有成员,并通过公有函数pub_foo1()将它们的地址打印出来,相关的C++语言代码如下,请看:
类的私有成员修改后的C++语言编译执行后输出如下,请看:
sizeofA:16
aaddr:0x7ffdbbfe
A::pub_i1addr:0x7ffdbbfe
A::pub_i2addr:0x7ffdbbfe
A::pub_foo1()addr:0xace
A::pub_foo2()addr:0xbb0
A::prv_i1addr:0x7ffdbbfe
A::prv_i2addr:0x7ffdbbfec
A::prv_foo1()addr:0xbba
A::prv_foo2()addr:0xbc4
此时sizeof(A)变为16了,这恰好等于4个int型成员变量占用的内存之和,并且仔细观察还能发现这4个int型成员在内存中是连续分布的,并且顺序是它们在类中被定义的顺序(这一点读者可自行验证)。private成员并无特别之处,私有函数也是独立于类A对象a独立存储的,因此推测此时类A的内存模型如下,请看:
private成员并无特别之处虚函数
我们已经知道C++语言中类的常规非虚成员函数并不占用类的size了,那么作为目前唯一已知动态绑定的虚函数是否也如此呢?我们在类A中添加两个虚函数:
虚函数编译并执行这段C++语言代码,可以得到如下输出,请看:
sizeofA:24
aaddr:0x7fffb26a22a0
A::pub_i1addr:0x7fffb26a22a8
A::pub_i2addr:0x7fffb26a22ac
A::pub_foo1()addr:0xb28
A::pub_foo2()addr:0xbc2
A::pub_vfoo1()addr:0xbcc
A::pub_vfoo2()addr:0xbd6
A::prv_i1addr:0x7fffb26a22b0
A::prv_i2addr:0x7fffb26a22b4
A::prv_foo1()addr:0xbe0
A::prv_foo2()addr:0xbea
此时类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(virtualtable[0]:%p\n,virtual_table[0]);
printf(virtualtable[1]:%p\n,virtual_table[1]);
对上述C++语言代码稍作解释:我们已经分析虚表指针__vptr恰好等于类A对象a的地址,因此可以根据a获得实际的虚表指针地址。因为__vptr指向的正是虚表(virtual_table),所以将得到的地址中的值取出就得到了virtual_table的地址,依次将virtual_table中的前2个(类A只有2个虚函数)元素的值打印出来,应该分别等于类A的两个虚函数地址,对不对呢?实际编译并执行这段C++语言代码,得到的输出如下,请看:
sizeofA:24
...
A::pub_vfoo1()addr:0xc16
A::pub_vfoo2()addr:0xc20
...
virtualtable[0]:0xc16
virtualtable[1]:0xc20
我省去了一些信息,这样便可以清晰的看出输出其实与我们的推测一致。
小结
敬请