zl程序教程

您现在的位置是:首页 >  其他

当前栏目

C++内存模型,我们常说的堆栈究竟指什么?

2023-02-18 16:38:01 时间

作者 | 梁唐

出品 | 公众号:Coder梁(ID:Coder_LT)

大家好,我是梁唐。

今天我们来聊聊程序运行时的内存管理。很多同学可能对内存管理这个概念比较陌生,尤其是在校学生,没有接触过这个方面是非常正常的。虽然存在感不高,但是它在我们工程能力当中起到非常重要的一个部分。尤其是从事后端相关的开发的话,这是一个很重要的领域。很多设计和算法的出发点都是围绕内存管理展开的。

这个部分的技术非常底层,并且和操作系统和编译等方面有比较紧密的结合,算是非常硬核的一个技术领域了。如果你听说身边有人在研究这个方向,那么十有八九此人一定是大牛。

好在,对于初学者而言,我们也不必这么深入,对一些主流编程语言的内存模型有一个大概的认知就可以了。

C++内存模型

关于C++的内存模型,《代码随想录》里将它分成了四个部分,也有一些博客更精细一些分成五个部分。不管怎么分,每个分块的逻辑和功能是类似的。

我们来看下这几个部分,分别是Stack(栈),Heap(堆),BSS(Block started by symbol),Data Segment(数据区),Code Segment(代码区)。

固定部分

这里面最容易理解的是代码区,顾名思义存放的就是可执行的代码。由于C++是编译语言,所以这里存放的是编译之后的机器码。

接着是数据区和BSS,这两个功能差不多,所以有些版本会直接合并在一起介绍。它们存放的是数据,主要是全局的数据以及静态数据。唯一的区别在于数据区存放的是已经初始化全局和静态数据和常量数据,而BSS存放的是未初始化的。所以我们也可以合并理解,数据区存放的是全局和静态变量以及常量。

代码区和数据区都是固定的,都是在代码编译时就可以提取得到的。而堆栈区则是动态的,是在代码运行时可能产生变化的。一般来说我们通常不太关注固定区的部分,更多地会关注动态的堆栈部分。所以大家谈论内存管理时,谈得最多的就是堆栈。

动态部分

堆栈虽然经常相提并论,但实际上它们是两个不同的概念。

先来说说栈,栈区储存的是程序中的局部变量,函数参数、返回变量以及函数栈。可以简单理解成当我们调用一个函数时所关联的上下文信息,比如函数的传入参数,函数内部的局部变量,函数本身的信息以及返回的结果。这些都会存放在栈区。

之所以叫做栈区,是因为存储这些信息的数据结构是栈。栈的特点是先进后出,编译器每次会执行最顶端的函数。

所以我们常用的递归算法本质上就是利用了这里的栈区,免去了我们自己手动编写栈的工作。这不仅仅是偷懒而已,在很多问题场景当中,如果不利用系统的栈区而要自己手动建栈的话会使得问题变得复杂得多。

不过系统栈也有问题,最大的问题就是它的内存大小是编译时确定的,在运行时不能更改。因此当我们的调用栈太长时,就会导致要存储的栈帧太多,超过了栈区的内存限制。大家感兴趣的话用C++编写一个无限递归的函数运行一下就知道了,一般来说不经过特殊优化的话,最大递归深度应该在40w~80w左右。

和栈相比,堆区的概念要好理解很多,它存储的是函数运行时动态创建的数据。

在C++当中体现出使用new或者malloc关键字创建的对象,通常情况下堆区的内存要比静态数据区大很多。所以这就是为什么我们在实际编程当中不推荐创建太多全局变量的原因,因为全局变量是存放在BSS区的,创建之后一直存在无法回收。一般除了比赛场景,通常只会将少量必要的信息作为全局变量。

既然堆区是动态的,那么可以创建自然也可以回收。谈到回收,要说的内容就有很多了。

最常见的问题就是忘了回收,或者是错过了回收的机会,这就是常说的内存泄漏。堆区虽然大但也是有限的,如果出现有些对象不再使用却不回收,就相当于是减少了堆区的内存上限。如果这样的对象越来越多,那么总有一刻会导致程序崩溃。这就是为什么很多古旧的服务虽然能正常运行,但是每隔一段时间就需要重启或者一段时间之后会自己崩溃的原因,往往罪魁祸首就是内存泄漏。

在使用new或者malloc创建对象时要牢记在哪里使用在哪里销毁的原则,一旦创建对象的函数执行结束,并且创建的对象指针没有保存下来,那么这块内存就永远无法释放了,这也是出现内存泄漏最常见的原因。

其次是newdelete要配对使用,不要使用delete去释放malloc创建的对象,也不要free new出来的对象。因为它们分别属于C++和C语言,并不是通用的,因为编译器的版本问题,可能会导致不可预测的问题发生,极大增加debug的成本。

根据我个人的经验,内存泄漏引发的问题是相对来说最难排查和修复的,更多的需要依赖工程师的素养在事前规避。

相比于C++,Java、Python、Go等语言就没有这个问题,因为这些语言使用了GC(垃圾回收)策略。会自动回收那些已经不再使用的对象,不同语言的GC策略略有不同,大家感兴趣的话可以自行了解。其中以Java的GC策略最经典也最复杂,基本上也是Java工程师面试必问的内容,如果准备面试的话,这块可以着重了解一下。

那是不是Java这些有GC的语言就更好呢?

其实也不是,作为使用者来说,语言本身自带GC当然是一件很轻松的事情。因为我们可以不必自己管理内存,而全部托管给编译器来解决。但某种意义上来讲,其实这也是丧失了操控内存的自由,在一些问题场景当中可能不是很方便。另外,GC也不是没有代价的。比如Java当中触发Full GC时会stop the world,即程序停止响应,等GC完成之后才会继续运行。显然,这样无疑会影响程序的运行效率。

所以我们是很难用一句好或者不好来评价C++的内存管理的,更多的还是要基于具体的问题场景。

除了上述提到的内容之外,C++内存模型涉及的细节很多,而且很多依赖实际项目工程经验。由于老梁不是专业的C++工程师,这方面积累也比较欠缺。如果有所疏漏谬误,还请各位大佬在评论区里赐教。