在前面Base类的对象模型中,有base对象实例,虚函数表,静态变量和函数等,这些信息在内存中都有各自的保存位置。了解进程的内存空间布局,比如内存空间分成几大块,各种不同的数据分别保存在内存空间的哪个位置,对深入理解对象模型是非常有帮助的。
1、进程内存空间布局
当把一个可执行文件加载到内存后,就变成了一个进程。
通常进程的内存地址空间可分为以下几个部分:栈、堆、BSS段、数据段、代码段。
1.1 各内存段说明
(1)栈(stack):栈用于存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
(2)堆(heap):堆用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。若程序员不释放,则会发生内存泄漏。
(3)bss段(bss segment):通常是指用来存放程序中未初始化的全局变量的一块内存区域。bss段属于静态内存分配。
(4)data段(data segment):通常是指用来存放程序中已初始化的全局变量的一块内存区域。data段属于静态内存分配。
(5)代码段(code segment/text segment):通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
1.2 代码段和数据段的区别
(1)代码段和data段都在可执行文件中,由系统从可执行文件中加载,其内容由程序初始化;而bss段不在可执行文件中,其内容由系统初始化。
(2)bss段并不给该段的数据分配空间,只是记录数据所需空间的大小。而数据段会给数据分配空间,数据保存在目标文件中。
2、内存空间布局的验证
2.1 示例程序的进程空间布局
当分别声明下面2行语句时,其进程空间内存布局是不同的。
Base* pb = nullptr;
Base* pb = new Base();
代码验证:
int main() {
Base* pb = NULL;
printf("print()的地址:%p\n", &(Base::print));
printf("print_s()的地址:%p\n", &(Base::print_s));
printf("Base::base_s的地址:%p\n", &(Base::base_s));
pb->print();
pb->print_s();
std::cout << pb->base_s << std::endl;
}
估计有读者会感到奇怪,这个pb是NULL,应该报空指针错误啊。
但这个代码是能够正确执行的。而且在编译完成后,多次运行输出的print()、print_s()、base_s地址都是相同的。
为什么呢?
因为一个可执行文件,它的全局变量、全局函数、类静态成员变量等的地址值在编译完成时就已经确定好的,不会发生改变。
成员函数print()、静态函数print_s()和静态变量base_s是跟着类走的,成员函数、静态函数存在代码段,静态变量存在数据段。
下面我们再在main()里添加2行代码:
pb->print_v();
std::cout << pb->base_i << std::endl;
发现这2行代码报错了,我这里用vs2019是提示:pb 是 nullptr。
为什么呢?
非静态成员变量、虚函数指针才会放到对象中。pb是个NULL指针,没指向任何对象,所以在调用其非静态变量base_i、虚函数print_v()时会报nullptr错误。
2.2 变量的存储位置
int bss_1; // 未初始化的全局变量 - bss段
int bss_2 = 0; // 初始化为0的全局变量 - bss段
int data_1 = 1; // 初始化非0的全局变量 - data段
int main() {
static int bss_3; // 未初始化的静态局部变量 - bss段
static int bss_4 = 0; // 初始化为0静态局部变量 - bss段
static int data_2 = 1; // 初始化非0静态局部变量,data段
printf(" ------ bss段地址 ------\n");
printf(" bss_1 = %p\n", &bss_1);
printf(" bss_2 = %p\n", &bss_2);
printf(" bss_3 = %p\n", &bss_3);
printf(" bss_4 = %p\n\n", &bss_4);
printf(" ------ data段地址 ------\n");
printf(" data_1 = %p\n", &data_1);
printf(" data_2 = %p\n", &data_2);
return 0;
}
从下面的运行结果可以看到,bss段和data段里数据的存放顺序跟声明顺序是一致的,且data段的地址在bss段的下面。