对象的内存模型
1. C++的内存格局
C++程序的内存格局通常分为四个区:
a. 全局数据区(data area):存放全局变量,静态数据和常量。已初始化的全局变量保存在.data段,未初始化的全局变量保存在.bss段中。
b. 代码区(code area):存放所有类成员函数和非成员函数的代码,该区域是只读的。
c. 栈区(stack area):存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
d. 堆区(heap area):除a,b,c后,余下的空间都被称为堆区。
物理内存中的这些分区大小有限,所以编译器编译出来的可执行文件是一个叫虚拟内存空间的东西,一般大小为4G。
2. C++类的组成
在C++类中有两种成员数据:static、nonstatic;三种成员函数:static、nonstatic、virtual。
C++类对象中的成员变量和成员函数是分开存储的,其中:
a. 成员变量:
普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式。每个对象都独占一份成员变量。
静态成员变量:存储于全局数据区中。
b. 成员函数:存储于代码段中,只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。
既然很多对象共用一块代码?代码是如何区分具体对象呢?如何知道函数内操作的是谁的成员变量呢?
C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。函数调用的时候,会传入该对象的指针,自然就知道访问谁的数据成员。
静态成员函数和非静态成员函数的区别也仅仅是在于这个this指针参数,静态成员函数是没有这个参数的。
3. 对象的大小
每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,
而成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的。所以说new一个对象其实只是new出了成员变量。
如下代码输出皆为12。
class C1 { public: int i, j, k; // 4 + 4 + 4 = 12 }; class C2 { public: int i, j, k; static int m; public: int GetK() const { return k; } void SetK(int val) { k = val; } }; int main() { printf("c1:%d \n", sizeof(C1)); printf("c2:%d \n", sizeof(C2)); return 0; }
4. 如果类中有虚成员函数,那虚函数表存放在哪里呢?
a. 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表.即虚函数表不是函数,不是程序代码,肯定不能存储在代码段。
b. 虚函数表存储虚函数的地址,即虚函数表的元素是指向类虚成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小
可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中。
所以:函数表和静态成员变量一样,存放在全局数据区。
5. 派生类对象如何构建
基类中的数据成员,通过继承成为派生类对象的一部分,需要在构造派生类对象的过程中调用基类构造函数来正确初始化;
注意这里并没有创建基类对象,调用基类的构造函数是为了初始化派生类对象中的基类成员。
派生类会全部继承基类的成员变量(会开辟内存空间),但是对于基类中的private成员,派生类是无权访问的,只能调用基类的接口来访问。
C++标准并没有明确规定派生类的对象在内存中如何分布,来自基类的成员和派生类独有的成员在内存中不一定连续存储。
贴一个例子,右图是派生类对象逻辑上的布局: