堆,栈,自由储存区

0x79 假如生活强奸了你,生活还要继续,你也得继续

刚度过了一个疲惫而无意义的周末:)有一个撒币女票真的是为生活增加了难度,4.23世界读书日这一天我们进入了一家图书咖啡馆,馆子不小人也不少,一路上她对于实验室藏书的炫耀间戏剧性的蹦出了mallo这个小玩意,我便问她“你知道malloc在内存里分配了哪一块区域吗?”,虽然我连续对“是在栈上”予以肯定想让她相信,可是她这次出奇的坚持,看来我的气势没以前那么有压迫性了:P

虽然上大学首先学习的是C,然后学习了C++,但是做了这么久Java,我突然发现之前为编程而学的那点C语言知识根本经不住时间的考验。纵使自己后来玩过一段时间Linux系统编程,随着时间的推移,很多东西都抛于脑后了。刚巧最近在补C++的知识,借着这个契机,我打算从0开始重新学习一下C++11,有一些易忘的点就顺便做个记录。由这件小事作为一个引子,我们来看看这个比较基础的概念性问题。

0x80 内容参考

  1. C语言变量声明内存分配
  2. C++ 自由存储区是否等价于堆?
  3. 细说new与malloc的10点区别

0x81 程序运行时的内存组成

我们从C语言说起:

  1. 程序代码区
    这个又叫Code区,其实这个概念在每一个可执行程序中都有,你的程序要运行,把必要的二进制代码加载到内存里是必须的,当然并不代表要一次性把所有可能执行的代码都加载到内存中,它们通常是按需加载。这一部分是程序启动时创建,程序退出时回收。

  2. 常量存储区
    狭义上它也可以称作字符串常量区,之所以这么说是因为这里通常保存的是常量字符串,表面上看起来有点像JVM的String常量池。我们用IDA等静态分析工具也可以发现它可以把字符串分析出来,有的时候可以节省我们很多时间。这一部分是程序启动时创建,程序退出时回收。

  3. 全局/静态区
    这个区域存放全局变量和静态变量,初始化和未初始化的分开存放。这块区域是在编译时决定的,而不是像动态变量一样运行时申请内存区域,其实字符串常量区也可以看作一种特殊的静态变量区域。这一部分是程序启动时创建,程序退出时回收。


  4. 运行时自动分配,通常存放了函数参数和局部变量,比如我们在函数内使用int num = 0;生命一个变量,它便是在栈区。之所以叫栈区是因为他的行为类似于数据结构中的栈,使用时入栈,函数返回时出栈,大名鼎鼎的栈溢出就发生在这个位置。


  5. 和上面的栈一样,属于动态存储区,但是这一部分区域是由程序编写者主动申请和释放,最好的实践是申请空间不使用时释放内存空间,以免发生内存泄露,不能仅仅依靠程序退出时操作系统对其进行回收。malloc关键字申请的内存便在这个区域上,C++很多实现中new的默认实现也是在堆上申请内存。

然后我们来说一个C++特有的概念——自由存储区:

我们通常会这样说,malloc在堆上分配一块指定大小的内存,用完使用free释放掉这一段内存;使用new 在自由存储区上分配一块对象大小的内存,使用delete删除该对象占用的内存。那自由存储区到底是什么?其实它只是C++里的一个抽象概念,它表示的是对象在new出来时所申请占用的内存区域。

0x82 栈和堆

栈和堆都属于动态分配,它们都是在运行时最终由操作系统负责分配给变量的内存,虽然堆(malloc/free)实际上给了编程人员更大的权利。栈的分配是在程序运行函数调用时由操作系统将局部变量、参数等压栈,在程序返回时出栈,这整个过程编程人员是没有办法直接干预的。使用malloc在堆上分配的内存则由编程人员控制,如果处理不好将产生内存泄露的风险,并且频繁的申请内存还会产生内存碎片影响整个作业系统的运行效率。

0x83 自由存储区与堆的区别

根据前面的说法,自由存储区只是C++里的一个抽象概念,它表示的是对象在new出来时所申请占用的内存区域。

堆,是操作系统术语,它是操作系统维护的一块内存,专门用于动态分配给程序使用,调用malloc时分配,调用free时回收。自由存储,是C++的一个抽象概念,使用new关键字申请的内存就叫做自由存储区内存,然后使用delete释放内存,在大部分情况下C++编译器都使用堆来实现自由存储区的分配。但是做C++开发的都知道,C++提供了强大的重载操作符机制,我们完全可以使用别的区域来实现自由存储区,所以它们其实是不同层面的两种东西,它们完全可以是同一种类型也可以不同。

0x84 malloc/free和new/delete

他们两个有很多相似的地方,但是却又有根本上的不同:

  1. 前面着重提到的内存位置
    malloc/free是在堆上为对象动态分配空间,new/delete是在自由存储区上。

  2. 返回值
    malloc是void 需要做强制类型转换,new是T 对应类型指针无需转换。

  3. 是否显式指定分配大小
    malloc需要显式指定大小,new不用。

  4. 构造函数与析构函数调用状况
    使用new创建的对象才会调用。

  5. 重新分配内存
    当发现内存不足时,我们可以使用realloc重新分配内存,如果连续内存足够则直接扩展,否则重新寻找内存区域。

差别其实有很多,甚至new也可以重载并使用malloc来实现一部分功能,但是不一样的东西过于揪住细节的话是没有意义的,我们需要做的就是在具有相似功能的事物上进行区分和甄别。