忘记释放内存(内存泄漏)

编程最佳实践是释放由你自己的代码直接分配的任何内存,或通过调用内部或外部函数(如 strdup() 等库 API)隐式释放 。无法释放内存可能会导致内存泄漏,这可能会累积到程序(或系统)无法使用的大量浪费内存中,从而可能导致崩溃或未定义的行为。如果在循环或递归函数中重复发生泄漏,则更可能发生问题。泄漏程序运行的时间越长,程序失败的风险就越大。有时候会出现问题; 其他时候,经常数小时甚至数年不会出现问题。内存耗尽故障可能是灾难性的,具体取决于具体情况。

以下无限循环是泄漏的一个示例,它最终会通过调用 getline() 来消耗可用的内存泄漏,getline() 是一个隐式分配新内存的函数,而不释放该内存。

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

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    /* The loop below leaks memory as fast as it can */

    for(;;) { 
        getline(&line, &size, stdin); /* New memory implicitly allocated */

        /* <do whatever> */

        line = NULL;
    }

    return 0;
}

相反,下面的代码也使用 getline() 函数,但这次,正确释放分配的内存,避免泄漏。

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

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    for(;;) {
        if (getline(&line, &size, stdin) < 0) {
            free(line);
            line = NULL;

            /* Handle failure such as setting flag, breaking out of loop and/or exiting */
        }

        /* <do whatever> */

        free(line);
        line = NULL;

    }

    return 0;
}

泄漏记忆并不总是会产生切实的后果,也不一定是功能性问题。虽然最佳实践要求在策略要点和条件下严格释放内存,以减少内存占用并降低内存耗尽的风险,但也有例外。例如,如果程序的持续时间和范围有限,则可能会认为分配失败的风险太小而无法担心。在这种情况下,绕过显式释放可能被认为是可接受的。例如,大多数现代操作系统在程序终止时自动释放程序消耗的所有内存,无论是由于程序故障,系统调用 exit(),进程终止还是到达 main() 结束。

如果内存不足,则分配可能会失败,并且应在适当的调用堆栈级别考虑处理故障。getline(),如上所示是一个有趣的用例,因为它是一个库函数,它不仅分配它留给调用者释放的内存,而且可能由于多种原因而失败,所有这些都必须加以考虑。因此,在使用 C API 时,必须阅读 文档(手册页) 并特别注意错误条件和内存使用情况,并注意哪个软件层承担了释放返回内存的负担。

另一种常见的内存处理方法是在释放这些指针引用的内存后立即将内存指针设置为 NULL,因此可以随时测试这些指针的有效性(例如,检查为 NULL /非 NULL),因为访问释放的内存可能导致严重问题,例如获取垃圾数据(读取操作)或数据损坏(写入操作)和/或程序崩溃。在大多数现代操作系统中,释放内存位置 0(NULL)是 NOP(例如它是无害的),正如 C 标准所要求的那样 - 因此通过将指针设置为 NULL,如果指针没有双重释放内存的风险被传递给 free()。请记住,双重释放内存可能会导致非常耗时,混乱且难以诊断故障。