1. malloc() 和 free()
我们前面讨论的自动变量、寄存器变量、无链接静态变量、内部链接静态变量、外部连接静态变量这些存储类别有一个共同之处:在确定用哪种存储类别后,根据已制定好的内存管理规则,将自动选择其作用域和存储期。然而,还有更灵活地选择,即用库函数分配和管理内存,也就是动态分配存储期。
首先,回顾一下内存分配。所有程序都必须预留足够的内存来储存程序使用的数据。这些内存中有些是自动分配的。静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。
C 能做的不止这些。可以在程序运行时分配更多的内存。主要的工具是 malloc() 函数。
1.1. malloc() 介绍
malloc() 函数定义在 <stdlib.h> 中:
void *__cdecl malloc(size_t _Size);
参数:所需的内存字节数。
功能:malloc() 函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc() 分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。
返回值:如果 malloc() 分配内存成功,返回一个 void* 指针指向分配的内存块首地址,如果 malloc() 分配内存失败,将返回空指针。
PS:从 ANSI C 标准开始,C 使用一个新的类型:指向 void 的指针,该类型相当于一个“通用指针”。把指向 void 的指针赋给任意类型的指针完全不用考虑类型匹配的问题。但是在 ANSI C 中,应该坚持使用强制类型转换,提高代码的可读性(C++ 中则要求必须强制转换)。malloc() 函数可用于返回指向数组的指针、指向结构的指针等,通常该函数的返回值会被强制转换为匹配的类型。
我们试着用 malloc() 创建一个数组。除了用 malloc() 在程序运行时请求一块内存,还需要一个指针记录这块内存的位置。例如:
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
以上代码为 30 个 double 类型的值请求内存空间,并设置 ptd 指向该位置。注意,指针 ptd 被声明为指向一个 double 类型,而不是指向内含 30 个 double 类型值的块。回忆一下,数组名是该数组首元素的地址。因此,如果让 ptd 指向这个块的首元素,便可像使用数组名一样使用它。也就是说,可以使用表达式 ptd[0] 访问该块的首元素,ptd[1] 访问第 2 个元素,以此类推。根据前面所学的知识,可以使用数组名来表示指针,也可以用指针来表示数组。
现在,我们有3种创建数组的方法。
- 声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态内存或自动内存创建这种数组。
- 声明变长数组(C99新增的特性)时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
- 声明一个指针,调用 malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。
使用第2种和第3种方法可以创建动态数组(dynamic array)。这种数组和普通数组不同,可以在程序运行时选择数组的大小和分配内存。动态分配内存比变长数组更灵活。
double item[n]; /* C99之前:n不允许是变量 */
ptd = (double *) malloc(n * sizeof(double)); /* 可以 */
通常,malloc()要与free()配套使用。
1.2. free() 介绍
free() 和 malloc() 函数一样在 <stdlib.h> 中声明。
void __cdecl free(void *_Memory);
参数:malloc() 返回的地址。
返回值:无
功能:释放之前 malloc() 分配的内存。因此,动态分配内存的存储期从调用 malloc() 分配内存到调用 free() 释放内存为止。
设想 malloc() 和 free() 管理着一个内存池。每次调用 malloc() 分配内存给程序使用,每次调用 free() 把内存归还内存池中,这样便可重复使用这些内存。
PS:free() 的参数应该是一个指针,指向由 malloc() 分配的一块内存。不能用 free() 释放通过其他方式(如,声明一个数组)分配的内存。
1.3. 动态数组的好处
使用动态数组有什么好处?
使用动态数组给程序带来了更多灵活性。假设你已经知道,在大多数情况下程序所用的数组都不会超过 100 个元素,但是有时程序确实需要 10000 个元素。要是按照平时的做法,你不得不为这种情况声明一个内含 10000 个元素的数组。基本上这样做是在浪费内存。如果需要 10001 个元素,该程序就会出错。这种情况下,可以使用一个动态数组调整程序以适应不同的情况。
1.4. free() 的重要性
静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非用 free() 进行释放。
...
int main()
{
double glad[2000];
int i;
...
for (i = 0; i < 1000; i++)
gobble(glad, 2000);
...
}
void gobble(double ar[], int n)
{
double * temp = (double *) malloc( n * sizeof(double));
.../* free(temp); // 假设忘记使用free() */
}
第1次调用 gobble() 时,它创建了指针 temp,并调用 malloc() 分配了 16000 字节的内存(假设 double 为 8 字节)。假设如代码注释所示,遗漏了 free()。当函数结束时,作为自动变量的指针 temp 也会消失。但是它所指向的 16000 字节的内存却仍然存在。由于 temp 指针已被销毁,所以无法访问这块内存,它也不能被重复使用,因为代码中没有调用 free() 释放这块内存。
第2次调用 gobble() 时,它又创建了指针 temp,并调用 malloc() 分配了 16000 字节的内存。第1次分配的 16000 字节内存已不可用,所以 malloc() 分配了另外一块 16000 字节的内存。当函数结束时,该内存块也无法被再访问和再使用。
循环要执行 1000 次,所以在循环结束时,内存池中有 1600 万字节被占用。实际上,也许在循环结束之前就已耗尽所有的内存。这类问题被称为内存泄漏(memory leak)。在函数末尾处调用 free() 函数可避免这类问题发生。
2. calloc() 函数
分配内存还可以用 calloc() 函数。同样是在 <stdlib.h> 中声明。
void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
参数:calloc() 函数接受两个无符号整数作为参数( ANSI 规定是 size_t 类型)。第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。
返回值:和 malloc() 函数类似,在 ANSI C 之后,返回指向 void 的指针。如果要储存不同的类型,应使用强制类型转换运算符。
功能:分配指定数目指定大小的存储空间,并返回指向这个空间的指针。
long * newmem;
newmem = (long *)calloc(100, sizeof (long));
在该例中,long 为 4 字节,所以,前面的代码创建了 100 个 4 字节的存储单元,总共400字节。 用 sizeof(long) 而不是 4,提高了代码的可移植性。这样,在其他 long 不是 4 字节的系统中也能正常工作。
calloc() 函数还有一个特性:它把块中的所有位都设置为 0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。 而 malloc() 函数不会初始化。
free() 函数也可用于释放 calloc() 分配的内存。
3. 动态内存分配和变长数组
变长数组(VLA)和调用 malloc() 在功能上有些重合。例如,两者都可用于创建在运行时确定大小的数组。
不同的是,变长数组是自动存储类型。因此,程序在离开变长数组定义所在的块时,变长数组占用的内存空间会被自动释放,不必使用 free()。另一方面,用 malloc() 创建的数组不必局限在一个函数内访问。例如,可以这样做:被调函数创建一个数组并返回指针,供主调函数访问,然后主调函数在末尾调用 free() 释放之前被调函数分配的内存。另外,free() 所用的指针变量可以与 malloc() 的指针变量不同,但是两个指针必须储存相同的地址。但是,不能释放同一块内存两次。
对多维数组而言,使用变长数组更方便。当然,也可以用 malloc() 创建二维数组,但是语法比较繁琐。如果编译器不支持变长数组特性,就只能固定二维数组的维度。
int n = 5;
int m = 6;
int ar2[n][m]; // n×m的变长数组(VLA)
int (* p2)[6]; // C99之前的写法
int (* p3)[m]; // 要求支持变长数组
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组(要求支持变长数 组)
ar2[1][2] = p2[1][2] = 12;
4. 静态存储区、栈、堆
存储类别和动态内存分配有何联系?
我们来看一个理想化模型。可以认为程序把它可用的内存分为 3 部分:一部分供具有外部链接、内部链接和无链接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配。
静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可访问储存在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。 这部分内存称为静态存储区 or 全局存储区。
自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。 这部分内存称为栈。
动态分配的内存在调用 malloc() 或相关函数时存在,在调用 free() 后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈内存慢。这部分内存称为堆。
总而言之,程序把静态对象、自动对象和动态分配的对象储存在不同的区域。