为什么有时候malloc无法正常工作?

21

我正在将一个C语言项目从Linux移植到Windows。在Linux上它非常稳定。在Windows上,大多数时间都能正常工作,但有时会出现分段错误。

我使用Microsoft Visual Studio 2010进行编译和调试,似乎有时我的malloc调用根本没有分配内存,返回NULL。机器上有空闲内存;代码已经执行过一千次了,但它仍然在不同的位置发生。

就像我说的,这种情况不是每次都发生或者发生在同一个位置;看起来是一个随机错误。

在Windows上,有哪些需要我特别注意的地方?我可能做错了什么?


1
一个问题似乎是你没有考虑到可能返回一个空指针。你在Linux上是否使用valgrind运行程序了? - Jonathan Leffler
2
可能有多个原因 - 这里有一些相关的线程:https://dev59.com/_0nSa4cB1Zd3GeqPSO7x - wkl
1
可能存在空闲内存,但它可能已经碎片化,以至于操作系统无法返回您所请求的大小的块。Windows / Linux管理内存的方式不同。 - Adrian Cornish
2
你是将它编译成32位二进制文件,然后尝试使用超过4GB吗? - cababunga
1
@cababunga:在32位进程上,你甚至无法获得4GB的内存。 - Steve Jessop
显示剩余3条评论
4个回答

36

malloc()在无法满足内存请求时会返回一个无效的NULL指针。在大多数情况下,C语言的内存分配例程通过调用操作系统来分配更多的内存块或堆来管理可用内存列表或堆,以便在发生malloc()调用且列表或堆上没有块可以满足请求时使用。

因此,malloc()失败的第一种情况是由于(1) C运行时的可用内存列表或堆中没有可用的内存块,以及(2)当C运行时内存管理从操作系统请求更多内存时,请求被拒绝而不能满足内存请求。

这里有一篇关于指针分配策略的文章。

这篇论坛文章给出了由于内存碎片化而导致malloc失败的例子

malloc()可能失败的另一个原因是内存管理数据结构已经损坏,可能是由于缓冲区溢出,其中分配的内存区域用于比分配的内存大小更大的对象。不同版本的malloc()可以使用不同的内存管理策略,并确定在调用malloc()时提供多少内存。例如,malloc()可能会精确地给你所请求的字节数,或者它可能会给你更多以适应分配的块在内存边界内或使内存管理更容易。

使用现代操作系统和虚拟内存,除非您正在进行某些真正大型的内存常驻存储,否则很难耗尽内存。然而,正如用户Yeow_Meng在下面的评论中提到的那样,如果您进行算术计算以确定要分配的大小,并且结果是负数,则可能会请求大量的内存,因为用于分配内存的参数是无符号的。

当进行指针算术运算以确定需要多少空间来存储一些数据时,您可能会遇到负大小的问题。这种错误通常发生在意外的文本解析中。例如,下面的代码将导致一个非常大的malloc()请求。

char pathText[64] = "./dir/prefix";  // a buffer of text with path using dot (.) for current dir
char *pFile = strrchr (pathText, '/');  // find last slash where the file name begins
char *pExt = strrchr (pathText, '.');    // looking for file extension 

// at this point the programmer expected that
//   - pFile points to the last slash in the path name
//   - pExt point to the dot (.) in the file extension or NULL
// however with this data we instead have the following pointers because rather than
// an absolute path, it is a relative path
//   - pFile points to the last slash in the path name
//   - pExt point to the first dot (.) in the path name as there is no file extension
// the result is that rather than a non-NULL pExt value being larger than pFile,
// it is instead smaller for this specific data.
char *pNameNoExt;
if (pExt) {  // this really should be if (pExt && pFile < pExt) {
    // extension specified so allocate space just for the name, no extension
    // allocate space for just the file name without the extension
    // since pExt is less than pFile, we get a negative value which then becomes
    // a really huge unsigned value.
    pNameNoExt = malloc ((pExt - pFile + 1) * sizeof(char));
} else {
    pNameNoExt = malloc ((strlen(pFile) + 1) * sizeof(char));
}
一个良好的运行时内存管理将尝试合并已释放的内存块,从而将许多较小的块合并为更大的块。这些内存块的合并减少了使用 C 内存管理运行时管理的内存列表或堆上已有的内容无法满足内存请求的可能性。您可以通过尽量重用已分配的内存并尽量减少对malloc()和free()的依赖来提高内存管理效率。如果不使用malloc()函数,则很难出现失败情况。同时,您还可以通过将许多小的malloc()调用更改为较少的大型malloc()调用来降低内存碎片化和扩展内存列表或堆的大小的机会。此外,如果能够在同一时间连续地进行malloc()和free(),则内存管理运行时就可以更容易地合并内存块。因此,您可以在调用malloc()时使用一些规则,例如将分配的内存块大小舍入到某个标准内存块的大小。许多脚本语言都采用类似的方法以增加重复调用malloc()和free()函数时能够匹配请求与内存列表或堆上的空闲块的机会。例如,您可以按块大小为16个字节来分配内存块,使用一个公式((size / 16) + 1) * 16或者更可能是((size >> 4) + 1) << 4。最后,下面是一个简单的例子,在这个例子中,我们试图减少分配和释放的块的数量,我们假设有一个可变大小的内存块的链接列表。
typedef struct __MyNodeStruct {
    struct __MyNodeStruct *pNext;
    unsigned char *pMegaBuffer;
} MyNodeStruct;

对于特定缓冲区和其节点的内存分配,有两种方式。第一种是标准分配节点后按以下方式分配缓冲区。

MyNodeStruct *pNewNode = malloc(sizeof(MyNodeStruct));
if (pNewNode)
    pNewNode->pMegaBuffer = malloc(15000);

不过另一种方法是采用以下方式,它使用单个内存分配和指针算术运算,以便单个malloc()提供两个内存区域。

MyNodeStruct *pNewNode = malloc(sizeof(myNodeStruct) + 15000);
if (pNewNode)
    pNewNode->pMegaBuffer = ((unsigned char *)pNewNode) + sizeof(myNodeStruct);

然而,如果您正在使用这种单一的分配方法,您需要确保在使用指针pMegaBuffer时保持一致,不要意外地对其进行free()操作。如果您需要用更大的缓冲区更换缓冲区,则需要释放节点并重新分配缓冲区和节点。因此,程序员需要进行更多的工作。


第二种分配策略更好,因为你将拥有一个连续的内存块,大小为(sizeof(myNodeStruct) + 15000),然后你可以相应地正确调整pMegaBuffer指针。第一种策略因为各种原因而不被推荐使用。 - Jay D
谢谢您的回答。现在我更好地理解了内存分配的工作原理。 - Pedro Alves
2
malloc(-1)或其他负数可能会失败,因为malloc采用的是无符号数据类型size_t。因此,您请求的大小会被隐式转换为正数据类型。隐式转换可能会产生非常大的正数,而您对malloc的调用会失败,因为操作系统不能(或不会)给您那么多内存。 - Yeow_Meng
@Yeow_Meng 感谢您提供了一个关于 malloc() 大小意外变得非常大的具体示例。我增加了一个编程错误的插图,说明了这一点。 - Richard Chambers

6
< p >在Windows上导致malloc()失败的另一个原因是,您的代码在一个DLL中分配内存,在另一个DLL或EXE中释放内存。

与Linux不同,在Windows上,一个DLL或EXE有自己的运行时库链接。这意味着您可以使用2013 CRT将您的程序链接到针对2008 CRT编译的DLL。

不同的运行时库可能会以不同的方式处理堆。Debug和Release CRTs一定会以不同的方式处理堆。如果您在Debug中使用malloc(),而在Release中使用free(),那么代码将会出现严重错误,并且这可能是导致您问题的原因。


-4

我曾经看到过 malloc 失败的情况,因为指向新内存的指针本身并没有被分配:

pNewNode = malloc(sizeof(myNodeStruct) + 15000);

如果由于某种原因pNewNode需要事先创建或分配,则它是无效的且malloc将失败,因为无法将malloc分配的结果(它本身是成功的)存储在指针中。当存在此错误时,我看到同一程序多次运行时,在某些情况下代码将正常工作(当指针意外存在但仅仅是出于运气),但在许多情况下,它将指向未分配任何内存空间的位置,因为它从未被分配。

如何找到这个bug?在调试器中查看调用malloc之前pNewNode是否有效。它应该指向0x000000或其他实际位置(直到malloc分配一个实际分配的内存段,该位置实际上是垃圾)。


1
malloc 不关心 pNewNode 中的内容(地址)。在 pNewNode 被赋予返回值时,malloc 已经返回了。很可能您误解了其他问题。可能您正在使用 C++,pNewNode 是类的成员,并且您在使用指针调用此代码时指针本身无效。或者 pNewNode 是一个局部变量,在调用之前您的堆栈已被破坏。 - codenheim

-23

你可以基于递归函数声明自己的安全 malloc:

void *malloc_safe(size_t size)
{
    void* ptr = malloc(size);
    if(ptr == NULL)
        return malloc_safe(size); 
    else
        return ptr;
}

如果malloc失败,此函数将再次调用并尝试分配内存,同时ptr变为!= NULL。
使用:
int *some_ptr = (int *)malloc_safe(sizeof(int));

17
我知道递归……但那不是我的重点。如果一个分配失败,因为你的内存已经用尽了,再次调用分配(递归)是毫无帮助的。 - Jeff Mercado
7
此外,如果您的堆内存不足,递归函数还将占用当前线程堆栈保留的所有内存,可能导致进程崩溃。 - mity
4
糟糕的回答。我们不应该在malloc失败时崩溃,而应该在栈溢出时崩溃并进入无用的追踪过程。如果可能的话,我会给予-10的评分,以免有人尝试这种方法。为什么人们不删除带有严重错误的危险回答呢? - codenheim
6
一个简单的while循环会表现出相同的无用行为,但不会有栈溢出的危险。 - Keith Thompson
5
这个回答真的很有趣。我喜欢它。 - Chris Watts
显示剩余3条评论

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