5. 动态分配
-
静态分配(static allocation)
当声明一个全局变量时,编译器给在整个程序中持续使用的变量分配内存空间,这种分配方式称为静态分配,因为变量分配到了内存的固定位置。 -
自动分配(automatic allocation)
当在函数中声明一个局部变量时,给该变量分配的空间在系统栈中。调用函数时给变量分配内存空间,函数返回时释放该空间,这种分配方式称为自动分配。 -
动态分配(dynamic allocation)
能在需要新内存的时候得到内存,不需要内存时就显式释放这部分内存,这种在程序运行时获取新内存空间的过程称为动态分配。
当程序载入内存时,通常只占用可用空间的一部分。在大多数系统中,当程序需要更多内存时,可以将一些未使用的内存空间分配给程序。
例如,如果程序运行时,需要一个新的数组空间,可以预留部分未分配的内存,将其余的内存留到后面分配。
- 堆(heap)
程序可用的未分配的内存资源。
作为库接口stdlib.h
的一部分,ANSIC语言的环境提供了一些从堆中分配新的内存的函数。
最重要的一个函数为malloc
,它能分配一块固定大小的内存。
待分配的内存块的大小以字节为单位给出(一个字节是足够存放一个字符值的内存单元)。
例如,如果要分配10个字节的内存,可调用
malloc(10)
结果会返回一个指针,指向一个10字节大小的内存块。
为了使用新分配的空间,必须将malloc
的结果存放在一个指针变量内,以后就可以像使用数组一样使用该指针变量了。
5.1 Void* 类型
在C语言中,指针是具有类型的。
malloc
函数用于为调用函数需要的任何类型的值分配新的内存空间,所以必须返回一个未确定类型的“通用”指针。
在C语言中,通用指针类型是一个指向空类型void的指针,void也用于指示无返回值或参数列表为空的函数。
如果声明一个指向void类型的指针, 如
void *vp;
可以将任何类型的指针值存入该变量,但不允许用*运算符间接引用vp
。编译器不知道vp
的基本类型是什么,所以没有办法谈论vp
指向的值。
void*类型的用处在于,允许函数,特别是malloc
函数,返回以后由调用函数创建实际类型的通用指针。
malloc
函数返回类型为void*的指针值,表明其原型为:
void *malloc(int nBytes);
注意,这个函数原型的结果类型与指针变量的声明方法非常相似。
*表示结果是和函数名而非基本类型相联系的指针,尽管在概念上,结果是与基本类型而非函数名联系的。
ANSI C能在指向void的指针类型和指向基本类型的指针类型间自动进行转换。
例如, 如果声明字符指针cp
为:
char *cp;
可以用语句
cp = malloc(10);
将malloc
的结果直接赋给cp
。
也可以使用强制类型转换将malloc
返回的结果转换成指向字符的指针:
cp = (char*)malloc(10);
这样做的一部分原因是历史因素,另一部分原因是能使指针类型间的转换更清楚。
不管有没有明确使用强制类型转换,这条语句均可分配10个字节的新内存空间,并将第一个字节的地址存放在cp
中。
常见错误:
一定要分清过程原型“void f(…);”和函数原型“void *f(…);”,后者声明了一个返回通用指针的函数。
5.2 动态数组
从概念上讲,赋值语句
cp = (char*)malloc(10);
建立了如下内存配置:
变量cp
指向已经在堆内分配的连续10个字节。因为指针和数组在C语言中能自由地相互转换,所以变量类似于声明为一个含10个字符的数组。
- 动态数组(dynamic array)
分配在堆上并用指针变量引用的数组称为动态数组。
一般来说,分配一个动态数组包含以下步骤:
(1) 声明一个指针变量,用以保存数组基地址。
(2) 调用malloc
函数为数组中的元素分配内存。由于不同的数据类型要求不同大小的内存空间,所以malloc
调用必须分配的字节大小等于数组元素数乘以每个元素字节大小的内存空间。
(3) 将malloc
的结果赋给指针变量。
比如,要给一个含10个元素的整型数组分配空间,然后将该内存赋给变量arr
,必须先用
int *arr;
声明arr
,随后使用
arr = malloc(10 * sizeof(int));
来分配空间。
声明数组和动态数组的主要区别是:
与一个已声明的数组相关的内存是作为声明过程的一部分自动分配的。
当声明数组的函数帧建立时,数组中所有的元素都作为帧的一部分进行分配。
在动态数组的情况下,实际内存在调用malloc
函数前是不会被分配的。
在程序中,已声明的数组大小必须是不变的。而由于动态数组的内存来自于堆,所以它们的大小可以是任意的。
而且,可以根据数据量推断数组的大小。如果知道需要一个含N
个元素的数组,可以保留大小正好的内存空间。
5.3 查找malloc中的错误
由于计算机内存系统的大小是有限的,堆的空间终会用完。此时malloc
返回指针NULL表示分配所需内存块的工作失败。
应该在每次调用malloc
时都检查失败的可能性。所以,分配一个动态数组后,需要写如下语句:
arr = malloc(10 * sizeof(int));
if (arr == NULL)
Error("No memory available.");
当空间用完时,程序显示出错消息并且停止运行通常是唯一可行的方法。
所以,调用malloc
并将其嵌入一个包括内存不够测试的新抽象层中是很有用的。
genlib.h
接口输出函数GetBlock
,并对NULL结果进行测试。
如果GetBlock
函数检测到内存不够的情况,就会调用Error
函数。
因此,以上两行代码可以替换为:
arr = GetBlock(10 * sizeof(int));
GetBlock
函数的执行结合了分配内存和检测错误的操作,使内存分配的操作更清晰,程序更易懂。
为进一步简化分配动态数组的过程,genlib.h
库还定义了函数NewArray
。
NewArray
取元素数和其基本类型后返回一个指针,指向特定大小的动态数组。
因此,要分配一个含50个字符串的动态数组,应调用:
NewArray(50, string)
5.4 释放内存
保证不会发生内存不够的一种方法是,一旦使用完已分配的空间就立刻释放它。
标准ANSI库提供了函数free
,用于归还以前由malloc
分配出去的堆内存。
例如,如果能肯定已经不再使用分配给arr
的内存,可以通过调用
free(arr);
释放该空间。
genlib.h
接口也包括一个FreeBlock
函数,其操作方法和free
完全相同。
但事实证明,知道何时释放一块内存并不那么容易。根本问题在于分配和释放内存的操作分别属于接口两边的实现及客户。
实现知道何时该分配内存,返回指针给客户,但它并不知道何时客户结束使用已分配的对象,所以释放内存是客户的责任。
对现在的大多数计算机的内存来说,可以随意分配所需内存而不需要考虑释放内存的问题。
这种策略几乎对所有运行时间不长的程序还是有效的。
内存有限的问题只有在设计一个需要运行很长时间的应用,比如所有其他系统所依靠的操作系统时,才变得有意义。
某些语言支持那些能主动检查正在使用的内存,并释放不再使用的内存的动态分配系统,此策略称为碎片收集(garbage collection)。
参考
《C语言的科学和艺术》 —— 13 指针