首先,堆栈都是内存中两块区域。
想要了解堆栈,那么先了解一下程序内存的分类。
1、栈区(stack sagment):由编译器自动分配释放,存放函数的参数的值,局部变量的值等。在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是1M,如果申请的空间超过栈的剩余空间时,将提示stack overflow。因此,能从栈获得的空间较小。
2、堆区(heap sagment) : 一般由程序员分配释放,若程序员不释放,程序结束时可能由系统回收 。它与数据结构中的堆是两回事。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3、全局区(静态区)(data sagment):全局变量和静态变量的存储区域是在一起的,程序结束后由系统释放。数据区的大小由系统限定,一般很大,Windows32位操作系统下可以达到2GB,因此不会溢出。32位操作系统的地址空间为4G,但是留给程序的只是2G,因为另外的2G留给操作系统自用。Windows Server 2003可以支持的全局变量空间达到3G。
4、文字常量区:常量字符串就是放在这里的, 程序结束后由系统释放。
5、程序代码区:存放函数体的二进制代码。
综上所述,局部变量空间是很小的,我们开一个a[1000000]就会导致栈溢出;而全局变量空间一般比较大,因此大小超过1M的变量尽量声明为全局变量或者静态变量。
1、栈区:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数,函数调用后返回的地址。(为运行函数而分配的局部变量、函数参数、函数调用后返回地址等存放在栈区)。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
2、堆区:内存使用new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。
3、自由存储区:使用malloc进行分配,使用free进行回收。
4、全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了。(全局变量、静态数据 存放在全局数据区)
5、常量存储区:存储常量,不允许被修改。
1、静态(全局)存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。也是程序结束后,由操作系统释放。
2、栈区:在执行函数时,函数参数,局部变量(包括const局部变量),函数调用后返回的地址都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或 delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
1、栈又叫堆栈,非静态局部变量/函数参数/返回值等等 ,还有每次调用函数时保存的信息。每当调用一个函数时,返回到的地址和关于调用者环境的某些信息的地址,比如一些机器寄存器,就会被保存在栈中。然后,新调用的函数在栈上分配空间,用于自动和临时变量。
2、内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3、堆用于程序运行时动态内存分配,堆是可以上增长的。堆区域从BSS段的末尾开始,并从那里逐渐增加到更大的地址。堆是由程序员自己分配的。堆区域由所有共享库和进程中动态加载的模块共享。
4、数据段分为初始化数据段和未初始化数据段。初始化的数据段,通常称为数据段,是程序的虚拟地址空间的一部分,它包含有程序员初始化的全局变量和静态变量,可以进一步划分为只读区域和读写区域。未初始化的数据段,通常称为bss段,这个段的数据在程序开始之前有内核初始化为0,包含所有初始化为0和没有显示初始化的全局变量和静态变量。
5、代码段也叫文本段,是对象文件或内存中程序的一部分,其中包含可执行代码和只读常量。文本段在堆栈的下面,是防止堆栈溢出覆盖它。,通常代码段是共享的,对于经常执行的程序,只有一个副本需要存储在内存中,代码段是只读的,以防止程序以外修改指令。
1、代码区:主要存储程序代码指令,define定义的常量。
2、全局数据区:主要存储全局变量(常量),静态变量(常量),常量字符串。
3、栈区:主要存储局部变量,栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但内存大小有限。
4、堆区:由malloc,calloc分配的内存区域,其生命周期由free决定。堆的内存大小是由程序员分配的,理论上可以占据系统中的所有内存。
(1)申请大小的限制
在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
(2)栈的申请效率很高
栈由系统自动分配,速度较快。但程序员是无法控制的,结束后由操作系统进行释放。
(3)栈使用的过程
- 栈在函数调用时:
- 第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,
- 然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,
- 然后是函数中的局部变量。注意静态变量是不入栈的。
- 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
“栈”由程序自动向操作系统申请分配以及回收,速度快,使用方便,但程序员无法控制。但是栈的空间很小,只要栈的剩余空间大于所申请空间,系统将为程序提供内存,若需要分配的空间大于栈内存,则分配失败,则提示栈溢出错误。
#include
int main()
{
int i = 10; //变量i储存在栈区中
const int i2 = 20; //const局部变量也存储在stack
int i3 = 30;
std::cout << &i << " " << &i2 << " " << &i3 << std::endl;
return 0;
}
/*运行结果为:
0x28fedc 0x28fed8 0x28fed4 16进制地址,递减的
*/
注意:const局部变量也储存在栈区内,栈区向地址减小的方向增长。
函数调用时通过一个指向函数的指针指向函数,函数返回时将回归到调用处,那个地方就是函数调用结束后返回地址。另外返回地址保存在栈上,最先调用的函数最早入栈,最后出栈,而最后调用的函数最后入栈,最先出栈。
操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
(1)申请大小的限制
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的物理内存。由此可见,堆获得的空间比较灵活,也比较大。
(2)堆的使用效率
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
(3)堆使用的过程
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统使用链表存储空闲内存地址的,自然是不连续的。而链表的遍历方式是由低地址向高地址,堆的大小受限于计算机系统中有效的内存,由此可见,堆获得的空间比较灵活,也比较大。
程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢,地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露。
项目 | 申请方式 | 内存大小 | 使用效率 | 是否连续 | 地址方向 | 存储方式 |
---|---|---|---|---|---|---|
栈(stack) | 自动申请释放 | 小 | 高效 | 连续 | 由高地址向低地址 | 先进后出 |
堆(heap) | 手动申请释放 | 大 | 缓慢 | 不连续 | 由低地址向高地址 | 先进先出 |
从上面的分析可以知道,由于函数的参数存储在栈内存中,所以如果一个数组比较大,我传递一个很大的数组,如果复制的是数组的值到形参上面,必然会导致栈内存不够用,即“栈溢出”,所以只传递一个地址值,而不传递实际的值就可以避免这一问题,其实向java,C#这些语言也有着相同的底层原理,后面继续说明。
总结:
我们在C语言里面函数的参数传递经常分为,值传递/指针传递
在C#,Java,C++里面函数的参数传递我们经常分为,值传递/引用传递
其实从内存的“栈内存”角度来说,所有的函数参数传递都只有一种形式,那就是值传递。
因为如果参数是值,则会拷贝栈里面的值到函数的形参里面去,这当然是值传递了,
如果参数是地址或者是引用,其实同样是会拷贝存在在栈区域里面的地址或者是引用传递到函数形参,只不过这个地址或者是引用不是真正的数据,拷贝的那个相同的地址或者谁引用会指向同一段数据,所以我们称之为传递引用或者是地址。