0.序
目前正在学习C++中,对于C++的类及其类的实现原理也挺感兴趣。于是打算通过观察类在内存中的分布更好地理解类的实现。因为其实类的分布是由编译器决定的,而本次试验使用的编译器为VS2015 RC,其编译环境为VC++,这里感谢@shenzhigang 提醒。所以此处的标题为《VC++中的类的内存分布》。因为博主可能比较懒,所以把这个知识点分作两次写。( ╯□╰ )。
1.对无虚函数类的探索
1.1 空类
我们先一步一步慢慢来,从一个空的类开始。
//空类 class test { };
int main(int argc, char *argv[]) { test ts; cout << sizeof(ts) << endl; return 0; }
结果输出的是1。
于是我们推测,对于一个空类,内存总会为其分配一个字节的空间。为此,我们可以来验证一下:
int main(int argc, char *argv[]) { test ts; char ch = '0'; int a1, a2; a1 = (int)(&ts); a2 = (int)(&ts + 1); memcpy(&ts, &ch, 1); cout << sizeof(ts) << endl; return 0; }
可以看到,a1为ts的地址(强制转化为int),a2为ts的下一个地址,然后我们去内存那里看一下
结果真的把ch里面的内容写入到ts中了。
综上可以得出一个结论,对于一个空类,编译器总会为其分配一个字节的内存。
1.2 仅含数据成员的类
首先对于数据成员为一个字节(char)的类,通过上述的测试代码,结果和空类一样,编译器分配了一个字节的空间给了类中的char。这是在我们的预料之内的事。
可是当我们的类设计成含有不同类型的数据结构的时候,结果就不同了:
class test { public: char c; int i; };
程序的输出结果是8。可以看到,此时类占用内存的空间是8个字节。
这就涉及到“内存对齐”了。所以接下来我们就先来探讨一下C++里的“内存对齐”。
内存对齐:
对于一个类(结构体),编译器为了提高内存读取速率以及可移植性,存在一种称作为“内存对齐”的规则。一般对于内存对齐,编译器会帮你完成,但是这种工作其实是可以由编程者自己完成的。
C++中,可以使用#pragma pack(n)的预编译处理进行设置“对齐系数”。(这个对齐系数在VC中一般默认为8。)
为了能够更好地了解内存中内存对齐的流程,我特地画了分配空间的流程图。(仅本人自己理解,如有谬误请各位大侠指出。)
下面我们通过实例来说明一下内存对齐。(在这里先只考虑数据成员不为类(结构体)的情况)
class test { public: char c; int i; short s; };
虽然类的内容一样,但是会因为对齐系数n的不同,内存中的分配也会有所不同。下图能够比较形象地说明,其中,红色表示char型,蓝色标识int型,绿色表示short型。图中的列数是根据min(max(结构体中的数据类型),n)确定的。
(1)例子1:pragma pack(1)
(2)例子2:pragma pack(2)
(3)例子3:pragma pack(4)
可以看到,不同的对齐系数会使内存的分布呈现不同的格局。
讨论完内存对齐之后,我们来看一看类中的static成员。
类中的static成员:
我们设计一个这样的类,类中包括有静态数据成员。
class test { public:
static int si; char c; int i; short s; };
结果我们发现,该类的大小还是和之前无异。
同时,我们通过cout语句查看类中的static成员地址。
cout << sizeof(ts) << ' ' << (int)&ts.si << ' ' << (int)(&ts) << endl;
结果得出的类的地址和类的static成员的地址相差十万八千里。显而易见,类中的static成员并不是和类储存在一起的。
综上可以得到的结论是:类中的成员数据中,仅有非static成员数据才会为其开辟内存空间。
1.3 包含成员函数的类
这里要先感谢一下@melonstreet 提到的问题,已改正。
对于类中的成员函数,我们知道,对于所有类,每个成员函数都只有一个副本。函数代码是存储在对象空间之外的。如果对同一个类定义了10个对象,这些对象的成员函数对应的是同一个函数代码段,而不是10个不同的函数代码段。在这里,关于具体到对象的成员函数是如何调用的,我们有两种猜想:第一种猜想是每一个对象中都必须开辟一段内存,用来存储指向类中的成员函数的指针,每次当外部对对象的成员函数进行调用的时候,通过访问对象空间中的函数指针从而访问函数。第二种猜想是类中的成员函数是独立出来的,每个对象中并没有储存成员函数的相关信息,而成员函数的调用是通过编译器在编译的时候自动帮我们选择要访问的函数。为此,我们也要进行一些测试。
class test { public: static int si; char c; int i; short s; test():c('0'),i(0),s(0) {}; void print(void) { cout << sizeof(test) << ' ' << (int)&si << ' ' << (int)this << endl; } };
输出结果显示内存仍然不变,说明成员函数在类的内存中并不占空间。
综上可以得出结论:对于无继承的类的成员函数,是独立出来的,类的内存中并没有存储相应的函数信息。对于成员函数的访问,是通过编译器完成的。
可是,在这里,我们少考虑了一种情况:虚函数的存在。这是一种特例,内存将会为其分配相应空间。在这里先不做讨论,且看下篇的具体分析。