C/C++中编译程序的内存结构分布
内存分配方式简介
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
1、栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
2、堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3、自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4、全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5、常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。关键字时const
注意:
1.在所有函数体外定义的是全局变量
2.加了static修饰符后不管在哪里都存放在全局区(静态区)
3.在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用
4.在函数体内定义的static表示只在该函数体内有效
5.函数中的常量字符串存放在常量区
为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内存布局对调试程序非常有帮助,可以知道程序执行时,到底做了什么,有助于写出干净的代码。
源文件转换为可执行文件
源文件经过以下几步生成可执行文件:
1、预处理(preprocessor):对#include、#define、#ifdef/#endif、#ifndef/#endif等进行处理
2、编译(compiler):将源码编译为汇编代码
3、汇编(assembler):将汇编代码汇编为目标代码
4、链接(linker):将目标代码链接为可执行文件
编译器和汇编器创建的目标文件包含:二进制代码(指令)、源码中的数据;链接器将多个目标文件链接成一个;装载器吧目标文件加载到内存。
可执行程序组成及内存布局
通过上面的小节,我们知道将源程序转换为可执行程序的步骤,典型的可执行文件分为两部分:
代码段(Code),由机器指令组成,该部分是不可改的,编译之后就不再改变,放置在文本段(.text)。
数据段(Data),它由以下几部分组:
常量(constant),通常放置在只读read-only的文本段(.text)
静态数据(static data),初始化的放置在数据段(.data);未初始化的放置在(.bss,Block Started by Symbol,BSS段的变量只有名称和大小却没有值)
动态数据(dynamic data),这些数据存储在堆(heap)或栈(stack)
源程序编译后链接到一个以0地址为始地址的线性或多维虚拟地址空间。而且每个进程都拥有这样一个空间,每个指令和数据都在这个虚拟地址空间拥有确定的地址,把这个地址称为虚拟地址(Virtual Address)。将进程中的目标代码、数据等的虚拟地址组成的虚拟空间称为虚拟存储器(Virtual Memory)。典型的虚拟存储器中有类似的布局:
当进程被创建时,内核为其提供一块物理内存,将虚拟内存映射到物理内存,这些都是由操作系统来做的。
数据存储类别
讨论C/C++中的内存布局,不得不提的是数据的存储类别!数据在内存中的位置取决于它的存储类别。一个对象是内存的一个位置,解析这个对象依赖于两个属性:存储类别、数据类型。
存储类别决定对象在内存中的生命周期。
数据类型决定对象值的意义,在内存中占多大空间。
C/C++中由(auto、 extern、 register、 static)存储类别和对象声明的上下文决定它的存储类别。
1、自动对象(automatic objects)
auto和register将声明的对象指定为自动存储类别。他们的作用域是局部的,诸如一个函数内,一个代码块{***}内等。操作了作用域,对象会被销毁。
在一个代码块中声明一个对象,如果没有执行auto,那么默认是自动存储类别。
声明为register的对象是自动存储类别,存储在计算机的快速寄存器中。不可以对register对象做取值操作“&”。
2、静态对象(static objects)
静态对象可以局部的,也可以是全局的。静态对象一直保持它的值,例如进入一个函数,函数中的静态对象仍保持上次调用时的值。包含静态对象的函数不是线程安全的、不可重入的,正是因为它具有“记忆”功能。
局部对象声明为静态之后,将改变它在内存中保存的位置,由动态数据--->静态数据,即从堆或栈变为数据段或bbs段。
全局对象声明为静态之后,而不会改变它在内存中保存的位置,仍然是在数据段或bbs段。但是static将改变它的作用域,即该对象仅在本源文件有效。此相反的关键字是extern,使用extern修饰或者什么都不带的全局对象的作用域是整个程序。
C/C++中由源程序到可执行文件的步骤,和可执行程序的内存布局,数据存储类别,最后还通过一个例子来说明。可执行程序中的变量在内存中的布局可以总结为如下:
变量(函数外):如果未初始化,则存放在BSS段;否则存放在data段
变量(函数内):如果没有指定static修饰符,则存放在栈中;否则同上
常量:存放在文本段.text
函数参数:存放在栈或寄存器中
内存可以分为以下几段:
文本段:包含实际要执行的代码(机器指令)和常量。它通常是共享的,多个实例之间共享文本段。文本段是不可修改的。
初始化数据段:包含程序已经初始化的全局变量,.data。
未初始化数据段:包含程序未初始化的全局变量,.bbs。该段中的变量在执行之前初始化为0或NULL。
栈:由系统管理,由高地址向低地址扩展。
堆:动态内存,由用户管理。通过malloc/alloc/realloc、new/new[]申请空间,通过free、delete/delete[]释放所申请的
空间。由低地址想高地址扩展
堆和栈的比较
申请方式
stack: 由系统自动分配。
heap: 需要程序员自己申请,并指明大小,在 C 中 用malloc 函数, C++ 中是 new 运算符。
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete 语句才能正确的释放本内存空间。
由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。意思是栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是 2M (也有的说是 1M ,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的 。
堆是由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片 , 不过用起来方便 。
堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
存取效率的比较 参考:https://blog.csdn.net/baidu_37964071/article/details/81428139
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、常量存储区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
————————————————
//main.cpp
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区
const int b = 10; // 变量将在常量存储区,程序执行的全程不能修改
char * p2 = new char[9]; // 这个是声明的一个动态变量,将存放在动态存储区,在程序运行时分配内存
// get dynamic memory:
int main()
{
// 在这个代码块里定义的变量都是定义在了栈上,随着程序的结束这里的变量将被全部释放
int b; // 栈
char s[] = "abc"; // 栈
char *p2; // 栈
char *p3 = "123456"; // 123456\0在常量区,p3在栈上。
static int c =0; // static关键字在哪里都可以让一个变量成为全局(静态)变量,初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
// 分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); // 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
return 0;
}
————————————————
编译其实只是一个扫描过程,进行词法语法检查,代码优化而已,编译程序越好,程序运行的时候越高效。
我想你说的“编译时分配内存”是指“编译时赋初值”,它只是形成一个文本,检查无错误,并没有分配内存空间。类似一个占位符的检查,你在的位置对吗,这个位置该你占位嘛。
当你运行时,系统才把程序导入内存。一个进程(即运行中的程序)在主要包括以下五个分区:
栈、堆、bss、data、code
代码(编译后的二进制代码)放在code区,代码中生成的各种变量、常量按不同类型分别存放在其它四个区。系统依照代码顺序执行,然后依照代码方案改变或调用数据,这就是一个程序的运行过程。
运行时程序是必须调到“内存”的。因为CPU(其中有多个寄存器)只与内存打交道的。程序在进入实际内存之前要首先分配物理内存。
编译
编译器能够识别语法,数据类型等等。然后逐行逐句检查编译成二进制数据的obj文件,然后再由链接程序将其链接成一个EXE文件。此时的程序是以EXE文件的形式存放在磁盘上。
运行
当执行这个EXE文件以后,此程序就被加载到内存中,成为进程。此时一开始程序会初始化一些全局对象,然后找到入口函数(main()或者WinMain()),就开始按程序的执行语句开始执行。此时需要的内存只能在程序的堆上进行动态增加/释放了。
参考:https://blog.csdn.net/qq_42570601/article/details/107598826
https://blog.csdn.net/ct2008112101/article/details/38680829
1.栈(stack):1.由编译器自动分配释放,存放函数的参数值,局部变量。函数被调用时用来传递参数和返回值
2.其他操作类似于数据结构中的栈
2.堆(heap):存放动态分布的内存段当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆 上(堆 被 被扩张)/释放的内存从堆中被提出(堆被缩减)。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3.全局数据区:全局变量和静态变量是放在一块,初始化后的全局变量和静态变量在一块区域,没有初始化的全局变量和静态变量在一块区域。文字常量和常量字符串在这块区域,程序结束后被系统释放
(1)BSS段:存放未初始化的全局变量
(2) 数据段:已初始化的全局变量
4.代码段 :存放程序执行代码的一块区域。程序段为程序代码在内存中的映射,一个程序可以在内存中有多个副本
注意:
- 在所有函数体外定义的是全局变量
- 加了static修饰符后不管在哪里都存放在全局区(静态区)
- 在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用
- 在函数体内定义的static表示只在该函数体内有效
- 函数中的常量字符串存放在常量区