我在讲座中学习到,对指针调用free()
两次非常不好。我知道释放完指针后,将其设置为NULL
是一个好习惯。
然而,我从未听说过任何解释为什么这样做。据我所知,malloc()
的工作方式应该在技术上跟踪它已分配并提供给您使用的指针。那么为什么它不知道通过free()
接收到的指针是否已释放?
我很想了解在先前已释放的位置上调用free()
时会发生什么内部情况。
我在讲座中学习到,对指针调用free()
两次非常不好。我知道释放完指针后,将其设置为NULL
是一个好习惯。
然而,我从未听说过任何解释为什么这样做。据我所知,malloc()
的工作方式应该在技术上跟踪它已分配并提供给您使用的指针。那么为什么它不知道通过free()
接收到的指针是否已释放?
我很想了解在先前已释放的位置上调用free()
时会发生什么内部情况。
malloc
时,你告诉计算机你想要在堆上保留一些内存位置,专为你使用。计算机返回指向该地址空间第一个字节的指针。free
时,实际上是在告诉计算机你不再需要该空间,所以它将这个空间标记为可用于其他数据。malloc
调用返回。当你第二次调用free
时,你不是释放之前的数据,而是新的数据,这可能不利于你的程序 ;)回答你的第一个问题,
那么为什么它不知道,通过
free()
接收到的指针是否已被释放?
因为,在 C 标准中,malloc()
的规范并没有要求这样做。当您调用 malloc()
或其相关函数时,它会返回一个指针,并在内部存储分配的内存位置的大小 在 该指针中。这就是为什么 free()
不需要大小来清理内存的原因。
此外,一旦使用 free()
进行释放,实际上分配的内存发生了什么仍然取决于实现。调用 free()
只是一个标记,指出分配的内存不再被进程使用,可以在需要时重新获取和重新分配。因此,在那个时候跟踪分配的指针是非常不必要的。这将对操作系统保留所有回溯记录是一个不必要的负担。
然而,为了调试目的,一些库实现可以帮你完成这项工作,比如DUMA或dmalloc,最后但并非最不重要的是来自Valgrind的memcheck工具。
从技术上讲,C
标准没有指定在已经释放的指针上调用free()
会发生什么行为。这是未定义的行为。
C11
,第§7.22.3.3章,free()
函数
[...] 如果参数与先前由内存管理函数返回的指针不匹配,或者如果空间已被
free()
或realloc()
调用释放,则行为未定义。
malloc
及其相关函数返回的指针调用free
两次会导致未定义行为,但并没有进一步解释原因。重复释放同一块内存
要理解这种错误可能会引起什么问题,我们应该记住内存管理器通常的工作方式。通常情况下,它会在内存中的块之前存储已分配块的大小。如果我们释放了内存,那么这个内存块可能被另一个
malloc()
请求重新分配,因此这个重复释放实际上会释放错误的内存块 - 导致我们在应用程序的其他地方拥有悬空指针。这些错误往往比它们发生的代码位置晚得多。有时我们根本看不到它们,但它们仍然潜伏着,等待机会露出丑陋的头。另一个可能发生的问题是,在将已释放的块与相邻的空闲块合并以形成较大的空闲块,然后重新分配较大的块之后,我们尝试第二次
free()
我们的块时,我们实际上只释放了应用程序当前使用的内存块的一部分。这将引起更多意外的问题。
malloc
时,你得到一个指针。运行库需要跟踪malloc
的内存。通常malloc
不会将内存管理结构与malloc
的内存分开存储,而是在同一个地方。所以对于x字节的malloc
实际上需要x+n字节,其中一种可能的布局是前n个字节包含链表结构,指向下一个(和前一个)已分配的内存块。free
一个指针时,函数free
会遍历其内部内存管理结构,并检查传递给它的指针是否是已经malloc
的有效指针。只有这样它才能访问内存块中隐藏的部分。但是这样做的检查将非常耗时,特别是如果你分配了很多内存。因此,free
简单地假定你传递了一个有效指针。这意味着它直接访问内存块的隐藏部分,并假定那里的链表指针是有效的。free
一个内存块,那么你可能会遇到这样的问题:有人进行了新的malloc
,得到了你刚释放的内存,覆盖它,第二次free
从其中读取无效的指针。free
的指针设置为NULL
是一个好习惯,因为它有助于调试。如果你访问了已free
的内存,你的程序可能会崩溃,但它也可能只是读取可疑的值,然后可能在稍后崩溃。那么找到根本原因可能很困难。如果你将已释放的指针设置为NULL
,当你尝试访问该内存时,你的程序将立即崩溃。这在调试期间非常有帮助。
malloc
和free
管理的堆内存看作是一池水。当你使用malloc
分配内存时,你会得到一勺水,你可以随意处理它。当你释放内存时,这勺水就会倒回池中,你会失去哪些内存是你自己的跟踪,内存管理器也是如此。释放内存只是清空了你的勺子,但你仍然保留着实际的勺子(指针)。 - Some programmer dude