C 中的指针和内存泄漏
介绍
询问过很多使用 C 的开发者,关于 C 中最困扰他们的是什么?他们中的许多人可能会回答指针和内存泄漏。这些确实是开发人员消耗大部分调试时间的项目。指针和内存泄漏对某些程序员来说似乎是一种威慑,但是,一旦你了解了指针和相关内存操作的基础知识,它们将成为你在 C 中拥有的最强大的工具。
本文分享了开发人员在开始使用指针编程之前应该知道的秘密。文章涵盖:
- 什么类型的指针操作会导致内存损坏
- 使用动态内存分配时必须考虑的检查点
- 导致内存泄漏的场景
如果你事先知道什么可能出错,那么你就可以小心避免陷阱并摆脱大多数指针和与内存相关的问题。
会出什么问题?
在构建完成后,可能会出现多种有问题的场景,这些场景可能会导致问题。在使用指针时,你可以使用本文中的信息来避免许多问题。
未初始化的内存
在这个例子2中,p已经分配了 10 个字节。这 10 个字节可能包含垃圾数据,如图 1所示。
char ∗p = malloc ( 10 );
图 1. 垃圾数据
如果一个代码段p在一个值被分配给它之前试图访问它,它可能会得到那个垃圾值,你的程序可能会表现得很神秘。p可能具有您的程序从未预料到的值。
一个好的做法是始终使用memsetwithmalloc
或始终使用calloc
。
char ∗p = malloc (10);
memset(p,’\0’,10);
现在,即使相同的代码段p在一个值被分配给它之前尝试访问,并且它对Null值进行了正确的处理(理想情况下应该是这样),那么它的行为也会正常。
内存覆盖
由于p已经分配了 10 个字节,如果某个代码片段试图将一个值写入p11 个字节,那么该操作将在不告诉你的情况下悄悄地从其他位置吃掉一个字节。让我们假设指针q代表这个内存。
图 2. q 的原始内容
图 3. q 的覆盖内容
结果,指针q将包含从未预料到的内容。即使你的模块编码良好,它也可能由于共存模块执行一些内存覆盖而导致行为不正确。下面的示例代码片段也可以解释这种情况。
char ∗name = (char ∗) malloc(11);
// Assign some value to name
memcpy ( p,name,11); // Problem begins here
在此示例中,memcpy操作试图将 11 个字节写入p,而它仅分配了 10 个字节。
一个好的做法是,每当向指针写入值时,请确保交叉检查可用字节数和正在写入的字节数。通常,该memcpy函数将是一个检查点。
内存溢出
内存过读是指正在读取的字节数超过预期的字节数。这不是太严重,所以我不会详述。下面的代码给出了一个例子。
char ∗ptr = (char ∗)malloc(10);
char name[20] ;
memcpy ( name,ptr,20); // Problem begins here
在这个例子中,memcpy操作试图从 中读取 20 个字节ptr,但它只分配了 10 个字节。这也将导致不希望的输出。
内存泄漏
内存泄漏真的很烦人。下面的列表描述了一些导致内存泄漏的场景。
- 重新分配 我将用一个例子来解释重新分配。
char ∗memoryArea = malloc(10); char ∗newArea = malloc(10);
展示更多这将值分配给下面图 4中所示的内存位置。
图 4. 内存位置
memoryArea并且newArea每个都分配了10个字节,它们各自的内容如图4所示。如果有人执行如下所示的语句(指针重新分配)——???
memoryArea = newArea;
那么它肯定会让你在这个模块开发的后期阶段陷入困境。在上面的代码语句中,开发人员已经将memoryArea指针分配给了newArea指针。结果,memoryArea之前指向的内存位置变成了孤立的,如下面的图 5所示。它不能被释放,因为没有对这个位置的引用。这将导致 10 个字节的内存泄漏。图 5. 内存泄漏
在分配指针之前,请确保内存位置不会成为孤立的。
- 首先释放父块 假设有一个指向memoryArea10 字节内存位置的指针。该内存位置的第三个字节进一步指向其他一些动态分配的 10 字节内存位置,如图 6所示。
图 6. 动态分配的内存
free(memoryArea)
如果memoryArea通过调用 free 被释放,那么newArea指针也将变为无效。newArea无法释放所指向的内存位置,因为没有指向该位置的指针。换句话说,指向的内存位置newArea成为孤儿并导致内存泄漏。每当释放结构化元素时,它又包含指向动态分配的内存位置的指针,首先遍历子内存位置(newArea在示例中)并从那里开始释放,遍历回父节点。这里的正确实现将是:
free( memoryArea‑>newArea);
free(memoryArea);
- 返回值处理不当有时,某些函数返回对动态分配内存的引用。calling跟踪此内存位置并正确处理它成为函数的责任。
char ∗func ( )
{
return malloc(20); // make sure to memset this location to â\0ââ¦
}
void callingFunc ( )
{
func ( ); // Problem lies here
}
在上面的例子中,func()函数内部对callingFunc()函数的调用并没有处理内存位置的返回地址。结果,该func()函数分配的 20 字节块丢失并导致内存泄漏。
回馈你所获得的
在开发组件时,可能会有很多动态内存分配。你可能忘记跟踪所有指针(指向这些内存位置),并且某些内存段没有被释放并一直分配给程序。
始终跟踪所有内存分配,并在适当的时候释放它们。事实上,可以开发一种机制来跟踪这些分配,例如在链接列表节点本身中保留一个计数器(但你还必须考虑这种机制的额外开销!)。
访问空指针
访问空指针非常危险,因为它可能会你的程序崩溃。始终确保你没有访问空指针。
总结
本文讨论了在使用动态内存分配时可以避免的几个陷阱。为了避免与内存相关的问题,好的做法是:
- 始终memset与 malloc 一起使用,或始终使用calloc.
- 每当向指针写入值时,请确保交叉检查可用字节数和正在写入的字节数。
- 在分配指针之前,确保没有内存位置成为孤立的。
- 每当释放结构化元素(它又包含指向动态分配的内存位置的指针)时,首先遍历子内存位置并从那里开始释放,再遍历回父节点。
- 始终正确处理返回动态分配内存引用的函数的返回值。
- 每一个都有对应的free malloc。
- 确保你没有访问空指针。