数据段、代码段、堆栈段、BSS段的区别进程(执⾏的程序)会占⽤⼀定数量的内存,它或是⽤来存放从磁盘载⼊的程序代码,或是存放取⾃⽤户输⼊的数据等等。不过进程对这些内存的管理⽅式因内存⽤途不⼀⽽不尽相同,有些内存是事先静Linux进程的五个段
下⾯我们来简单归纳⼀下进程对应的内存空间中所包含的5种不同的数据区都是⼲什么的。
BSS段:BSS段(bss segment)通常是指⽤来存放程序中未初始化的全局变量的⼀块内存区域。BSS是英⽂Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(data segment)通常是指⽤来存放程序中已初始化的全局变量的⼀块内存区域。数据段属于静态内存分配。
代码段:代码段(code segment/text segment)通常是指⽤来存放程序执⾏代码的⼀块内存区域。这部分区域的⼤⼩在程序运⾏前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含⼀些只读的常数变量,例如字符串常量等。
堆(heap):堆是⽤于存放进程运⾏中被动态分配的内存段,它的⼤⼩并不固定,可动态扩张或缩减。当进程调⽤malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利⽤free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack):栈⼜称堆栈,是⽤户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调⽤时,其参数也会被压⼊发起调⽤的进程栈中,并且待到调⽤结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别⽅便⽤来保存/恢复调⽤现场。从这个意义上讲,我们可以把堆栈看成⼀个寄存、交换临时数据的内存区。
它是由操作系统分配的,内存的申请与回收都由OS管理。
PS:
全局的未初始化变量存在于.bss段中,具体体现为⼀个占位符;全局的已初始化变量存于.data段中;⽽函数内的⾃动变量都在栈上分配空间。.bss是不占⽤.exe⽂件空间的,其内容由操作系统初始化(清零);⽽.data却需要占⽤,其内容由程序初始化,因此造成了上述情况。
bss段(未⼿动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的⼤⼩。
data(已⼿动初始化的数据)段则为数据分配空间,数据保存在⽬标⽂件中。数据段包含经过初始化的全局变量以及它们的值。BSS段的⼤⼩从可执⾏⽂件中得到,然后链接器得到这个⼤⼩的内存块,紧跟在数据段后⾯。当这个内存区进⼊程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。
⼀. 在c中分为这⼏个存储区
1.栈 - 由编译器⾃动分配释放
2.堆 - ⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
3.全局区(静态区),全局变量和静态变量的存储是放在⼀块的,初始化的全局变量和静态变量在⼀块区域,未初始化的全局变量和未初始化的静态变量在相邻的另⼀块区域。- 程序结束释放
4.另外还有⼀个专门放常量的地⽅。- 程序结束释放
在函数体中定义的变量通常是在栈上,⽤malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。在所有函数体外定义的是全局量,加了static修饰符后不管在哪⾥都存放在全局区(静态区),在所有函数体外定义的static变量表⽰在该⽂件中有效,不能extern到别的⽂件⽤,在函数体内定义的static表⽰只在该函数体内有效。另外,函数中的"adgfdf"这样的字符串存放在常量区。⽐如:
int a = 0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //t}在常量区,p3在栈上
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10); //分配得来得10字节的区域在堆区
p2 = (char *)malloc(20); //分配得来得20字节的区域在堆区
strcpy(p1, "123456");
//t}放在常量区,编译器可能会将它与p3所指向的"123456"优化成⼀块
}
⼆.在C++中,内存分成5个区,他们分别是堆、栈、⾃由存储区、全局/静态存储区和常量存储区
1.栈,就是那些由编译器在需要的时候分配,在不需要的时候⾃动清楚的变量的存储区。⾥⾯的变量通常是局部变量、函数参数等。
2.堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应⽤程序去控制,⼀般⼀个new就要对应⼀个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会⾃动回收。
3.⾃由存储区,就是那些由malloc等分配的内存块,他和堆是⼗分相似的,不过它是⽤free来结束⾃⼰的⽣命的。
4.全局/静态存储区,全局变量和静态变量被分配到同⼀块内存中,在以前的C语⾔中,全局变量⼜分为初始化的和未初始化的,在C++⾥⾯没有这个区分了,他们共同占⽤同⼀块内存区。
5.常量存储区,这是⼀块⽐较特殊的存储区,他们⾥⾯存放的是常量,不允许修改(当然,你要通过⾮正当⼿段也可以修改)
三. 谈谈堆与栈的关系与区别
具体地说,现代计算机(串⾏执⾏机制),都直接在代码底层⽀持栈的数据结构。这体现在,有专门的寄
存器指向栈所在的地址,有专门的机器指令完成数据⼊栈出栈的操作。这种机制的特点是效率⾼,⽀持的数据有限,⼀般是整数,指针,浮点数等系统直接⽀持的数据类型,并不直接⽀持其他的数据结构。因为栈的这种特点,对栈的使⽤在程序中是⾮常频繁的。对⼦程序的调⽤就是直接利⽤栈完成的。机器的call指令⾥隐含了把返回地址推⼊栈,然后跳转⾄⼦程序地址的操作,⽽⼦程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的⾃动变量是直接利⽤栈的例⼦,这也就是为什么当函数返回时,该函数的⾃动变量⾃动失效的原因。
和栈不同,堆的数据结构并不是由系统(⽆论是机器系统还是操作系统)⽀持的,⽽是由函数库提供的。基本的malloc/realloc/free 函数维护了⼀套内部的堆数据结构。当程序使⽤这些函数去获得新的内存空间时,这套函数⾸先试图从内部堆中寻可⽤的内存空间,如果没有可以使⽤的内存空间,则试图利⽤系统调⽤来动态增加程序数据段的内存⼤⼩,新分配得到的空间⾸先被组织进内部堆中去,然后再以适当的形式返回给调⽤者。当程序释放分配的内存空间时,这⽚内存空间被返回内部堆结构中,可能会被适当的处理(⽐如和其他空闲空间合并成更⼤的空闲空间),以更适合下⼀次内存分配申请。这套复杂的分配机制实际上相当于⼀个内存分配的缓冲池(Cache),使⽤这套机制有如下若⼲原因:
1. 系统调⽤可能不⽀持任意⼤⼩的内存分配。有些系统的系统调⽤只⽀持固定⼤⼩及其倍数的内存请求(按页分配);这样的话对于⼤量的⼩内存分类来说会造成浪费。
2. 系统调⽤申请内存可能是代价昂贵的。系统调⽤可能涉及⽤户态和核⼼态的转换。
3. 没有管理的内存分配在⼤量复杂内存的分配释放操作下很容易造成内存碎⽚。
动态清零是什么意思堆和栈的对⽐
从以上知识可知,栈是系统提供的功能,特点是快速⾼效,缺点是有限制,数据不灵活;⽽栈是函数库提供的功能,特点是灵活⽅便,数据适应⾯⼴泛,但是效率有⼀定降低。栈是系统数据结构,对于进程/线程是唯⼀的;堆是函数库内部数据结构,不⼀定唯⼀。不同堆分配的内存⽆法互相操作。栈空间分静态分配和动态分配两种。静态分配是编译器完成的,⽐如⾃动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配⽆需释放(是⾃动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被⿎励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/ 释放内存匹配是良好程序的基本要素。
1.碎⽚问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从⽽造成⼤量的碎⽚,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的⼀⼀对应,以⾄于永远都不可能有⼀个内存块从栈中间弹出,在他弹出之前,在他上⾯的后进的栈内容已经被弹出,详细的可以>参考数据结构,这⾥我们就不再⼀⼀讨论了。
2.⽣长⽅向:对于堆来讲,⽣长⽅向是向上的,也就是向着内存地址增加的⽅向;对于栈来讲,它的⽣长⽅向是向
下的,是向着内存地址减⼩的⽅向增长。
3.分配⽅式:堆都是动态分配的,没有静态分配的堆。栈有2种分配⽅式:静态分配和动态分配。静态分配是编译器完成的,⽐如局部变量的分配。动态分配由alloca函数进⾏分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进⾏释放,⽆需我们⼿⼯实现。
4.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供⽀持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执⾏,这就决定了栈的效率⽐较⾼。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配⼀块内存,库函数会按照⼀定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可⽤的⾜够⼤⼩的空间,如果没有⾜够⼤⼩的空间(可能是由于内存碎⽚太多),就有可能调⽤系统功能去增加程序数据段的内存空间,这样就有机会分到⾜够⼤⼩的内存,然后进⾏返回。显然,堆的效率⽐栈要低得多。
明确区分堆与栈:
在bbs上,堆与栈的区分问题,似乎是⼀个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第⼀个开⼑。
⾸先,我们举⼀个例⼦:
void f()
{
int* p=new int[5];
}
这条短短的⼀句话就包含了堆与栈,看到new,我们⾸先就应该想到,我们分配了⼀块堆内存,那么指针p呢?他分配的是⼀块栈内存,所以这句话的意思就是:在栈内存中存放了⼀个指向⼀块堆内存的指针p。在程序会先确定在堆中分配内存的⼤⼩,然后调⽤operator new分配内存,然后返回这块内存的⾸地址,放⼊栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
这⾥,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是⼀个数组,VC6就会根据相应的Cookie信息去进⾏释放内存的⼯作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下⼏点:
1、管理⽅式不同;
2、空间⼤⼩不同;
3、能否产⽣碎⽚不同;
4、⽣长⽅向不同;
5、分配⽅式不同;
6、分配效率不同;
管理⽅式:对于栈来讲,是由编译器⾃动管理,⽆需我们⼿⼯控制;对于堆来说,释放⼯作由程序员控制,容易产⽣memory leak。
空间⼤⼩:⼀般来讲在32位系统下,堆内存可以达到4G的空间,从这个⾓度来看堆内存⼏乎是没有什么限制的。但是对于栈来讲,⼀般都是有⼀定的空间⼤⼩的,例如,在VC6下⾯,默认的栈空间⼤⼩是1M(好像是,记不清楚了)。当然,我们可以修改:
打开⼯程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最⼤值和commit。
注意:reserve最⼩值为4Byte;commit是保留在虚拟内存的页⽂件⾥⾯,它设置的较⼤会使栈开辟较⼤的值,可能增加内存的开销和启动时间。
堆和栈相⽐,由于⼤量new/delete的使⽤,容易造成⼤量的内存碎⽚;由于没有专门的系统⽀持,效率很低;由于可能引发⽤户态和核⼼态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应⽤最⼴泛的,就算是函数的调⽤也利⽤栈去完成,函数调⽤过程中的参数,返回地址,EBP和局部变量都采⽤栈的⽅式存放。所以,我们推荐⼤家尽量⽤栈,⽽不是⽤堆。
另外对存取效率的⽐较:
代码:
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运⾏时刻赋值的;
⽽bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组⽐指针所指向的字符串(例如堆)快。
⽐如:
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第⼀种在读取时直接就把字符串中的元素读到寄存器cl中,⽽第⼆种则要先把指针值读到edx中,在根据edx读取字符,显然慢了.
⽆论是堆还是栈,都要防⽌越界现象的发⽣(除⾮你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产⽣以想不到的结果,就算是在你的程序运⾏过程中,没有发⽣上⾯的问题,你还是要⼩⼼,说不定什么时候就崩掉,编写稳定安全的代码才是最重要的
发布评论