我有点困惑于如何检查内存分配是否失败,以防止由取消引用的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
返回NULL
,则createNextNode
也返回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失败,立即终止程序会是错误的吗?我读到这对多线程程序来说是有问题的,而且在某些情况下,程序可能能够在没有任何进一步内存分配的情况下继续运行,所以在任何可能的情况下都将其视为致命错误是错误的。这是对的吗?
很抱歉发了这么长的帖子,谢谢你的帮助!
这取决于更广泛的情况。对于某些程序来说,简单地中止是正确的做法
对于某些应用程序,正确的做法是收缩缓存并再次尝试malloc
。对于一些多线程程序,只需等待(给其他线程一个释放内存的机会(并重试即可。
对于需要高度可靠的应用程序,您需要一个应用程序级解决方案。我使用过并经过战斗测试的一个解决方案是:
- 在启动时分配一个紧急内存池
- 如果
malloc
出现故障,请释放一些应急池 - 对于无法正常处理
NULL
响应的调用,请休眠并重试 - 有一个服务线程尝试重新填充应急池
- 让使用缓存的代码通过减少内存消耗来响应未满的应急池
- 如果你有能力减轻负载,例如,通过将负载转移到其他实例,那么在应急池未满的情况下就这样做
- 对于需要分配大量内存的任意操作,请检查应急池的级别,如果未满或接近该级别,则不要执行该操作
- 如果应急池变空,则中止
如何处理malloc失败并返回NULL?
考虑代码是否是一组辅助函数/库或应用程序。
终止的决定最好由更高级别的代码来处理。
示例:除了exit()
、abort()
和朋友之外,标准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 %un", __func__, __LINE__);
exit(EXIT_FAILURE);
}
不退出示例:
考虑一个例程,该例程多次使用LinkedList
将文件读取到数据结构中,但该文件在某种程度上已损坏,导致内存分配过多。代码可能希望简单地释放该文件的所有内容(但仅限于该文件(,并简单地向用户报告"无效文件/内存不足",然后继续运行。
if (addAllToLinkedList(self, ptrs)) {
free_file_to_struct_resouces(handle);
return oops;
}
...
return success;
带走
低级例程以某种方式指示错误。如果需要,高级例程可以退出代码。