如何处理malloc失败并返回NULL?

4

我有些困惑如何检查内存分配是否失败,以防止解引用NULL指针引起的未定义行为。我知道malloc(和类似的函数)可能会失败并返回NULL,因此必须在继续程序的其余部分之前始终检查返回的地址。我不理解的是如何处理这种情况的最佳方式。换句话说,当malloc调用返回NULL时,程序应该怎么做?

我正在开发的一个双向链表实现时,遇到了这个问题。

struct ListNode {

    struct ListNode* previous;
    struct ListNode* next;
    void* object;
};

struct ListNode* newListNode(void* object) {

    struct ListNode* self = malloc(sizeof(*self));

    if(self != NULL) {

        self->next = NULL;
        self->previous = NULL;
        self->object = object;
    }

    return self;
}

只有节点的指针正确分配了,节点才能被初始化。如果没有正确分配,该构造函数将返回NULL

我还编写了一个函数,它从已存在的节点开始创建一个新节点(调用newListNode函数),然后返回它。

struct ListNode* createNextNode(struct ListNode* self, void* object) {

    struct ListNode* newNext = newListNode(object);

    if(newNext != NULL) {

        newNext->previous = self;

        struct ListNode* oldNext = self->next;

        self->next = newNext;

        if(oldNext != NULL) {

            newNext->next = oldNext;
            oldNext->previous = self->next;
        }
    }

    return newNext;
}

如果newListNode返回NULLcreateNextNode也返回NULL,并且传递给该函数的节点不会受到影响。然后,ListNode结构用于实现实际的链表。
struct LinkedList {

    struct ListNode* first;
    struct ListNode* last;
    unsigned int length;
};

_Bool addToLinkedList(struct LinkedList* self, void* object) {

    struct ListNode* newNode;

    if(self->length == 0) {

        newNode = newListNode(object);
        self->first = newNode;
    }
    else {

        newNode = createNextNode(self->last, object);
    }

    if(newNode != NULL) {

        self->last = newNode;
        self->length++;
    }

    return newNode != NULL;
}

如果创建新节点失败,则 addToLinkedList 函数返回 0,而链表本身保持不变。
最后,让我们考虑这个将链表中所有元素添加到另一个链表的函数。
void addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {

    struct ListNode* node = other->first;

    while(node != NULL) {

        addToLinkedList(self, node->object);
        node = node->next;
    }
}

如何处理addToLinkedList可能返回0的情况?据我所知,当无法再分配内存时,malloc会失败,因此我认为在分配失败后进行的后续调用也会失败,这样理解对吗?所以,如果返回0,由于无法添加任何新元素到列表中,循环是否应该立即停止?
另外,按照我现在的写法,把所有这些检查一个接一个地堆叠在一起,这样做是正确的吗?这不是多余的吗?如果malloc失败就立即终止程序,这样做是否有问题?我读到过这样做会对多线程程序产生问题,并且在某些情况下,程序可能能够继续运行而无需进一步分配内存,因此将其视为致命错误在任何情况下都是不正确的。这样做是对的吗?
对于这篇很长的文章,非常抱歉,谢谢你的帮助!

4
没有一个通用的答案 - 这取决于程序的更大上下文。如果在不分配这个内存的情况下,你的整个程序还有其他有用的工作可以做吗?如果是,那就去做。如果没有,就退出。 - Nate Eldredge
这也在某种程度上取决于平台。 如果在Windows或Linux等平台上,malloc返回NULL,那么很可能您的程序出现了严重错误,几乎始终是致命错误。 - Jabberwocky
一旦malloc返回NULL,后续的调用是否也会返回NULL?这取决于情况。答案大多数情况下是“是”。很难给出一个普遍的答案。 - Jabberwocky
@GovindParmar 当然,有许多可能的情况。 - Jabberwocky
1
实际上,你应该把可用内存看作是汽车中的油。你永远不想用完它。你总是需要一些储备,当你用完时,大多数情况下你就没有运气了。 - Jabberwocky
显示剩余2条评论
2个回答

6

这取决于更广泛的情况。对于某些程序,仅中止是正确的做法。

对于一些应用程序,正确的做法是缩小缓存并再次尝试 malloc。对于一些多线程程序,等待(以便给其他线程释放内存的机会)并重试将起作用。

对于需要高度可靠性的应用程序,您需要一个应用级解决方案。我使用过并经过实战测试的一个解决方案如下:

  1. 在启动时分配一个紧急内存池。
  2. 如果 malloc 失败,请释放一些紧急内存池。
  3. 对于无法合理处理 NULL 响应的调用,请睡眠并重试。
  4. 有一个服务线程尝试重新填充紧急内存池。
  5. 使用缓存的代码通过减少内存消耗来响应非满紧急内存池。
  6. 如果紧急内存池未满,请将负载转移到其他实例来卸载负载。
  7. 对于需要分配大量内存的自由裁量操作,请检查紧急内存池的水平,如果未满或接近满,请勿执行该操作。
  8. 如果紧急内存池变空,则中止。

1
关于(3),请确保在代码放弃之前有重试和休眠的限制,并制定一个关于重试次数用尽时发生什么的策略。考虑延迟时间的长短 - 什么是可以接受的?在文字处理器中,一秒钟可能还可以,但在 SpaceX Falcon 9 火箭的发射控制中可能就不行了。 - Jonathan Leffler
1
非常好的回答,尽管对于初学者来说有些高级。 - Jabberwocky
1
@JonathanLeffler 当每个内存分配点都存在内存不足的情况时,处理起来确实非常困难。在大多数情况下,你只能永远重试或者终止进程。 - David Schwartz
3
“无限重试”让我感到担忧 - 视情况而定。 - Jonathan Leffler
1
@JonathanLeffler 那段代码将不断消耗紧急池(每 2)。最终,要么其他机制允许成功分配,要么会启用 8。无法识别代码中的每个内存分配位置并合理地处理故障。 - David Schwartz
显示剩余3条评论

2
如何处理malloc失败并返回NULL?
考虑代码是帮助函数/库还是应用程序。
最好由更高级别的代码决定是否终止。
例如:除了exit(),abort()和friends之外,标准C库不会退出。
同样,对于OP的低级功能集,返回错误代码/值是一个合理的解决方案。即使对于addAllToLinkedList(),我也会考虑在返回代码中传播错误。(非零是某些错误。)
// void addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {
int addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {
  ...
  if (addToLinkedList(self, node->object) == NULL) {
    // Do some house-keepeing (undo prior allocations)
    return -1;
  }

对于高级应用程序,请按照您的设计进行操作。目前,退出并显示失败消息可能足够简单。

if (addAllToLinkedList(self, ptrs)) {
  fprintf(stderr, "Linked List failure in %s %u\n", __func__, __LINE__);
  exit(EXIT_FAILURE);
}

没有退出的例子:

考虑一种将文件读入具有许多使用LinkedList的数据结构的程序,而该文件在某种程度上已损坏,导致过多的内存分配。代码可能只想简单地释放该文件所用的所有内容(但仅限于该文件),然后向用户报告“无效的文件/内存不足”-并继续运行。

if (addAllToLinkedList(self, ptrs)) {
  free_file_to_struct_resouces(handle);
  return oops;
}
...
return success;

要点

低级别的例程会以某种方式指示错误。而高级别的例程可以选择退出代码。


那我应该编写使用malloc等函数的功能,以便始终可以检查调用它们时是否出现了问题?这样做可以让任何想在自己的程序中使用这些函数的人根据情况决定是继续传播错误还是立即终止执行。 - Gian
具体来说,如果其中一个函数用于构建另一个辅助/库函数,则程序员应该以某种方式继续发出错误信号,而在管理实际程序执行的代码中,失败时仅终止可能是明智的(或者以某种方式处理它并显示错误消息,就像您在上一个示例中所做的那样)。我理解得对吗? - Gian
1
基本上,这是一种更加灵活的方法。我的意思是,malloc本身可以被设计成在失败时总是终止程序(我认为甚至有一个替代函数可以这样做),但它不这样做给用户提供了选项,根据上下文以最合适的方式处理这些情况。 - Gian
1
@Gian 同意所有三点。将代码想象成层次结构。越靠近底部,越不希望使用 exit() 来编写可移植的代码。如果我使用了一个工具集/库,它可以做很多好事,但在某些情况下会 exit(),我可能会放弃这个本来不错的包。是的,在 C 中处理错误确实很麻烦。 - chux - Reinstate Monica
考虑到我最近学习(我还在大学)关于异常的内容,这实际上很有道理(异常在较低级别抛出,在较高级别处理)。这实际上让我更加欣赏它们。现在我真正明白它们为什么如此方便:基本上,它们自动化了向上传播错误的过程。 - Gian
1
@Gian 错误处理是一个深入的话题,有许多不同的观点。有时错误处理可能是可忽略的,程序需要尽快发布。但在其他情况下错误处理则至关重要。对我而言,错误处理是代码设计中实用但十分重要的因素 - 如何高效地检测/处理错误可以引导算法选择。这就是编程的工程化:不是过度关注错误,而是足够重视。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接