[程序设计语言] 堆和栈的全面总结

操作系统堆栈:

        分配由编译器自己主动和自己主动释放。对应于堆栈的函数。参数存储功能值、函数调用结束后完成值和局部变量的函数体内。段内存空间。其操作和组织方式与数据结构中的栈十分相似。栈是为了运行线程留出的内存空间。当调用函数时创建栈。当函数运行完毕,栈就被回收了。

操作系统中的堆:

         由程序猿手动进行内存的申请与释放。因为程序猿手动申请及释放的内存块存放在堆中。堆中有非常多内存块,所以堆的组织方式类似于链表。操作系统中的堆与数据结构中的堆全然不同。我认为通俗的理解能够是这种:数据结构中的堆是"结构堆"。有严谨的逻辑和操作方式。而操作系统中的堆。更像是使用链表将"一堆杂乱的东西"联系起来。堆是为动态分配预留的内存空间,其生命周期为整个应用程序的生命周期。

当应用程序结束以后,堆開始被回收。

        每个线程都有一个属于自己的栈,但每个应用程序通常仅仅有一个堆(一个应用程序使用了多个堆的情况也是有的)。当线程被创建的时候。设置了栈的大小。

在应用程序启动的时候,设置了堆的大小。栈的大小一般是固定的,可是堆能够在须要的时候进行扩展,如程序猿向操作系统申请很多其它内存的时候。

因为栈的工作方式类似于数据结构中的栈,堆的工作方式类似于链表,所以栈显然会比堆快得多。依照栈的存取方式,想要释放内存或是新增内存。仅仅须要对应移动栈顶指针就可以。堆则要首先在内存的空暇区域寻找合适的内存空间,然后占用。然后指向这块空间。显然堆比栈要复杂得多。

       接下来本来是想将栈和堆分开进行陈述,斟酌了一下还是决定从同一方面对栈和堆进行比較。有了比較才明显。

1. 在创建栈的时候栈的大小就固定了,由于栈要连续占用一段空间。依据上文所属的堆的特性,决定了堆的大小是动态的,其分配和释放也是动态的。

2. 栈中的数据过多会导致爆栈。比方dfs写搓了。

而假如堆也爆了的话。。

。那说明内存也爆了。

3. 每一个函数的栈都是各自独立的,可是一个应用程序的堆是被全部的栈共享。

既然提到共享,那么这里就有"并行存取"的问题了。实际上并行存取是由堆控制的。而不是被栈控制的。

4. 栈的作用域仅限于函数内部,栈在函数结束的时候会自行释放掉空间。可是创建于堆上的变量必需要手动释放。堆中的变量不存在作用域的问题。由于堆是全局的。

5. 栈中存放的是函数返回值地址、函数參数。函数内的局部变量等。堆中存放的是由程序猿手动进行申请的内存块(malloc、new等)。

6. 堆和栈都按需进行分配。栈有严格的容量上限。而堆的容量上限则是"不严格"的。堆并没有固定的容量上限,它与当前的剩余内存量有关(事实上还不准确,操作系统还有虚拟内存或其它概念,所以堆的工作方式较为抽象)。


7. 通过移动栈顶指针就可以实现栈内存的分配。

在堆上分配内存的做法则是从当前空暇的内存中找一块满足大小的区域。就像链表的工作方式一样。

8. 仅仅要没有超出栈容量。栈能够进行随意的释放和申请内存。并不会造成内存出现故障,是安全的。而堆不同,大量申请和释放小内存块可能会造成内存问题,这些小的内存块零散的分布在内存中。导致兴许大块的内存申请失败。由于尽管空暇的内存足够多。可是并不连续。这样的情况下的小块内存叫做"堆碎片"。只是这并非什么大问题。详细详见"操作系统"的有关知识。

9. 栈在确定了栈底地址后,其栈顶指针从栈底地址開始。逐渐向低地址走。也就是说栈的存储空间是从高地址走向低地址的。

堆则相反,堆在申请空间的时候通常逐渐往高地址的方向来寻找可用内存。

纯粹的文字描写叙述显得枯燥无味,我们来看一些代码:

#include <iostream>
using namespace std;

void func()
{
   int i = 5;
   int j = 3;
   int k = 7;
   int *p = &i;
   printf("%d
", *p);
   printf("%d
", *(p-1));
   printf("%d
", *(p-2));

}

int main()
{
   func();

   getchar();
   return 0;
}
上述代码的结果是:5 3 7

从结果中我们能够看出两件事:

一是栈地址是连续的,我们能够通过一个指针和一个相对的大小,来"偏移"到别的变量上去。

二是从中能够看出栈地址是从高到低分布的,栈底在高地址。朝低地址的方向生长。

所以程序中是p-1而不是p+1。

void func()
{
	int *p = NULL;
	// 上行代码是个重点。这个指针待会会用于申请新的内存。
	// 此时除了它自身作为一个变量须要占用4字节的空间(指针都占4字节),没有不论什么其它空间被申请。
	// 这个指针变量是函数的局部变量,所以它被创建在栈上。
	int num = 100; 		// 这个变量相同创建于栈上。
	int buffer[100];	// 相同的,buffer占用了栈的400字节的空间
	p = new int[100];	// 注意,程序猿手动申请了一块空间,这400字节的内存创建于堆上。
	// 所以此刻p的状态是:p为函数局部变量,它指向了一块全局范围的内存空间。
}
// 函数体结束。上述函数有个严重的问题,那就是指针p的内存泄露。
// 正确的做法是在函数最后delete掉这块内存。或是返回这块内存的地址以供继续使用。
接下来我们来了解一下当调用一个函数的时候所发生的事情:
首先操作系统为这个函数分配了一个栈。由于在调用完这个函数以后须要能正确返回到下一条语句并继续运行,所以第一步是将调用完函数的下一条指令的地址压入栈。这样当函数调用完毕,栈顶指针一点点释放内存以后。栈顶指针指向了这个地址,就能返回到正确的位置继续运行了。

int main()
{
	func();
	printf("%d
", 100);
	return 0;
}
比方上述代码,在调用func之前,首先把func的下一条语句,也就是printf语句的地址。存在栈中。

这样函数调用完毕后就能正确返回到这个printf并继续往后运行了。

注意这里的地址是指令地址,而不是变量地址什么的。它有那么点类似于操作系统中的程序计数器(PC,即Program Counter)。

然后把实參从右到左的顺序依次入栈(大多数的C/C++编译器为从右到左)接着是函数中的各种局部变量。要注意的是函数中的static变量是不入栈的。

全局变量和static变量在编译的时候就已经在静态存储区分配好内存了。


假设这个时候该函数又调用了其他函数,过程也是一样的。首先是返回地址,然后是參数和局部变量。

这样在每层调用结束,栈顶指针不断下降(释放内存)的时候,就能正确返回到之前调用的位置并继续往下运行了。
出栈。或者说释放内存的过程。依据栈的特性,是相反的,所以就不赘述了。


一个 C或C++程序,它眼中的内存地址分分为这么五个区域:

栈区(stack)、堆区(heap)、全局静态区(static)、文字常量区和程序指令区。

栈区和堆区前面已经介绍过。全局静态区用于存放全局变量和静态static静态变量。全局静态区分为两块内容:一块用于初始化以后的全局变量和静态变量,一块用于未初始化的全局变量和静态变量。全局静态区和堆一样,程序结束后由操作系统进行释放。文字常量区用于存放常量字符串。程序结束后由操作系统进行释放。

程序指令区最好理解,就是存放程序代码的二进制指令。

int cnt;		// 存放在全局静态区的未初始化区
int num = 0;	// 存放在全局静态区的已初始化区
int *p;			// 存放在全局静态区的未初始化区
int main()
{
	int i, j, k;			// 存放在栈区
	int *pBuffer = (int *)malloc(sizeof(int) * 10);	// 指针pBuffer在栈中,该内存在堆中
	char *s = "hactrox";	// 指针s存放在栈中,字符串存放在文字常量区中
	char str[] = "hactrox";	// str和字符串存放在栈中
	static int a = 0;		// a存放在全局静态区的已初始化区
}

char *s = "hactrox";    // "hactrox"在文字常量区。s指向这个区域中的"hactrox",所以这能够理解为。首先在文字常量区创建了这个字符串,然后s指向这个字符串这样两个步骤。

s本身作为一个局部变量存储在栈中。
// 以下的代码是错误的,指针还没指向就直接赋值了?
int *p = 5;
// 以下的代码才是正确的,首先要创建这个int型变量,然后p指向这个变量。new来的int变量在堆中。
int *p = new int(5);
接下来我们看一看一个很常见的问题:下述代码有没有什么问题?有问题的话问题在哪里?

#include <iostream>
using namespace std;

char* f1()
{
   char *s = "hactrox";
   return s;
}

char* f2()
{
   char s[] = "hactrox";
   return s;
}

int main()
{
   printf("%s
", f1());
   printf("%s
", f2());

   getchar();
   return 0;
}
问题在于第二个函数,f2并不能正确返回那个字符串。在函数f1中。"hactrox"字符串创建于文字常量区。然后返回该常量字符串的地址,由于文字常量区的字符串是全局的,尽管指针s是局部变量,可是s在消亡前已经把目标地址送出来了。所以s消亡与否不是重点。重点是返回的地址所指向的区域还在,所以能正确显示。在函数f2中。“hactrox”与s均为局部变量。它们保存在栈中。

尽管s相同返回了一个地址。但这个地址所指向的内存已经被释放掉了。地址有效,但目标已无效。所以输出的仅仅是乱码。
关于文字常量区另一些东西要说(好像和本博客的主题扯远了?)

#include <iostream>
using namespace std;

void func()
{
	char *str1 = "123";
	printf("%x
", str1);
	char *str2 = "123";
	// 同在文字常量区。编译器可能会将str2直接指向str1所指向的内存。
	// 而不是开辟新的空间来存放第二个同样字符串。
	// 通过打印str2的指针可验证
	printf("%x
", str2);

	char *s1 = "hactrox";
	printf("%x
", s1);
	char *s2 = "hactrox";
	printf("%x
", s2);
}
int main()
{
	func();

	getchar();
	return 0;
}
char s[] = "hactrox";
char *s = "hactrox again";
第二段代码,即文字常量区变量在编译的时候就已经确定了,
而第一段代码。是在执行的时候进行赋值的。
这样看起来貌似第二段代码的效率要高。事实上不然,
当在执行时刻用到这两个变量的时候,对于第一段代码。直接读取字符串,而对于第二段代码,首先读取该字符串指针。然后依据指针再读取字符串,显然效率就下降了。

事实上我认为关注栈和堆,事实上主要是关注作用域、生命周期和有效性的问题。

指针被释放了。不代表指针指向的内存会被释放。相同的。指针指向的内存被释放了,不代表指针会被同步释放或自己主动指向NULL,指针依然指向那块已经失效了的地址。这个地址不能用于。没有人能保证较长的有效地址,接下来会发生什么。

版权声明:本文博客原创文章,博客,未经同意,不得转载。

原文地址:https://www.cnblogs.com/bhlsheji/p/4675742.html