知道c++中的变量存储方式,但是最近在思考一个问题,c++中的函数是怎样存储的,所以在网上找了一些资料,做了个总结,本文主要谈一谈C++程序内存的各个分配的内存区域以及各个内存区域之间的区别。
一、 内存的基本构成
C++编译器将计算机内存分为代码区和数据区,很显然,代码区就是存放程序代码,而数据区则是存放程序编译和执行过程出现的变量和常量。数据区又分为静态数据区、动态数据区,动态数据区包括堆区和栈区。
1.1 以下是各个区的作用:
(1)代码区:存放程序代码;
(2)数据区
a.静态数据区: 在编译器进行编译的时候就为该变量分配的内存,存放在这个区的数据在程序全部执行结束后系统自动释放,生命周期贯穿于整个程序执行过程。它主要存放静态数据、全局数据和常量。
b.动态数据区:包括堆区和栈区
·堆区:亦称动态内存分配。这部分存储空间完全由程序员自己负责管理,它的分配和释放都由程序员自己负责。这个区是唯一一个可以由程序员自己决定变量生存期的区间。可以用malloc,new申请对内存,并通过free和delete释放空间。如果程序员自己在堆区申请了空间,又忘记将这片内存释放掉,就会造成内存泄露的问题,导致后面一直无法访问这片存储区域。但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
·栈区:存放函数的形式参数和局部变量,由编译器分配和自动释放,函数执行完后,局部变量和形参占用的空间会自动被释放。栈内存分配运算内置于处理器的指令集中,效率比较高,但是分配的容量很有限。
1.2 注意:
1)全局变量以及静态变量存放在静态数据区;
2) 注意常量的存放区域,通常情况下,常量存放在程序区(程序区是只读的,因此任何修改常量的行为都是非法的),而不是数据区。有的系统,也将部分常量分配到静态数据区,比如字符串常量(有的系统也将其分配在程序区)。但是要记住一点,常量所在的内存空间都是受系统保护的,不能修改。对常量空间的修改将造成访问内存出错,一般系统都会提示。常量的生命周期一直到程序执行结束为止。
二、一些变量内存分配的例子:
- <span style="font-family:Microsoft YaHei;"></span><pre class="cpp" name="code">int a=1; // a在栈区
- char s[]="123"; // s在栈区,“123”在栈区,其值可以被修改
- char *s="123"; // s在栈区,“123”在常量区,其值不能被修改,s中保存的是常量去的内存地址
- int *p=new int; // p在栈区,申请的空间在堆区(p指向的区域)
- int *p=(int *)malloc(sizeof(int)); // p在栈区,p指向的空间在堆区
- static int b=0; // b在静态区
- </pre>
- <pre></pre>
- <p><span style="font-family:Microsoft YaHei; color:#990000; background-color:rgb(255,255,255)"><strong>三、静态存储区、堆区和栈区之间的区别</strong></span></p>
- <p><span style="font-family:Microsoft YaHei; color:#000000; background-color:#ffffff"><strong> 3.1 :静态存储区与栈区</strong></span></p>
- <span style="font-family:Microsoft YaHei"></span><pre class="cpp" name="code">char* p = “Hello World1”; //p存放于栈区; "Hello World1"为字符常量(不能改变),存放于静态存储区
- char a[] = “Hello World2”; //a存放于栈区; "Hello World "存放在栈区
- p[2] = ‘A’; //error
- a[2] = ‘A’;
- char* p1 = “Hello World1;” </pre>
- <p><span style="font-family:Microsoft YaHei">这个程序是有错误的,错误发生在p[2] = ‘A’这行代码处,为什么呢,是变量p和变量数组a都存在于栈区的(任何临时变量都是处于栈区的,包括在main()函数中定义的变量)。但是,数据 “Hello World1”和数据“Hello World2”是存储于不同的区域的。</span></p>
- <p><span style="font-family:Microsoft YaHei; color:#ff0000">因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”为字符串常量,</span><span style="font-family:Microsoft YaHei">不可以改变,所以在程序运行时,会报告内存错误。</span><span style="font-family:Microsoft YaHei; color:#ff0000">并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据</span><span style="font-family:Microsoft YaHei"><strong><br>
- </strong></span></p>
- <p><span style="font-family:Microsoft YaHei"><strong> 3.2 : 栈区 与 堆区</strong></span></p>
- <pre class="cpp" name="code"><span style="font-family:Microsoft YaHei;"></span><pre class="cpp" name="code">char* f1()
- {
- char* p = NULL;
- char a;
- p = &a;
- return p; //p在栈区,函数结束时会被释放
- }
- char* f2()
- {
- char* p = NULL:
- p =(char*) new char[4]; //堆不会释放
- return p; //但是p在栈区,函数结束时会释放
- }
- int main()
- {
- char *p1=f1(); //f1中的指针指向的临时变量被释放,故p1为悬空指针
- char *p2=f2(); //此时p2指针指向f2()中新分配在堆上的内存地址,
- *p1='a'; //error! p对应的无内存地址,不可操作
- }
- </pre>
- <pre></pre>
- <p><span style="font-family:Microsoft YaHei"><strong>· </strong> f1()函数虽然返回的是一个存储空间,但是此空间为临时空间。也就是说,此空间只有短暂的生命周期,它的生命周期在函数f1()调用结束时,也就失去了它的生命价值,即:此空间被释放掉。此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。</span></p>
- <p><span style="font-family:Microsoft YaHei"><strong>· </strong> f2()函数不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。也可以这样理解,堆内存是共享单元,能够被多个函数共同访问。如果你需要有多个数据返回却苦无办法,堆内存将是一个很好的选择。但是一定要避免下面的事情发生:</span></p>
- <pre class="cpp" name="code">void f() //不返回记录堆内存的指针,p在函数结束后被释放。
- {
- …
- char * p;
- p = (char*)new char[100];
- …
- }<span style="font-family:Microsoft YaHei;"> </span></pre>
- <p><span style="font-family:Microsoft YaHei">这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。</span></p>
- <p><span style="font-family:Microsoft YaHei; color:#ff0000">但是,这块堆内存却一直标识被你所使用(因为没有到程序结束,你也没有将其delete,所以这块堆内存一直被标识拥有者是当前您的程序),进而其他进程或程序无法使用。我们将这种不道德的“流氓行为”(我们不用,却也不让别人使用)称为内存泄漏。这是我们C++程序员的大忌!!</span><span style="font-family:Microsoft YaHei">请一定要避免这件事情的发生。</span></p>
- <p><span style="font-family:Microsoft YaHei">总之,对于堆区、栈区和静态存储区它们之间最大的不同在于,栈的生命周期很短暂。但是堆区和静态存储区的生命周期相当于与程序的生命同时存在(如果您不在程序运行中间将堆内存delete的话),我们将这种变量或数据成为全局变量或数据。但是,对于堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。</span></p>
- <p><span style="font-family:Microsoft YaHei"><strong>3.3 深入区别 栈区 和 堆区</strong></span></p>
- <pre class="cpp" name="code">void f() {
- int* p=new int[5];
- } </pre>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text">看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?它分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。</span><span class="oblog_text"> 在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:</span></span></p>
- <pre class="cpp" name="code">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</pre>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text">这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。 好了,我们回到我们的主题:堆和栈究竟有什么区别?<span class="oblog_text">主要的区别由以下几点:</span></span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text">1、管理方式不同; 2、空间大小不同; 3、能否产生碎片不同; 4、生长方向不同;5、分配方式不同;6、分配效率不同;</span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"><span style="color:#ff0000"><span style="font-size:16px"><strong></strong></span></span><span style="color:#cc0000; background-color:rgb(255,255,255)"><strong>管理方式不同</strong></span>: 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory
- leak。 </span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"><strong><span style="color:#cc0000">空间大小不同:</span></strong> 一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在 VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在Category
- 中选中Output,然后在Reserve中设定堆栈的最大值和commit。 注意:Reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。<span style="color:#cc0000"><strong><br>
- </strong></span></span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"><span style="color:#cc0000"><strong>碎片问题:</strong></span> 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在它弹出之前,在它上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。<br>
- </span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"><strong><span style="color:#cc0000">生长方向:</span></strong><span style="color:#ff0000; background-color:rgb(255,255,255)">对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。</span></span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"><span style="color:#cc0000"><strong>分配方式:</strong></span>堆都是动态分配的,没有静态分配的堆。<span style="color:#ff0000">栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现</span>。<br>
- </span></span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"><span style="color:#cc0000"><strong>分配效率:</strong></span> 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高<span style="background-color:#00ccff"></span>。</span> 堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。<br>
- </span></p>
- <p><span style="font-family:Microsoft YaHei"><span class="oblog_text"> 从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。<br>
- 虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。 <br>
- 无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生意想不到的结果, 就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的.</span></span></p>
- </pre>