C++对象模型(2)-- 进程内存空间布局

在前面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段的下面。

### C++ 存储器内存模型详解 #### 1. 基本概念 C++ 中的存储器内存模型定义了程序如何管理计算机系统的物理和虚拟内存资源。该模型不仅涉及变量、对象以及函数在内存中的分配方式,还包括这些实体之间相互作用的方式。 #### 2. 内存区域划分 C++ 程序运行时会将内存划分为多个不同的部分: - **栈区 (Stack)**:用于存放局部变量、参数等临时数据结构;当函数调用结束时,这部分内存会被自动释放[^2]。 - **堆区 (Heap)**:通过 `new` 或者 `malloc()` 动态分配的空间,在此区域内创建的对象直到显式删除 (`delete`) 或者程序终止才会被回收。相比栈来说更灵活但也更容易引发泄漏问题。 - **全局/静态区 (Global / Static Area)**:保存全局变量及静态成员的位置,其生命周期贯穿整个应用程序执行期间[^1]。 - **常量区 (Constant Area)**:类似于全局区但是专门用来放置编译期确定下来的字面量字符串和其他不可变数值[^3]。 - **代码区 (Code Section)**:包含可执行指令序列即机器码形式的目标文件内容,通常位于进程地址空间较低位置并受到操作系统保护防止非法修改[^4]。 #### 3. 对象布局与内存对齐 简单类型(如整型、浮点型)可以直接按照所需字节数依次排列于相邻单元格内形成复合体实例化后的映像;而对于含有指针字段或其他复杂成分的数据结构,则需遵循特定规则来决定各组成部分之间的相对偏移距离以确保高效访问性能的同时满足硬件平台关于边界条件的要求。 ```cpp class SimpleStruct { public: int a; // 占用 4 字节 char b; // 占用 1 字节, 后跟 3 字节填充使下一个成员能按适当对齐 }; ``` #### 4. 动态内存管理机制 为了简化程序员的工作负担同时也提高灵活性,现代 C++ 提供了一套完整的动态内存管理系统——包括但不限于运算符重载版本的新建(`operator new`) 和 删除(`operator delete`) ,允许用户自定义行为模式从而更好地适应具体应用场景下的需求变化趋势。 ```cpp int* pInt = new int{10}; // 使用 operator new 分配单个 int 变量 double* pData = static_cast<double*>(::operator new(sizeof(double))); // 调用全局 ::operator new 函数手动分配未初始化 double 类型空间 // ... do something ... delete pInt; // 销毁由上述语句建立起来的对象关系链路 ::operator delete(pData); // 显式销毁先前经由 ::operator new 获取到的大块原始字节数组 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值