0%

程序和内存 04.堆区

程序和内存 04.堆区

如果在一个进程空间里只有一个战区肯定是没有办法完全满足我们的要求的, 因为战区的特点是容量相对较小, 容纳不了大量的动态数据。

关于战区的内容可以回头看前面的文章, 既然战区满足不了就拿出heap堆区来实现这部分的需求。

比如说我们有个需求, 拼接两个字符串, 这里用到的库函数非常重要如果不熟悉好好研究一下, 这里不赘述了, memcpy和strcpy都在string.h头文件里。

char *str_join(char *s1, char *s2)
{
char result[1024];
memcpy(result, s1, strlen(s1));
strcpy(result + strlen(s1), s2);
// printf("%s\n", result);
return result;
}
// main
char *s = str_join("hello,", "world");
printf("%s\n", s);

编译阶段出现了警告, 返回了无效的局部变量的地址。

从运行结果可以看出函数业务逻辑没有问题的, 但是返回的时候出现了问题,

我们知道函数内部的数组是放在了战区, 所以函数返回之后result变量就销毁了, 自然而然它的地址也无效了, 你返回了局部变量的地址就是严重错误,

堆区heap 初体验

所以我们就需要heap来存取result里的内容, heap空间可以用C语言的malloc函数来申请内存空间。

如下是改进后的str_join函数, malloc函数在stdlib.h头文件里, 此外这里没有处理错误情况。

char *str_join(char *s1, char *s2)
{
char *result = (char *)malloc(strlen(s1) + strlen(s2) + 1);
memcpy(result, s1, strlen(s1));
strcpy(result + strlen(s1), s2);
printf("%s\n", result);
return result;
}

现在编译运行一切完美, 改动的地方只有一个把数组替换成了申请heap空间。

前面我们讲过的内存空间都是编译阶段已经确定好大小的, 而heap按需使用, 用的时候申请malloc, 用完了释放free,

理论上heap空间管够, 要多少给多少, 只要有空闲内存就给你用, 即便没有空闲内存, 也把内存上的东西挪到磁盘上, 腾出空间之后给你用, 当然磁盘上的也满了就没办法了, 因为我们知道磁盘上的虚拟内存也是有限的么。

这个和借钱特别相似, 借钱的时候特别爽快, 还钱的时候各种幺蛾子。

heap好用确实好用但是带来了三个关于内存的三大错误。

  • 内存泄漏 你申请malloc过来的heap空间没有释放free掉, 如果这个程序长期运行的话肯定出现问题, 比如我们知道电子设备运行一段时间后重启一下比较好, 现在重启的周期越来越长了, 也许这背后是垃圾回收算法的进步, 当然内存也越来越大了所以内存泄漏一点点无所谓,
  • 重复释放, 比如一块heap空间已经free掉了, 然后程序再次free这块空间, 本来你家房子已经拆迁, 在原地重建好了, 然后拆迁办的犯错再来一次拆迁你家新房子, 你说这严重不严重
  • 悬垂指针 访问已经free掉后的heap空间, 从前你加楼下有个诊所, 有一天你突然身体难受火急火燎的过去找这个诊所, 发现诊所已经搬迁了或者倒闭了

内存泄漏

三个内存错误里内存泄漏算是轻的了, 因为程序退出后所有的空间都被收回去了, 这里包括了你申请完后没有及时释放的,

如果你的程序运行时间特别短, 那么可以不释放 heap空间, 当然不提倡这样编写程序!

如果你的程序7*24小时不间断运行, 那么内存泄漏就是重大bug, 接下来感受一下内存泄漏的威力, 还是继续使用str_join函数, 去掉里面的printf输出!

while (1)
{
char *s = str_join("hello,", "world");
}

内存使用率到达90%后赶紧按下CTRL+c杀死这个进程, 或者放在虚拟机里运行, 否则可能要重启电脑了!

这里是一个简单演示而已, 真是世界的软件内存泄漏肯定是非常复杂, 难以发现和修复。

悬垂指针

这里我们用随机函数来演示一下悬垂指针。

char *s = str_join("hello,", "world");
srand((unsigned long)time(NULL));
if (rand() % 100 <= 50)
{
free(s);
}
printf("%s\n", s);

多运行几次, 感受一下悬垂指针。

重复释放也和悬垂指针一样, 这里就不展开了。

那么我们知道只有c/c++手动申请和释放heap空间, 其他语言是如何解决这些问题的? 我们已经习惯使用new, 想new多少就new多少, 从来没有关心过这些heap是怎么被释放的!

顶多学习某个编程语言介绍的时候看到xx语言使用了自动垃圾回收技术, 免去了你手动管理内存的痛苦。

垃圾回收

绝大多数的现代编程语言无一例外的使用了自动垃圾回收技术。

垃圾回收的好处在于你毫无负担的尽情使用heap空间就可以了, 什么内存泄漏; 悬垂指针这些麻烦全部丢给垃圾收集器来负责。

但是垃圾收集器运行的时候需要额外的性能开销, 你想想这个东西一直在扫描哪个heap已经没用了, 如果发现某个heap空间没用了就释放这块空间。

所以在某些场景下垃圾收集器不是特别适用, 当然我们现在能耳熟能详的高级编程语言都用到了这个技术, 比如:

java; C Sharp; golang; js; python; php; ruby; perl; lua等等等等。

围绕着垃圾收集算法也是百花齐放, 各显神通。

感兴趣可以参考专业文章和图书。

rust的特立独行

rust是一个崭新的系统编程语言, 自我定位是和C/C++一个性质, 火狐浏览器是用rust编写的, 它没有和c/c++一样手动管理内存; 也没有和其他主流语言一样使用垃圾回收技术。

rust提出了所有权和生命周期系统来避免内存的三大错误。

当然还包括了rust编译器的严苛的静态分析。

总而言之rust有一套严苛的语法规则, 从而保证编译生成可执行文件后没有任何heap方面的错误。

我之所以编写这一系列文章的目的也是为了学习rust和内存安全, 当然关于rust如何保证内存安全的, 我也是一知半解, 似懂非懂, 所以拿出c语言关于内存方面的知识复习一下, 顺便写下了这些文章。

最后给heap做一个总结, 也是本系列文章的结尾……

总结

  • heap空间按需使用, 用完就释放

  • heap没有正确释放带来了三大内存错误

  • 内存泄漏; 重复释放和悬垂指针

  • 为了避免内存错误出现了自动垃圾收集技术, 绝大多数的现代编程语言都引入了这一技术

  • 最近也有一些内存管理方面的新的技术, rust的所有权生命周期和配套的静态规则

    万变不离其宗, 如上是关于内存的最少知识量, 作为一个合格的编程爱好者必须要明白……