C/C++ 的思考:int a[10] 和 int *a = malloc(10 * sizeof(int)) 的区别

曾几何时,我们初学 C/C++ 的新人总会产生这么一个疑问:

int a[10] 和 int *a = malloc(10 * sizeof(int)) 究竟有何区别?

这个问题的答案或许很简单,但是这里我想要细细的去探讨下。

我们都知道,C 和 C++ 对于数组和动态内存的处理方案是不同的,C 使用了数组、malloc/free 的方案,而 C++ 为了整合 OO 的概念,提出了 std::vector 的数组替代物(STL,或者说 std::array)、new/delete 的方案。

然而设计思想都离不开,本文标题中这个关键的概念,int a[10] 和 int *a = malloc(10 * sizeof(int)) 的区别。

二、int a[10]

我们都说,数组是可以随机存取的。那么为什么呢?

数组就是一块内存,一块内存也可以被用作数组
《C 程序设计新思维》(第 2 版)

比如说 int a[10] 这个数组中的第 5 个元素,你可以通过数组开始的位置,计算 5 * sizeof(int) 字节的偏移,便可找到第 5 个元素。

数组元素的初始化也是非常简单的:

int a[10] = {0};

 
 
  • 1

并且,我们都知道数组的使用也是非常方便的,系统负责分配内存和释放内存(不像 malloc/free 那么复杂)。

概念很简单,但是 int a[10] 带来了一个莫大的限制,那就是,int a[10] 定死了申请内存的范围

这里,我们要记住一个概念,函数内部声明的 int a[10] 的内存分配于栈区。这里暂且留个悬念,后面我会合着堆区一起讲解。

三、int *a = malloc(10 * sizeof(int))

上面提到的 int a[10] 使用的是栈区的定死了的内存区域,那么相应的,malloc 提供的就是动态分配的内存区域。

malloc 所动态分配的内存区域,位于堆上,需要我们程序员手动在使用完毕后进行 free(相应的 C++ 就是 new/delete)。

对于 malloc 的内存区域来说,初始化该区域的值可以使用 calloc 函数:

int *a = calloc(10, sizeof(int));

 
 
  • 1

甚至于,我们可以重新分配 malloc 的内存区域的大小:

a = realloc(a, 20 * sizeof(int));

 
 
  • 1

最重要的,使用完之后千万要记得释放内存:

free(a);

 
 
  • 1

那么二者究竟有什么区别呢?栈区内存和堆区内存又有什么区别呢?

为什么 C/C++ 在设计的时候,数组就可以不用手动 free,而堆区一定要手动 free 呢?既然数组都可以自动释放内存,为何动态内存不这么设计呢?

带着这些问题,我们接下来继续探讨。

四、一切源于堆栈

这一切都因为栈与堆的设计。这里我大胆引用 浅析栈区和堆区内存分配的区别 这篇博客的博主所做的比喻:

堆和栈的区别可以用如下的比喻来看出:

使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

使用堆就像是自己动手做喜欢的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度高。

我很喜欢这位博主的比喻,非常的形象。

这里也建议大家去看下该博主的这篇博客,将栈区和堆区讲解的非常好,我这里简单总结几条要点:


连续的内存区域,向低地址扩展的数据结构。栈顶的地址和栈的最大容量是系统预先制定好的。如果想了解系统栈的大小的默认值,可以看看这篇博客:
Windows栈大小

一般来说,32 位 Windows 一个进程的栈的大小为 1M。如果申请的控件超过栈的剩余空间时,将提示内存溢出。

总之,能从栈获得的空间较小,但是内存分配速度快,可由编译器自动分配释放。


不连续的内存区域(类似链表一样由多个内存碎边控件链接而成),向高地址扩展的数据结构。系统使用链表存储空闲内存地址,自然是不连续的,遍历方向从低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存(这里根据我网上查询的结果,大概是 GB 级别的,跟栈的 MB 简直是大了三个量级!!!)。

所以堆中分配的内存速度较慢(毕竟是不连续的内存,又是链表结构),容易产生内存碎边。

总之,能从堆中获得的空间较大,自由度也很高,但是内存分配速度低,必须要程序员负责分配和释放。

然而我们还有一个问题没有解决
那么到这里你应该就懂了,实际上看似很相似的 int a[10] 和 int *a = malloc(10 * sizeof(int)) 这两种方式,实际上对应了栈区和堆区这两种内存使用方式。

但是上述的阐述,并没有解释一个最关键的问题,为什么堆区的内存一定要程序员去释放,而栈区不需要呢?

五、堆区的内存为什么一定要程序员释放

这个问题我也回答不出来,因此我去网上寻找了一份自认为很好的回答,引用在这里分享给大家 堆内存为什么要程序员自己释放

对SP寄存器的值进行操作而形成逻辑上的栈,而局部变量是在函数内部定义的,就是在栈上定义的,函数的调用和对栈的操作这是一个很基础的也是很重要的知识点,你把局部变量的释放理解成了一个单独的动作,事实上编译器没有对这个局部变量的空间做任何内存管理意义上的操作。只是一个简单的对SP寄存器的值的一个改变即形成了栈的恢复动作。栈一恢复了,那之前那段栈内存上的数据你肯定找不到了,但那个物理内存地址上的数据却还是原状,所以我建义你把 ”自动释放局部变量“ 这几个字改成“自动丢弃局部变量”,丢了的东西就是找不回了,你没法用了,但那东西不会凭空从地球上消失,只是从你的视野里消失了,可能别人捡到了,别人就能用得着了,就像在函数A里创建的局部变量 local_a 可能在进程的地址空间里是地址0X11,但函数A返回之后,可能主程序又要调用函数B,而在函数B里创建的局部变量 local_b 也可能就是在0X11这个地址,那么地址0X11上边的值从A函数结束到B函数开始这段时间一直没变,只是在这段时间里没有人管理它的值。而这个过程就叫释放,或叫丢弃,
对堆内存的操作实际是动态内存操作,就涉及到内存管理了,其实现在的操作系统内核对用户空间的进程这块的创建与注销都加入了内存保护,你在用户空间的程序中释不释放堆内存,系统都会在这个程序结束时做内存回收动作,但这仅限于用户空间的程序,如果是内核编程的话,就一定要严格释放掉动态分配的内存,否则造成系统内存泄漏,内存泄漏的后果就是可用内存被你人为的弄成了不可用内存,到最后导至系统无动态内存可分配,就无法加载程序。
对于你的提问,其实没有办法回答得让你理解透澈,你现在对程序的运行机制和操作系统原理基本上是一个零的认识,慢慢来吧,多看操作系统原理的书,其实用户程序都是基于操作系统编程,理解一些原理性的东西是非常重要的。

简单来说,就是栈区的内存,编译器并非是 “释放” 掉,而是简单的 “丢弃” 掉了;而堆区的内存,如果你不去手动释放掉的话,就会造成可用的空闲内存空间越来越少,可想而知其导致的结果是什么。

这里我也需要去认真拜读下《程序员的自我修养》这本书吧,哈哈哈 ^_^

六、总结

这是一篇我自己自我探索的博客,自己提出问题,从 int a[10] 与 int *a = malloc(10 * sizeof(int)) 的区别开始,探索到栈区与堆区的区别,最后探索到栈区与堆区的实际意义上。当然这个问题继续深究下去,就能到操作系统那个层面上去,毕竟探索永无止境。

这篇博客融合了自我的思考和网上博客、回答的信息,算是给我自己一个满意的探索结果。希望这篇博客能够给你带来一些帮助吧。

To be Stronger:)

        </div>
					<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-258a4616f7.css" rel="stylesheet">
                  </div>
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值