1、内存结构
-
C/C++语言一只被认为是一种底层语言,与其他语言不一样,对内存结构理解是C/C++程序员从入门到入土的开端。
-
其他编程语言对内存管理是透明的,程序员无序关心可以认为是一个黑盒;而C/C++不一样理解好内存结构有利于编写健壮性的代码
-
C++的内存结构主要涉及以下区域
- 代码区:存储程序的机器码指令,包括执行程序和只读数据:全局常量、const修饰的变量、字符串常量
- 全局/静态存储区:存储全局变量和静态变量,其生命周期贯穿整个程序执行过程的变量!
- 堆区:用于动态分配内存,存储在堆上的数据的生命周期由程序员自行管理(地址由低到高)
- 内存映射区:mmap共享映射区,主要包括动态库.dll/.so、文件映射、匿名映射
- 栈区:用于存储函数调用信息、局部变量、临时数据等,遵循后进先出的原则(地址由高到低)
2、内存各区介绍
- 代码段可制度数据段通常在程序加载时有操作系统加载到内存,一旦加载就不能被修改
- 在函数调用时,函数的机器码也存储在代码段中,每个函数有其独特的代码段地址
- 字符串常量等只读数据段中的数据是不可修改的,任何企图修改这些数据的尝试都会导致运行时错误
2.1、代码区
- 在C++程序中,代码区是存储程序执行代码的一部分内存区域。它通常被划分为两个主要部分:代码段(.text)和只读数据段(.rodata)
- 这里说的代码区是指已经运行并且加载到内存中的可执行的二进制指令,并不是存储在磁盘上的源代码文件
2.1.1、代码段(.text)
- 结构:代码段存储程序的可执行指令,即机器码。这是程序中实习执行的代码部分。
- 使用场景:包括程序的函数、方法、控制流等。这部分内存是只读的,程序在运行时不能修改代码段的内容
2.1.2、只读数据段(.rodata)
- 只读数据段:rodata是read-only data的缩写
- 结构:只读数据段存储常量数据,例如字符串常量,以及全局或静态变量的初始化值。
- 使用场景:用于存储不可修改的数据。字符串字面量是一个常见的只读数据段的例子
2.1、小结
代码区有两个很重要的特性:
- 只读(read only):代码区的东西都是只读的,这意味着程序在运行时这部分的内容不被修改,有助于保证程序执行区间的数据的一致性和安全性
- 可复用性(Sharable):代码区的内容通常是共享的,有趋势对于相同的程序的多个实例或同时运行起来的多个程序来说,多个程序实例可以共享相同的机器码,有助于节省内存
这些特点使得代码区能够更有效地支持多个程序的并发执行,并在运行时提供一定程序的保护,确保代码和只读数据的完整性。
2.2、全局/静态存储区
全局/静态存储区是程序中用于存储全局变量和静态变量的内存区域。这些变量在程序的整个声明周期内存在,并且其内存分配发生在程序启动时,知道程序结束。全局/静态存储区包括两个主要部分:全局变量区和静态变量区。
- .data段:
- 已初始化的全局变量、静态变量存放在.data段。
- .data段占用可执行文件空间,其内容有程序初始化。
- .bss段:
- 未初始化的全局变量、静态变量存放在.bss段。
- 初始化为0的全局变量、静态变量存放在.bss段。
- .bss段不占用可执行文件空间,其内容由操作系统初始化。
- 注意事项:
- 全局、静态存储区的数据在程序启动时分配,在程序结束时释放
- 全局变量区的数据可以被整个程序访问,而静态变量区的数据访问权限与其定义的位置有关。
- 多线程访问,全局变量和静态变量可能需要额外的同步/互斥机制,以确保多个线程对它们的安全访问。
2.3、堆区
- 堆区是程序运行时用于动态分配内存的一种内存区域,也称为自由存储区。
- 堆上的内存可以在运行时动态分配和释放,由程序员自行负责管理其生命周期
- 使用场景
- 堆是有操作系统分配的一块较大的内存区域,可以分配出较大的一块虚拟内存连续的地址
- 动态内存分配:当程序无法确定需要多少内存时或者需要在程序的不同部分共享数据时,使用堆上的内存非常有用
- 对象的动态创建和销毁:使用new 和 malloc操作符分配的内存,使用delete和free操作释放相应的内存。
2.4、栈区
- 使用场景:
- 存储函数的局部变量:酶促函数调用时,其局部变量被分配到栈上,函数返回时将这些变量自动释放
- 存储函数的调用信息:每次函数调用时,函数的地址和一些其他信息被压入栈中,函数返回时再从栈中弹出这些信息
- 栈帧:
- 在函数调用时,一个栈帧(Stack Frame)被压入栈中。
- 栈帧包含了函数的局部变量、返回值地址和其他与函数调用相关的信息。
- 栈帧主要是通过寄存器地址偏移来实现的。
- 栈的管理通常有编译器负责。编译器根据程序的结构和函数调用关系来分配和管理栈空间。在编译阶段,编译器会生成一些代码来处理栈的操作,包括栈帧的创建和销毁,局部变量的分配和释放,以及函数调用时的相关操作
- 分析函数调用关系:编译器需要了解程序中函数的调用关系,以便正确生成栈帧和处理函数调用时的参数传递和返回值
- 分配栈空间:对于每个函数,编译器需要决定分配多少空间用于栈帧,以容纳局部变量、函数操作、返回地址等
- 生成栈操作指令:编译器会生成相同的汇编或机器码指令,用于执行栈的压栈和出栈操作,以及处理函数调用时的栈操作
高级语言中一般不需要管理栈帧的操作,在低级(汇编)语言中,程序员有更多的控制权,可以直接操作栈,高级语言中这种底层的栈帧操作通常有编译器自动处理。
2.5、内存映射区
- 这个区域很灵活主要负责:
- 动态库:windows下的.dll库、Linux下的.so库的加载与库调用
- 共享内存映射、文件映射的处理
- malloc分配超过128k也会进入内存映射区进行分配空间
- 其分配方向不同:32位和64位分配的方向相反
2.6、内核空间
-
所有程序共享的一个空间
-
用户代码不能读写的一段地址
3、总结
-
作为一个专业的C++使用者来说,清楚的知道自己的代码变量存储的区域会有非常大的好处,补单能够写出高性能代码,而且有助于减少一些深层次的BUG。
-
使用C++内存的一些注意事项
-
内存泄漏:确保在动态分配内存后找个合适的时机释放掉,避免出现内存泄漏
-
野指针:注意在指针使用后及时置为nullptr,避免访问已经释放的内存
-
栈溢出:谨慎使用递归或者在栈区使用巨大的空间分配局部变量,以免造成栈溢出
-
悬挂指针:避免悬挂指针的问题,即指向已经释放的内存区域
-
智能指针:考虑使用C++的智能指针(std::unique_ptr、std::shared_ptr),提高内存管理的安全性和便利性。
-
局部变量生命周期:理解局部变量的生命周期,确保在离开其作用域前不在访问。
-