17.1 动态存储分配
背景:c语言的数据结构通常是固定大小的,为了扩大数据结构的容量,必须修改程序并且再次编译。
说明(行为):在程序执行期间分配内存单元
用途:可以根据需要设计可以扩大(和缩小)的数据结构
适用类型 | 说明 |
---|---|
字符串 | 略 |
数组 | 略 |
结构 | 可以链接成表、树和其它数据结构 |
17.1.1 内存分配函数
函数 | 说明 | 库 | 备注 | 返回值 |
---|---|---|---|---|
malloc |
分配内存快,但是不对内存块进行初始化 | stdlib.h | 最常用,不需要对分配的内存块进行清除,所以它比calloc 更高效 |
void * (通用指针,本质上只是内存地址) |
calloc |
分配内存块,并且对内存块进行清除 | stdlib.h | ||
realloc |
调整先前分配的内存块 | stdlib.h |
17.1.2 空指针
说明:“指向为空的指针”,这是一个区别于所有有效指针的特殊值。K&R C
用NULL
(宏)来表示空指针。
相关场景:当调用内存分配函数时,如果无法定位满足我们需要的足够大的内存块,函数会返回空指针(null pointer
)。
定义了NULL
的库文件:
- locale.h
- stddef.h
- stdio.h
- stdlib.h
- string.h
- time.h
真假:所有非空指针都为真,而只有空指针为假。
1 | if (p == NULL) ... |
1 | p = malloc(10000); |
更酷的方式
1 | if ((p = malloc(10000)) == NULL) { |
17.2 动态分配字符串
17.2.1 使用malloc函数为字符串分配内存
注意:为字符串分配内存空间时不要忘记包含空字符串的空间。
1 | // 动态分配 |
17.2.2 在字符串函数中使用动态存储分配
1 |
|
17.2.3 动态分配字符串的数组
说明:在数组中存储字符串有两种方式。二维字符数组或者字符串字面量指针数组,相比之下,前者可能会浪费空间。
17.2.4 程序:显示一个月的提示列表(改进版)
1 | /** |
1 | $ gcc -Wall -o remind remind.c |
17.3 动态分配数组
原理:在程序执行期间为数组分配空间,然后通过指向数组第一个元素的指针访问数组。由于c语言中数组和指针的关系紧密,指向动态分配的内存块的指针可以当作数组的名字使用。
注意:计算数组所需的空间要使用 sizeof 运算符,如果分配空间不足,稍后往数组中存储时程序会出现异常。
17.3.1 使用malloc函数为数组分配存储空间
1 | // 创建一个含n个整数的数组 |
17.3.2 calloc函数
函数原型(stdlib.h
):如果要求的空间无效,那么此函数返回空指针。
1 | /** |
注意:
- 当第一个参数为1时,可以为任何类型的数据项(不仅仅是数组)分配空间
- calloc函数会清除分配的空间中的数据
1 | // 声明一个长度为n的int型数组,并将所有项初始化为0 |
17.3.3 realloc函数
原型(stdlib.h
):
1 | /** |
用途:一旦为数组分配完内存,稍后可能会出现数组过大或过小。relloc函数可以调整数组的大小以使它更适合需要。
局限:要确定传递给realloc函数
的指针来自于先前malloc函数
、calloc函数
或realloc函数
的调用获得的。否则程序会出现异常。
规则:
- 如果无法扩大内存(后边内存被占用),会在别处分配新的内存,然后把旧块中的内容复制过去
- 当扩展内存块时,
realloc函数
不会对添加进内存块的字节进行初始化 - 如果
realloc函数
不能按要求扩大内存块,那么它会返回空指针,并且在原有的内存块中的数据不会发生改变。 - 如果
realloc函数
调用时以空指针作为第一个实际参数,那么它的行为就将像malloc函数
一样 - 如果
realloc函数
调用时以0作为第二个实际参数,那么它会释放掉内存块
注意:一旦realloc函数
返回,一定要对指向内存块的所有指针进行更新(将新的地址赋值给指针),因为可能realloc函数移动到了其它地方的内存块。
17.4 释放存储
堆(heap):malloc函数
和其他内存分配函数所获得的内存块都来自一个称为堆的存储池。调用这些函数经常会耗尽堆,或者要求大的内存块也会耗尽堆,这会导致函数返回空指针。
垃圾(garbage):对程序而言不再访问到的内存块被称为垃圾。
内存泄漏(memroy leak):运行中留有垃圾被称为内存泄漏。
垃圾收集器(garbage collector):用于垃圾的自动定位和回收,但c语言不提供。相反,每个c程序负责回收各自的垃圾(调用free函数
)。
1 | /*模拟内存泄漏*/ |
17.4.1 free函数
用途:调用free函数
将内存块释放返回堆。
原型:stdlib.h
限制:free函数
的实际参数必须是指针,而且一定是先前内存分配函数
返回的。
1 | /** |
17.4.2 “悬空指针”问题
悬空指针(dangling pointer):指向被free
掉的内存块的指针。
注意:悬空指针很难被发现,而且试图通过“悬空指针”修改被释放掉的内存块会导致程序异常。
17.5 链表
链表(linked list):是由一连串的结构(节点)组成的,其中每个节点都包含指向下一个链中节点的指针。
优点:更灵活,方便扩大和缩小(插入和删除)。
缺点:没有“随机访问”的能力
17.5.1 声明节点类型
注意:结点类型只能使用标记而不能使用typedef
定义结构,因为后者无法在节点内声明指向另一个结点的成员。
1 | // 简单的单个节点的结构 |
17.5.2 创建节点
1 | // 声明节点 |
17.5.3 ->运算符
右箭头选择(right arrow selection):通过指针访问结构中的成员
1 | scanf("%d", &new_node->value); //scanf("%d", &(*new_node).value) |
17.5.4 在链表的开始处插入节点
步骤
- 为节点分配内存单元
- 把数据存储在节点中
- 把节点插入到链表中
伏笔:在17.6节中对add_to_list
有进一步优化。
1 |
|
17.5.5 搜索链表
惯用法:for (p = first; p != NULL; p = p->next)
形式一:惯用法
1 | /** |
形式二:省略中间变量
1 | /** |
形式三:链表到末尾和找到目标判定合并
1 | /** |
形式四:使用while
1 | /** |
17.5.6 从链表中删除节点
步骤
- 定位要删除的节点
- 改变前一个节点,从而使它“绕过”删除节点
- 调用
free函数
从而收回删除节点占用的内存空间
1 | /** |
17.5.8 程序:维护零件数据库(改进版)
说明:使用链表代替数组有两个主要的好处
- 不需要事先限制数据库的大小,数据库可以扩大到没有更多内存空间存储零件为止
- 可以很容易保持用零件编号排序的数据库,当往数据库中添加新零件时,只是简单把它插入链表中的适当位置就可以了
1 | $ make |
readline.h
1 |
|
readline.c
1 |
|
invent2.c
1 | /** |
17.6 指向指针的指针
说明:对17.5.4中的add_to_list
进行优化,优化后插入链表的功能将完全由该函数提供。
原理:通过指针的指针的副本,达到修改指针指向的目的。
1 | /** |
17.7 指向函数的指针
说明:毕竟函数占用内存单元,所以每个函数都有地址,就像每个变量都有地址一样。
17.7.1 函数指针作为实际参数
声明:声明为指向函数的指针有两种方式,从编译器的角度看是完全一样的。
方式一:返回值 函数名(
返回值 (*函数名)(参数1, 参数2, ...)
, 参数)
1 | double integrate (double (*f)(double), double a, double b) { |
方式二:返回值 函数名(
返回值 (函数名)(参数1, 参数2, ...)
, 参数)
1 | double integrate (double f(double), double a, double b) { |
17.7.2 qsort函数
原型:stdlib.h
1 | /** |
1 | /** |
17.7.3 函数指针的其他用途
说明:c语言对待指向函数的指针就像对待指向数据的指针一样。我们可以把函数存储在变量中,或者用做数组的元素,再或者用做结构或联合的成员,甚至可以编写返回函数指针的函数,
1 | /*存储函数的变量*/ |
17.7.4 程序:列三角函数表
1 | /** |