动态数据结构:Malloc 和 Free
假设您希望在应用程序执行期间分配一定量的内存。您可以随时调用 malloc 函数,它会从堆中请求一个内存块。操作系统会为您的程序保留一个内存块,您可以随意使用它。当您使用完该内存块后,通过调用 free 函数将其返回给操作系统以供回收。然后,其他应用程序可以在稍后将其保留供自己使用。
例如,以下代码展示了堆最简单的用法
广告
int main() { int *p; p = (int *)malloc(sizeof(int)); if (p == 0) { printf("ERROR: Out of memory\n"); return 1; } *p = 5; printf("%d\n", *p); free(p); return 0; }
程序中的第一行调用 malloc 函数。该函数执行以下三项操作
- malloc 语句首先查看堆上可用的内存量,然后询问:“是否有足够的内存来分配所需大小的内存块?”内存块所需的大小是从传递给 malloc 的参数中得知的——在本例中,sizeof(int) 是 4 字节。如果没有足够的可用内存,malloc 函数将返回地址零以指示错误(零的另一个名称是 NULL,您会在 C 代码中随处可见)。否则 malloc 将继续执行。
- 如果堆上有内存可用,系统将从堆中“分配”或“保留”指定大小的内存块。系统保留该内存块,以防止它被多个 malloc 语句意外使用。
- 然后系统将保留块的地址放入指针变量(在本例中为 p)中。指针变量本身包含一个地址。分配的块能够保存指定类型的值,并且指针指向它。
程序接下来检查指针 p,以确保分配请求成功,检查行是 if (p == 0)(也可以写成 if (p == NULL) 甚至 if (!p))。如果分配失败(如果 p 为零),程序将终止。如果分配成功,程序将初始化该块的值为 5,打印出该值,并在程序终止前调用 free 函数将内存返回给堆。
这段代码与之前将 p 设置为现有整数 i 的地址的代码之间没有实质区别。唯一的区别在于,对于变量 i,内存作为程序预分配内存空间的一部分而存在,并有两个名称:i 和 *p。而对于从堆中分配的内存,该内存块只有一个名称 *p,并在程序执行期间分配。两个常见问题
- 每次分配后检查指针是否为零真的很重要吗? 是的。由于堆的大小会根据正在运行的程序、它们分配了多少内存等因素而不断变化,因此不能保证对 malloc 的调用一定会成功。在每次调用 malloc 后都应该检查指针以确保其有效。
- 如果我在程序终止前忘记删除内存块会发生什么? 当程序终止时,操作系统会“清理其遗留物”,释放其可执行代码空间、栈、全局内存空间以及任何堆分配的内存以供回收。因此,在程序终止时保留未释放的分配不会产生长期后果。然而,这被认为是一种不良做法,并且在程序执行期间出现的“内存泄漏”是有害的,如下所述。
以下两个程序展示了指针的两种不同有效用法,并试图区分指针本身的使用和指针所指值的使用
void main() { int *p, *q; p = (int *)malloc(sizeof(int)); q = p; *p = 10; printf("%d\n", *q); *q = 20; printf("%d\n", *q); }
这段代码的最终输出将是第 4 行的 10 和第 6 行的 20。
以下代码略有不同
void main() { int *p, *q; p = (int *)malloc(sizeof(int)); q = (int *)malloc(sizeof(int)); *p = 10; *q = 20; *p = *q; printf("%d\n", *p); }
这段代码的最终输出将是第 6 行的 20。
请注意,编译器将允许 *p = *q,因为 *p 和 *q 都是整数。此语句表示:“将 q 指向的整数值移动到 p 指向的整数值中。”该语句移动的是值。编译器也允许 p = q,因为 p 和 q 都是指针,并且都指向相同的类型(如果 s 是指向字符的指针,则不允许 p = s,因为它们指向不同的类型)。语句 p = q 表示:“让 p 指向 q 所指向的同一内存块。”换句话说,q 所指向的地址被移动到 p 中,因此它们都指向同一个内存块。此语句移动的是地址。
从所有这些例子中可以看出,初始化指针有四种不同的方式。当声明一个指针时,如 int *p,它在程序中处于未初始化状态。它可能指向任何地方,因此对其进行解引用是错误的。指针变量的初始化涉及将其指向内存中已知的位置。
- 一种方法,如前所述,是使用 malloc 语句。此语句从堆中分配一个内存块,然后将指针指向该内存块。这会初始化指针,因为它现在指向一个已知位置。指针之所以被初始化,是因为它已被填充了一个有效地址——新内存块的地址。
- 第二种方式,正如刚才所见,是使用 p = q 这样的语句,使 p 指向与 q 相同的位置。如果 q 指向一个有效内存块,那么 p 就被初始化了。指针 p 被加载了 q 所包含的有效地址。然而,如果 q 未初始化或无效,p 也将获得相同的无用地址。
- 第三种方式是将指针指向一个已知地址,例如全局变量的地址。例如,如果 i 是一个整数,p 是一个指向整数的指针,那么语句 p=&i 通过将 p 指向 i 来初始化 p。
- 初始化指针的第四种方式是使用零值。零是与指针一起使用的特殊值,如下所示:p = 0; 或:p = NULL; 这样做在物理上是将零放入 p 中。指针 p 的地址为零。通常将其绘制为
任何指针都可以设置为指向零。然而,当 p 指向零时,它并不指向一个内存块。指针仅仅包含地址零,而这个值作为标记很有用。您可以在以下语句中使用它,例如:
if (p == 0){ ...}
或
while (p != 0){ ...}
系统也识别零值,如果您碰巧解引用了一个零指针,系统将生成错误消息。例如,在以下代码中
p = 0;*p = 5;
程序通常会崩溃。指针 p 不指向内存块,它指向零,因此不能将值赋给 *p。当我们讲到链表时,零指针将用作一个标志。
malloc 命令用于分配内存块。当不再需要内存块时,也可以将其解除分配。当内存块被解除分配时,它可以被后续的 malloc 命令重复使用,从而允许系统回收内存。用于解除分配内存的命令称为 free,它接受一个指针作为参数。free 命令执行两项操作:
- 指针指向的内存块被取消保留,并返回到堆上的空闲内存中。随后可以被未来的 new 语句重新使用。
- 指针处于未初始化状态,在使用前必须重新初始化。
free 语句只是将指针返回到其原始的未初始化状态,并使内存块在堆上再次可用。
以下示例展示了如何使用堆。它分配一个整数块,填充它,写入它,然后释放它
#include <stdio.h> int main() { int *p; p = (int *)malloc (sizeof(int)); *p=10; printf("%d\n",*p); free(p); return 0; }
这段代码实际上只用于演示 C 语言中内存块的分配、解除分配和使用过程。malloc 这一行分配了指定大小的内存块——在本例中为 sizeof(int) 字节(4 字节)。C 语言中的 sizeof 命令返回任何类型的大小(以字节为单位)。这段代码也可以简单地写成 malloc(4),因为在大多数机器上 sizeof(int) 等于 4 字节。然而,使用 sizeof 会使代码更具可移植性和可读性。
malloc 函数返回一个指向已分配内存块的指针。这个指针是泛型的。不进行类型转换而直接使用该指针通常会导致编译器发出类型警告。(int *) 类型转换将 malloc 返回的泛型指针转换为“指向整数的指针”,这正是 p 所期望的。C 语言中的 free 语句将内存块返回给堆以供重复使用。
第二个示例演示了与上一个示例相同的功能,但它使用了结构体而不是整数。在 C 语言中,代码如下所示
#include <stdio.h> struct rec { int i; float f; char c; }; int main() { struct rec *p; p=(struct rec *) malloc (sizeof(struct rec)); (*p).i=10; (*p).f=3.14; (*p).c='a'; printf("%d %f %c\n",(*p).i,(*p).f,(*p).c); free(p); return 0; }
请注意以下行
(*p).i=10;
许多人想知道为什么以下代码不起作用
*p.i=10;
答案与 C 语言中运算符的优先级有关。计算 5+3*4 的结果是 17,而不是 32,因为在大多数计算机语言中,* 运算符的优先级高于 +。在 C 语言中,. 运算符的优先级高于 *,因此使用括号强制正确的优先级。
大多数人厌倦了一直输入 (*p).i,因此 C 语言提供了一种简写符号。以下两个语句完全等效,但第二个更容易输入
(*p).i=10; p->i=10;
在阅读他人的代码时,您会更频繁地看到第二种写法。