发生错误后是否必须释放任何先前分配的内存?

Do I have to free any previously allocated memory after an error occurs?

我正在用 C 为一个 uni 项目编写一个多线程服务器,但我很难弄清楚如何以一种漂亮、可读和标准的方式进行错误处理。

现在,如果程序成功终止,我会在程序末尾释放所有分配的内存。但是如果在执行过程中出现致命错误怎么办(例如malloc returns NULL)?

例如,假设我有一个自定义数据类型 mydata_t 和一个构造函数 mydata_t *mydata_init(),我的程序的多个模块都使用了它们。在网上看到一些代码后,我会这样写:

mydata_t *mydata_init() {

    mydata_t *mydata = malloc(sizeof(mydata_t));
    if (!mydata) return NULL;

    mydata->field1 = malloc(sizeof(mydata2_t));
    if (!mydata->field1) return NULL;

    mydata->field2 = malloc(sizeof(mydata3_t));
    if (!mydata->field2) return NULL;

    /*
    Initialization of other fields
    */

    return mydata;

}

看起来确实干净整洁,但这是 "standard" 的方法吗?

特别是,如果其中一个 mallocs returns NULL 怎么办?是否有必要释放所有先前分配的内存?把代码改成这样合理吗?

mydata_t *mydata_init() {

    mydata_t *mydata = malloc(sizeof(mydata_t));
    if (!mydata) goto err_1;

    mydata->field1 = malloc(sizeof(mydata2_t));
    if (!mydata->field1) goto err_2;

    mydata->field2 = malloc(sizeof(mydata3_t));
    if (!mydata->field2) goto err_3;

    /*
    Initialization of other fields
    */

    return mydata;

/*
Other tags
*/

err_3:
    free(mydata->field1);
err_2:
    free(mydata);
err_1:
    return NULL;

}

一种可能的选择。

mydata_t *mydata_init()
{
    mydata_t *mydata = malloc(sizeof(mydata_t));
    if (mydata == NULL)
    {
        /* Handle error */
        return NULL;
    }

    mydata->field1 = malloc(sizeof(mydata2_t));
    mydata->field2 = malloc(sizeof(mydata3_t));
    ...

    if (mydata->field1 != NULL && 
        mydata->field2 != NULL &&
        ...)
    {
        /* success */
        /*
         * Initialize everything
         */
        return mydata;
    }

    free(mydata->field1);
    free(mydata->field2);
    ...
    free(mydata);
    return NULL;
}

请注意,在错误路径上调用 free() 之前不需要检查 NULL。正如 this question

的第一个答案中所述

Quoting the C standard, 7.20.3.2/2 from ISO-IEC 9899:

If ptr is a null pointer, no action occurs.

Is it necessary to free all the previously allocated memory?

不,但非泄漏(好的)代码可以。

你会发现关于如何如何做到这一点(释放东西)的各种意见,但最终目标是做到这一点,以某种方式.免费未使用的资源。

代码清晰。

注意:goto 形式是 goto 的可接受用法。


我将提供另一种方法并尽可能使用 mydata_uninit() 伴随函数。

mydata_t *mydata_uninit(mydata_t *mydata) {
  if (mydata) {
    free(mydate->field1); 
    mydate->field1 = NULL; // example discussed below **
    free(mydate->field2); 
    // free other resources and perform other clean up/accounting.
    free(mydate); 
  }
  return NULL;
}

我还会分配给取消引用的指针的大小,而不是类型。

mydata_t *mydata_init(void) {
  mydata_t *mydata = calloc(1, sizeof *mydata);
  if (mydata == NULL) {
    return NULL;
  }   
  mydata->field1 = calloc(1, sizeof *(mydata->field1));
  mydata->field2 = calloc(1, sizeof *(mydata->field2));
  // Other 1st time assignments

  // If any failed
  if (mydata->field1 == NULL || mydata->field2 == NULL) {
    mydata = mydata_uninit(mydata);
  }

  return mydata;
}

** 将指针 struct 成员设置为 NULL (mydata->field1),然后释放 struct 指针 (mydata) 我在调试为取消引用 NULL 指针的错误代码通常比释放指针更快地出错。

假设一个非平凡的 OS,即具有 'kill-9' 的任务管理器或在 GUI 上下文菜单上具有 'End Process' 条目的任务管理器,请在开始之前牢记以下几点在编写特定用户代码以在发生致命错误时释放所有 malloced/whatever 内存的漫长而昂贵的活动中:

1) 用用户代码释放所有内存需要更多的用户代码。必须设计、编码测试和维护额外的代码,通常在更改 and/or 新版本后重复进行。对于一个复杂的多线程应用程序,可能具有在线程之间共享和通信的对象池,即使尝试使用用户代码将其关闭也不是一件微不足道的事情。

2) 在发生致命错误后释放用户代码的所有内存会使事情变得更糟。如果错误是堆管理器损坏的结果,那么在尝试释放它时会引发更多错误。带有错误日志条目的客户应用 'diasppearing' 已经够糟糕了,满屏 AV 错误框和卡住的应用更糟糕。

3) 如果所有其他线程都已停止,则只能由一个线程使用用户代码安全释放所有内存,这样它们就不可能访问任何内存。可靠地停止所有进程线程只能由 OS 在进程终止时完成。用户代码不能保证这样做——如果一个线程卡在库中执行一个冗长的操作,你不能可靠地停止它。如果您尝试,它可能会锁定内存管理器。仅仅解除阻塞在 I/O 操作上的线程就已经足够困难了,通常需要像在本地网络堆栈上打开一个连接这样的障碍来强制提前对 return 进行 accept() 调用。

4) OS 'terminate process' API 以及其中涉及的所有内容都经过了大量测试。它有效,并且随 OS 免费提供。您尝试停止线程和释放内存的用户代码永远不会积累那么多测试。

5) 试图停止线程和释放内存的用户代码是多余的 - OS 做同样的工作,只是更好、更快、更可靠。您正在尝试从 OS 将很快销毁并重新分配的子分配器中清理内存。

6) 许多 OS 和其他商业图书馆已经让位于不可避免的事情,并承认他们无法在关闭时安全地释放所有内存而不引起问题,尤其是对于多线程应用程序。库作者不能可靠地做到这一点,你也不能。

当然,在 运行 期间以可控和明智的方式管理您的内存,根据需要释放您的 malloc,以免在进程的生命周期内泄漏内存。

但是,如果您遇到致命错误,可能会尝试将详细信息写入日志文件或执行其他一些 debugging/logging 操作,如果可以的话,然后调用您的 OS 'terminate process' API.

除此之外,您别无他法。

您的应用即将死亡,让 OS 安乐死它。

现有答案涵盖了各种malloc/free场景,所以我不会添加。

我想指出的是,如果您使 malloc 失败,则程序几乎就结束了,通常只是 exit() 比尝试恢复情况更好.

即使您设法清理部分分配的结构、其他代码和该代码使用的库,也将无法分配内存,并且可能(而且经常会)以神秘的方式失败。

malloc() 通常仅当您 运行 在严格受限的环境中(如嵌入式微控制器)或如果您像筛子一样泄漏内存时才会失败。