Skip to content

🔥更新:2024-12-17📝字数: 0 字⏱时长: 0 分钟

第一章:回顾知识

1.1 虚拟地址空间

  • 为了更好的管理程序,操作系统将虚拟地址空间分为了不同的内存区域,这些内存区域存放的数据、用途、特点等皆有不同,下面是 Linux 下 32 位环境的经典内存模型,如下所示:
  • 每个内存区域的特点,如下所示:
内存分区说明
程序代码区(code)存储程序的执行代码,通常为只读区,包含程序的指令。 程序启动时,这部分内存被加载到内存中,并不会在程序执行期间改变。
常量区(constant)存放程序中定义的常量值,通常也是只读的,这些常量在程序运行期间不可修改。
全局数据区(global data)存储程序中定义的全局变量和静态变量。 这些变量在程序的整个生命周期内存在,且可以被修改。
堆区(heap,⭐)用于动态分配内存,例如:通过 mallocnew 分配的内存块。 堆区的内存由程序员手动管理,负责分配和释放。 如果程序员不释放,程序运行结束时由操作系统回收。
动态链接库动态链接库(如: .dll.so 文件)被加载到内存中特定的区域,供程序运行时使用。
栈区(stack)用于存储函数调用的局部变量、函数参数和返回地址。 栈是自动管理的,随着函数的调用和返回,栈上的内存会自动分配和释放。

提醒

  • ① 程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。
  • ② 函数被调用时,会将参数、局部变量、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。
  • ③ 常量区、全局数据区、栈上的内存由系统自动分配和释放,不能由程序员控制。程序员唯一能控制的内存区域就是堆(Heap):它是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分,在这片空间中,程序可以申请一块内存,并自由地使用(放入任何数据)。堆内存在程序主动释放之前会一直存在,不随函数的结束而失效。在函数内部产生的数据只要放到堆中,就可以在函数外部使用。
  • 在 64 位 Linux 环境下,虚拟地址空间大小为 256TB,Linux 将高 128TB 的空间分配给内核使用,而将低 128TB 的空间分配给用户程序使用,如下所示:

提醒

  • 程序代码区,也可以称为代码段;而全局数据区常量区,也可以称为数据段
  • 全局数据区分为初始化数据段(存储已初始化的全局变量和静态变量)和未初始化数据段(存储未初始化的全局变量和静态变量);常量区也称为只读数据段,通常是只读的,防止数据被修改。
  • ③ 冯·诺依曼体系结构中的程序,也被称为存储式程序,需要通过加载器(Loader),将程序从硬盘加载到内存中运行。
  • 存储式程序中的程序分为指令数据;其中,代码段中保存的是指令数据段中保存的是数据

1.2 栈区的内存管理及其特点

1.3 堆区的内存管理及其特点

第二章:C 语言中的动态内存分配(⭐)

2.1 概述

malloc 是 C 语言中用于动态分配内存的函数,所分配的内存不会随着程序结束而自动释放,除非操作系统负责回收。这意味着,在程序运行期间,使用 malloc 分配的内存如果不手动释放(即使用 free() 函数释放),程序本身并不会自动回收这些内存。

具体情况如下:

  1. 程序运行期间:
    在程序运行过程中,使用 malloc 分配的内存必须由程序显式地调用 free() 来释放。如果不这样做,就会造成内存泄漏,即这些内存永远不会再被程序使用,直到程序结束。

  2. 程序结束后:
    当程序完全结束后,操作系统通常会收回该程序所占用的所有资源,包括 malloc 分配的内存。因此,程序终止后,尽管没有调用 free(),操作系统仍然会回收这些动态分配的内存。

  3. 为什么还是要手动释放?
    尽管程序终止后内存会被操作系统回收,但如果你的程序是一个长时间运行的应用(如服务器、后台服务),持续的内存泄漏将会导致内存的不可用部分不断增多,最终导致系统内存耗尽。因此,为了保证程序的健壮性和高效性,在程序运行中及时手动释放内存是非常重要的。

例子:

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(10 * sizeof(int));  // 动态分配内存

    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 10; i++) {
        ptr[i] = i;
        printf("%d ", ptr[i]);
    }
    printf("\n");

    free(ptr);  // 手动释放内存

    return 0;
}

在这个例子中,malloc 分配了 10 个整数的内存,并在最后通过 free() 释放了这部分内存。如果没有调用 free(),那么在程序运行期间这部分内存将无法被重新使用,导致内存泄漏,但程序终止后,操作系统还是会回收它。

总结来说,尽管操作系统会在程序结束时回收内存,但在程序运行过程中,我们仍然需要显式地释放通过 malloc 分配的内存以防止内存泄漏。

是的,尽管操作系统会在程序终止时回收程序所占用的内存资源,仍然建议手动释放通过 malloc 分配的内存。有几个原因:

  1. 良好的编程习惯:及时释放动态分配的内存有助于养成良好的编程习惯。内存泄漏虽然在程序终止时会被操作系统回收,但在长时间运行的程序或嵌入式系统中,内存泄漏会逐渐耗尽可用内存,导致程序运行效率低下,甚至崩溃。

  2. 动态库和插件环境:如果你编写的是动态库或插件,这些代码可能在应用程序的生命周期内被多次加载和卸载。如果不手动释放内存,内存泄漏可能累积,导致更严重的问题。

  3. 避免误用或未及时释放:即使操作系统会回收内存,如果程序运行中频繁分配而不释放内存,可能会导致内存使用量过大,甚至超出系统资源,特别是在长时间运行的服务或应用中。这会使程序在运行过程中性能下降或出现异常。

  4. 跨平台行为差异:虽然大多数现代操作系统会在程序退出时自动回收内存,但在某些嵌入式系统或资源有限的环境中,可能不会有这样的机制。因此,手动释放内存是一种更为安全的选择。

总结来说,尽管操作系统最终会释放程序终止时的内存,手动释放动态分配的内存仍然是一个良好的编程实践,能够提高代码的健壮性和可维护性。

在 C 语言中,使用 malloc 动态分配内存后,需要使用 free 来释放内存,主要原因有以下几点:

1. 避免内存泄漏

当使用 malloc 动态分配内存时,系统为程序分配了一块指定大小的内存,而这块内存不会在使用完毕后自动释放。只有通过调用 free 函数,操作系统才能将这块内存标记为可重新使用。如果不调用 free,内存将一直被程序占用,直到程序结束,这会导致内存泄漏

内存泄漏的危害在于:

  • 耗尽系统内存:程序长期运行而不释放内存会逐渐消耗系统内存资源,最终可能导致系统或程序崩溃。
  • 性能下降:随着内存泄漏积累,系统内存资源减少,导致程序运行速度变慢或出现资源不足问题。

2. 提高程序的内存管理效率

手动管理内存允许程序员根据需要灵活地分配和释放内存资源。通过及时释放不再使用的内存,可以有效地提高程序的内存利用效率,使系统能够将这些内存重新分配给其他任务。这对于长期运行的程序,尤其是服务器或后台进程,尤为重要。

3. 避免程序异常行为

如果不及时释放内存,不仅会导致内存泄漏,还可能导致程序出现意外行为,例如:

  • 内存碎片化:当分配和释放内存的顺序不一致时,内存会变得零碎化,剩余的内存虽然总量足够,但可能无法满足某次较大的分配请求,从而导致分配失败。
  • 内存访问问题:未释放的内存在后续程序运行中可能仍被访问,如果程序继续使用这块已过期的内存,可能会出现不可预知的错误或崩溃。

4. 符合动态内存分配的对称性原则

mallocfree 是一对相互配合的函数。malloc 用于申请内存,而 free 则用于归还这块内存。这种对称性使得内存管理变得可控和可维护,符合编程中资源管理的基本原则:谁申请资源,谁负责释放资源

5. 防止其他潜在的内存管理问题

  • 悬空指针(Dangling Pointer):如果没有正确使用 free 释放内存,并继续使用释放前的指针,可能会导致悬空指针问题。调用 free 后通常也应将指针置为 NULL,以避免无效的指针引用。

  • 内存重用问题:在不释放内存的情况下重新分配内存,可能导致内存重叠或不可预期的行为。

总结

通过 free 释放动态分配的内存,可以避免内存泄漏、提高内存管理效率、减少异常行为以及确保程序的健壮性。总的来说,良好的内存管理实践要求在使用 malloc 分配内存后,在合适的时机释放这些内存,以保证程序的稳定性和高效性。

Released under the MIT License.