数组的元素存储于内存中连续的位置上。当一个数组被声明时,它所需要的内存在编译时就被分配。但是,也可以使用动态内存分配在运行时为它分配内存。在本章中,我们将研究这两种技巧的区别,看看什么时候应该使用动态内存分配以及怎样进行动态内存分配。
11.1 为什么使用动态内存分配
在声明数组时,必须用一个编译时常量指定数组的长度。但是,数组的长度常常在运行时才知道,这是因为它所需要的内存空间取决于输入数据。例如,一个用于计算学生等级和平均分的程序可能需要存储一个班级所有学生的数据,但不同班级的学生数量可能不同。在这些情况下,我们通常采取的方法是声明一个较大的数组,它可以容纳可能出现的最多元素。
这种方法的优点是简单,但它有好几个缺点。首先,这种声明在程序中引入了人为的限制,如果程序需要使用的元素数量超过了声明的长度,它就无法处理这种情况。要避免这种情况,显而易见的方法是把数组声明得更大一些,但这种做法使它的第2个缺点进一步恶化。如果程序实际需要的元素数量比较少时,巨型数组的绝大部分内存空间都被浪费了。这种方法的第3个缺点是如果输入的数据超过了数组的容纳范围,程序必须以一种合理的方式做出响应。它不应该因为一个异常而失败,但也不应该打印出看上去正确实际上却是错误的结果。实现这一点所需要的逻辑其实很简单,但人们在头脑中很容易形成“数组永远不会溢出”这个概念,这就诱使他们不去实现这种方法。
批在:这里感觉不如上课讲的清楚,我记得上课讲的是一种动态内存分配的实现方式,如果输入的数据超出了初始的空间,就把需要使用的空间扩大两倍。依此类推。
11.2 malloc和free
C函数库提供了malloc和free两个函数,分别用于执行动态内存分配和释放。这些函数维护一个可用内存池。当一个程序另外需要一些内存时,它就调用malloc函数,malloc从内存池中提取一块合适的内存,并向该程序返回一个指向这块内存的指针。这块内存此时并没有以任何方式进行初始化。如果对这块内存进行初始化非常重要,你要么自己动手对它进行初始化,要么使用calloc函数(在下一节描述)。当一块以前分配的内存不再使用时,程序调用free函数把它归还给内存池供以后之需。
这两个函数的原型如下所示,它们都在头文件stdlib.h中声明:
void *malloc(size_t size); void free(void *pointer);
malloc的参数就是需要分配的内存字节(字符)数。如果内存池中的可用内存可以满足这个需求,malloc就返回一个指向被分配的内存块起始位置的指针。
malloc所分配的是一块连续的内存。例如,如果请求它分配100字节的内存,那么它实际分配的内存就是100个连续的字节,并不会分开位于两块或多块不同的内存。同时,malloc实际分配的内存有可能比请求的稍微多一点。但是,这个行为是由编译器定义的,所以不能指望它肯定会分配比请求更多的内存。
如果内存池是空的,或者它的可用内存无法满足请求,会发生什么情况呢?在这种情况下,malloc函数向操作系统请求,要求得到更多的内存,并在这块新内存上执行分配任务。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。因此,对每个从malloc返回的指针都进行检查,确保它并非NULL,这非常重要。
free的参数必须要么是NULL,要么是一个先前从malloc、calloc或realloc(稍后描述)返回的值。向free传递一个NULL参数不会产生任何效果。
malloc又是如何知道所请求的内存需要存储的是整数、浮点值、结构还是数组呢?它并不知情——malloc返回一个类型为void *的指针,正是缘于这个原因。标准表示一个void *类型的指针可以转换为其他任何类型的指针。但是,有些编译器,尤其是那些老式的编译器,可能要求你在转换时使用强制类型转换。
对于要求边界对齐的机器,malloc所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求。
批注:记的话其实就是memory allocation吧?
11.3 calloc和realloc
另外还有两个内存分配函数:calloc和realloc。它们的原型如下所示:
void *calloc(size_t num_elements, size_t element_size); void realloc(void *ptr, size_t new_size);
calloc也用于分配内存。malloc和calloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0。这个初始化常常能带来方便,但如果你的程序只是想把一些值存储到数组中,那么这个初始化过程纯属浪费时间。calloc和malloc之间另一个较小的区别是它们请求内存数量的方式不同。calloc的参数包括所需元素的数量和每个元素的字节数。根据这些值,它能够计算出总共需要分配的内存。
realloc函数用于修改一个原先已经分配的内存块的大小。使用这个函数,可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,剩余部分内存的原先内容依然保留。
如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,就不能再使用指向旧内存的指针,而是应该改用realloc所返回的新指针。
最后,如果realloc函数的第一个参数是NULL,那么它的行为就和malloc一模一样。
批注:初始化为0,所以是clear allocation?
11.4 使用动态分配的内存
这里有一个例子,它用malloc分配一块内存:
符号NULL定义于stdio.h,它实际上是字面值常量0。它在这里起着视觉提醒器的作用,提醒我们进行测试的值是一个指针而不是整数。
如果内存分配成功,那么我们就拥有了一个指向100字节的指针。在整型为4字节的机器上,这块内存将被当作25个整型元素的数组,因为pi是一个指向整型的指针。
如果你的目标就是获得足够存储25个整数的内存,这里有一个更好的技巧来实现这个目的。
pi = malloc( 25 * sizeof(int))
这个方法更好一些,因为它是可移植的。即使是在整数长度不同的机器上,它也能获得正确的结果。
既然已经有了一个指针,那么该如何使用这块内存呢?当然可以使用间接访问和指针运算来访问数组的不同整数位置,下面这个循环就是这样做的。它把这个新分配的数组的每个元素都初始化为0:
可以看到,不仅可以使用指针,也可以使用下标。下面的循环所执行的任务和前面一个相同:
感觉和之前上课的区别在于这本书会写的更详细而且更好理解一点。。。