C++内存分布与内存模型

数据段和存储区是计算机内存中的两个不同的概念。

数据段是指程序中存放数据的内存段,包括全局变量、静态变量等。数据段通常是在程序编译时就确定的,它有一个固定的大小,只能读取和写入,不能执行。数据段通常位于程序的静态存储区。

存储区则是指计算机内存中分配给程序使用的内存区域,包括栈、堆、全局数据区和代码区。存储区分为动态存储区和静态存储区。

静态存储区是指由编译器分配的内存空间,生命周期与程序运行时间相同,在程序运行期间一直存在。静态存储区包括全局数据区和代码区(也就是data段、bss段、代码段和常量数据段)。全局数据区用于存放静态变量和全局变量,代码区用于存放程序的指令。

动态存储区主要指的是堆区,它在程序运行期间按需分配和释放。而栈区虽然动态变化,但因其管理方式和用途的不同,通常不直接归类于“动态存储区”的讨论范畴

自动存储区:栈用于存放函数调用时的局部变量、函数参数和返回地址等信息,具有后进先出的特点。

总之,数据段是存放程序数据的内存段,而存储区则是计算机内存中分配给程序使用的内存区域。

C++的内存结构

栈(Stack)

作用:

存储局部变量: 函数内部定义的局部变量通常存储在栈上。这些变量在函数调用开始时分配内存,在函数返回时自动释放。

管理函数调用: 当一个函数被调用时,其返回地址、参数值、以及函数的局部变量都会存储在栈上。这被称为栈帧(Stack Frame)。栈帧在函数执行完毕后会被销毁。

控制递归: 递归函数通过栈来保存每次函数调用的状态,这样递归可以在多次调用后正确地返回到上一层调用。

使用:

栈内存分配: 栈内存的分配由编译器自动完成。例如,当您定义一个局部变量时,编译器会在栈上为它分配空间。

栈帧创建和销毁: 每次函数调用时,都会在栈上创建一个新的栈帧。这个栈帧包含了函数的局部变量、参数和返回地址。函数返回时,这个栈帧会被销毁,释放内存。

递归: 在递归函数中,每次递归调用都会创建一个新的栈帧。这允许递归函数在多次调用之间保持状态。

限制:

大小限制: 栈的大小通常是有限的,当函数调用层次太深或者局部变量占用太多内存时,可能会导致栈溢出(Stack Overflow)。

动态内存分配: 栈内存的分配是静态的,不适用于需要动态分配大量内存的场景。这种情况下,通常会使用堆(Heap)内存。

注意事项:

  • 避免在栈上分配大量内存,尤其是在递归函数中,以防止栈溢出。
  • 注意局部变量的生命周期,它们在函数返回时会被销毁。

堆(Heap)

动态内存分配的区域,由程序员手动申请和释放。在C++中,堆内存的分配通常使用new关键字,释放使用delete关键字。堆内存用于存储程序运行过程中需要动态创建和销毁的对象和数据。

特点

动态分配与释放: 堆区的内存不是在程序编译时分配的,而是在程序运行期间根据需要动态分配和释放。这为程序提供了更大的灵活性,尤其是在处理不确定数量或大小的数据结构时。

程序员控制: 开发者通过函数(如C/C++中的malloc, calloc, realloc, 和 free)手动管理堆上的内存。这意味着开发者必须负责跟踪分配的内存并在不再需要时释放,以避免内存泄漏。

非连续性: 堆区的内存分配不保证是连续的,分配的块可以分散在堆的不同位置,这与栈区连续分配的方式不同。

生命周期: 堆上的内存块的生命周期由程序员决定,从分配时刻开始,直到显式释放为止。与栈区的自动清理相比,堆区的内存管理更加灵活但也更复杂。

用途

动态数据结构: 当需要创建大小未知或在运行时变化的数据结构(如链表、树、图等)时,堆区是理想的选择。

大对象分配: 对于大型数据对象,栈区可能无法提供足够的空间,此时使用堆区进行分配更为合适。

长期存活的对象: 如果某个对象需要在整个程序执行期间存在,但又不适合放在全局存储区,可以选择在堆上分配。

管理机制

  • 分配: 使用如malloc的函数可以从堆中请求一块特定大小的内存。成功分配后,函数返回指向这块内存的指针。
  • 释放: 当不再需要堆上的内存时,应使用如free的函数将其释放回堆,以便后续重新分配。忘记释放会导致内存泄漏。
  • 碎片问题: 随着时间推移,频繁的分配和释放可能导致堆中出现碎片,即小块未使用的内存分布在已分配块之间,降低内存利用率。某些现代操作系统和编程环境提供了内存整理机制来缓解这个问题。
  • 性能影响: 动态分配和释放内存相比于栈上自动管理来说开销更大,因为它涉及到更复杂的算法和可能的系统调用。

正确管理堆区是软件开发中的一个重要方面,不当的管理不仅会导致内存泄漏、内存碎片等问题,还可能引发程序崩溃或性能下降。因此,掌握好堆区的使用是提升程序质量和性能的关键。

静态/全局存储区(Static/Global)

存储全局变量和静态变量(包括全局静态变量和局部静态变量)。这部分内存在程序整个运行期间都存在,分为初始化数据段(初始化的全局变量和静态变量,也称为data段)和未初始化数据段(未初始化的全局变量和静态变量,也称为BSS段)。

简单总结:

  • 全局/静态存储区的数据在程序启动时分配,在程序结束时释放
  • 全局变量区的数据可以被整个程序访问,而静态变量区的数据仅在声明它的函数内可见。
  • 在多线程环境中,可能需要额外的同步机制以确保安全访问。
  • 静态局部变量在函数第一次被调用时被初始化,并且之后的每次函数调用中,它们都保持之前调用结束时的值。

其他需要理解的点

关于静态局部变量

  • static用于局部变量时,它改变了变量的生命周期。通常,局部变量在函数调用结束后会被销毁,但静态局部变量则会在第一次初始化后持续整个程序的生命周期,即使函数结束调用后依然存在。
  • 它的作用域仍然限于定义它的函数内部。
  • 生命周期:程序运行期间一直存在。
  • 作用域:定义该变量的函数内部。

初始化数据段(.data段)

初始化数据段,也称作数据段或.data段,是用来存储程序中所有在编译时期就已经明确赋予了初始值的全局变量和静态变量。这里的“全局”指的是在函数外部定义的变量,而“静态”指的是使用static关键字定义的变量,无论是在函数外部还是内部。

例如,当你在程序中这样声明一个变量:

int globalVar = 10; // 全局变量,初始化为10

或者

void someFunction() {
    static int staticVar = 20; // 静态局部变量,初始化为20
}

这些变量的初始值(在这个例子中分别是10和20)会在编译时被记录下来,并且在程序运行开始时,内存中的初始化数据段就会根据这些记录分配空间并填充相应的初始值。

未初始化数据段(BSS段)

未初始化数据段,通常称为BSS段(Block Started by Symbol),是用来存储那些在程序中声明了但没有赋予明确初始值的全局变量和静态变量。这里所说的“未初始化”并不是说这些变量在程序运行时没有值,而是指在源代码中没有明确指定初始值。

例如:

int globalVarWithoutInit; // 全局变量,没有初始化

或者

void anotherFunction() {
    static int staticVarWithoutInit; // 静态局部变量,没有初始化
}

在这些情况下,编译器不会为这些变量分配实际的存储空间来保存默认值。相反,它仅仅记录这些变量所需的总字节数,并在程序加载到内存时由操作系统自动将这些区域初始化为零(或对应的零值,如0、NULL、false等)。

总结

  • 初始化数据段包含了具体的值,这些值在编译时已经确定,并在程序加载时复制到内存中。
  • BSS段则仅记录了所需的空间大小,没有实际的值,程序启动时由系统自动清零。

这两个段都是程序静态存储区的一部分,意味着它们在程序执行期间一直存在,且其内存地址在程序加载时就已经确定,这与动态分配的内存(如堆上的内存)是不同的。正确理解和利用这两个段,可以帮助优化程序的内存使用和提高程序的效率。

其实下面两者可以统分为代码区,一个叫常量数据段,一个叫代码段

常量区(Constant)

用于存放程序中定义的常量,这些常量在程序执行期间不会改变。

非静态局部常量,保存在栈区,会随着其所在的作用域作用完而消除掉。

程序中定义的只读数据,如字符串常量、const声明的常量等。这些数据在程序执行过程中不能被修改。

它存放字符串常量、const修饰的全局变量等不可修改的数据。实际上,这部分内容经常被看作是代码区的一部分,特别是当讨论内存四区时,常量数据通常与只读代码一起提及,而不是单独列为一个区域。但理论上,从数据的性质来区分,可以认为它是独立的,用来专门存放常量数据。

代码区(Code)

用于存储程序的可执行指令

作用:
  1. 代码区存储了程序的机器指令,这些指令是程序的主要逻辑,由编译器从源代码生成。
特点:

共享性: 在操作系统中,同一程序的多个实例可以共享代码区,因为这部分数据在程序执行过程中不会被修改。

只读性: 代码区是只读的,这意味着程序在执行过程中不能修改这里的指令和数据。这是为了保护程序不被意外或恶意修改。

固定性: 代码区的位置在程序加载到内存后通常是固定的。操作系统负责将代码区映射到内存中,并确保它不会被其他程序覆盖。

使用:

程序执行: 当程序运行时,CPU从代码区读取指令并执行。这些指令构成了程序的执行逻辑。

注意事项:
  • 代码区的数据是只读的,尝试修改可能会导致程序崩溃或安全风险。
  • 在多线程环境中,代码区的共享特性需要特别注意,以避免竞态条件

总结

程序加载到内存的过程涉及多个组成部分,具体哪些部分在程序启动时加载,哪些在运行过程中按需加载,可以概述如下:

程序加载时加载到内存中的部分:

1.代码段(Text Segment):包含程序的机器指令,是程序执行的实际代码。这部分在程序启动时被加载到内存的代码区。

2.初始化数据段(Initialized Data Segment):包括全局变量和静态变量的初始化值,以及那些在程序开始前就需要设定的常量数据。这些数据会在程序加载时放入内存的数据段。

3.只读数据段:存储字符串常量、const关键字声明的全局变量等不可修改的数据。这部分同样在程序启动时加载到内存的只读区域。

4.未初始化数据段(BSS Segment):虽然未初始化数据段在磁盘上的可执行文件中不占用实际空间(因为它默认为0或空),但在程序加载到内存时,会为这部分分配空间并清零。

程序运行过程中按需加载到内存中的部分:

1.动态链接库(Dynamic Link Libraries/Dynamic Shared Objects):如果程序依赖外部库,这些库可能不会随着主程序一起预先加载,而是在程序运行到需要它们的时候动态加载。

2.堆(Heap):程序运行时通过malloc、new等操作动态分配的内存空间。这部分内存不是在程序启动时一次性分配好的,而是根据程序运行时的需求动态分配和回收。

3.栈(Stack):用于存储函数调用时的局部变量、函数参数和返回地址等。栈空间也是动态分配的,随函数调用和返回而增长和收缩。

4.延迟加载的模块或数据:一些大型程序可能会设计成模块化,其中某些模块或数据仅在特定功能被请求时才加载到内存中,以减少启动时间和内存占用。

综上所述,程序的基本结构如代码、初始化数据会在启动时加载,而动态分配的内存、动态链接库及按需加载的模块则在程序运行过程中根据需要加载。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值