如何理解堆和栈(堆和栈它们的内涵有多深)
前言
作为一名程序员,许多同学会把堆和栈这两个字,变成一个词放在一起,其实这是错误的。
堆(heap)
堆是一个内存空间,这个内存可以由程序员分配和释放,当然部分语言自带 GC( Garbage Collection 垃圾回收),部分堆内存可以由 GC 回收。这里千万要注意,这里说的「堆」和 数据结构 里面说的「堆」不是同一个概念,大家千万不要混淆。
堆是程序在运行的时候请求操作系统分配给自己内存。由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率相对栈来说略低。但是堆的优点在于,编译器不必知道要从堆里分配多少内存空间,也不必知道存储的数据要在堆里停留多长的时间,因此用堆保存数据时会得到更大的灵活性。所以为达到这种灵活性,在堆里分配存储空间时会花相对更长的时间,这也是效率低于栈的原因。
栈(stack)
栈是由编译器自动分配和释放的,存放函数的参数值,局部变量的值等。也请注意,这里说的「栈」不是 数据结构 中的「栈」,大家千万不要混淆。这里请注意,栈是由由系统自动分配。
栈的优势是存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
说的再通俗点:
使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就像是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
实际应用
看完上面一段文字,大多数同学自然就懂了,而不懂的,还是一脸懵。这些东西我知道了有什么实际意义?比如栈既然是系统分配和释放,我干预不了,我知道它干什么?那这里就以编程语言 C#(C Sharp)来举例说明。也可以看相应的 LeetBook
堆、栈的实际意义
堆、栈,算是相对底层一些的内容,大部分的语言其实这部分内容都是类似的。如果你不会 C# 语言也能看懂~。
我们都知道,C# 的数据类型有两大类型 —— 值类型 和 引用类型。大致结构如下图:
值类型我们看到包含内置值类型、用户定义的值类型和枚举。枚举就不用说了,内置值类型指的是 int、float、bool、double 等等。
用户定义的值类型指的就是 struct。引用类型大概我们学 C 语言的时候学到的指针类型(C# 做了封装,所以我们看不到 int * 这类的代码,当然可以使用 unsafe 关键字,这里就不多赘述)、接口类型、用户自定义的类(class),数组等。
类型的含义
这里为什么要解释这两个大的类型?首先是因为值类型是放在栈上的,值类型变量声明后,不管是否已经赋值,编译器为其分配内存。然而引用类型当声明一个变量时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,这时会分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
通俗一点,堆上存放的是我们声明出来的实例对象,当我们需要访问这个实例对象时,我们找到它的方法是先找到变量对应在栈上的内存,然后通过栈上存放的数据,我们才能找到其真正的实例在堆的任意位置。
代码
class A{
public A(int x){
X = x;
}
public int X { get; set; }
}
static void Main(string[] args){
int m = 1;
int n = m;
m = 2;
Console.WriteLine($"m = {m}, n = {n}");
A a = new A(1);
var b = a;
a.X = 2;
Console.WriteLine($"a.X = {a.X}, b.X = {b.X}");
}
我们会发现输出结果是
m = 2, n = 1
a.X = 2, b.X = 2
我们发现 m 和 n 的值是分别独立的,而 a 和 b 修改其中一个会修改两个值。这是因为 m 和 n 都是分配在栈上的,而 a 和 b 虽然也是在栈上,但是其只是存储的堆上的一个地址索引,我们不管通过 a 还是 b 索引到的堆上的内存都是同一份。
我们再来看值类型和引用类型,在栈上的数据访问快,在堆上的数据访问相对慢,因此,当我们开发过程中,通过需求,比如底层的一些不变数据,完全可以有 struct 来实现,因为其不会为 null,符合值类型的要求,而且我们经常访问会更快一些。
我们看上图发现,值类型和引用类型都是继承自 Object ,也就是说:
Object a = 1;
int b = (int)a;
这两个都是合法语句。但是这里面隐藏了一个非常常见的一个现象,那就是装箱和拆箱。
- 什么是拆装箱?
装箱实际上是将值类型转换为引用类型,而拆箱是将引用类型转换为值类型。再透彻点,实际上装箱拆箱就是栈内存和堆内存的来回拷贝和赋值,这不仅仅浪费性能,而且还会造成没必要的 GC。因此,能避免 “装箱”、“拆箱” 操作的就尽量避免。除上面所说的这些数据类型,还有一个写代码必不可少的,那就是函数。
函数是如何调用的?实际上函数调用的参数是通过栈空间来传递,在调用过程中会占用线程的栈资源。
当我们使用递归算法时,每次虽然调用的是同一个函数,但是会在栈中占用一个空间,只有走到最后的结束点后函数才能依次退出,而未到达最后的结束点之前,占用的栈空间一直没有释放。因此如果递归调用次数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,这也是为什么一定要尽量少用递归的原因。
写在最后
上面说了很多,侧面说明了堆、栈的应用的优缺点,平时应用中需要更加了解我们写的代码都“干了什么”,这样才有可能写出更高效、更可靠的代码。对于一名程序员来说,不管未来编程语言发展的多么容易上手,一定不要忘了学习计算机的基础知识,哪怕是先学会了编程再返回来补习这些知识。祝大家工作顺利、评论留言少 BUG !点赞转发不脱发!
BY /
本文作者:力扣
声明:本文归“力扣”版权所有,如需转载请联系。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com