C和指针:第十一,十二章
第11章 动态内存分配(动态内存分配的一个常见用途是为那些长度只有运行时才知道的数组分配内存空间。)
1. 使用malloc, free 函数动态内存分配,函数原型:
void *malloc (size_t size);
void free (void *pointer);
1.) malloc 函数向内存池中申请一块合适的内存,并返回指向该块内存地址的指针;
2.) malloc 申请成功的内存地址,必须初始化;
3.) malloc 申请的内存地址是连续,如果操作系统无法提供给malloc 更多的内存,malloc 返回一个Null指针。所以检测malloc 返回指针是否为NULL很重要;
4.) malloc 返回void * 类型指针,void * 类型指针可以转换为其他任何类型的指针;
5.) 当申请的内存不再使用时,要调用free 函数将内存释放出来;
6.) free 参数必须要是NULL或先前从malloc,calloc或realloc返回的值。向free传递一个NULL 参数不会产生任何效果。
int *pi;
…
pi = malloc (100);
if (pi == NULL)
{
printf(“out of memory!\n”); // pi = malloc (25 * sizeof(int));
exit(1);
}
// 初始化
int *pi2, i;
pi2 = pi;
for (i = 0; i < 25; i += 1)
*pi2++ = 0; // pi2[i] = 0;
2. caloc 和 realloc 函数动态内存分配,函数原型:
void *calloc (size_t num_elements, size_t element_size);
void realloc (void *ptr, size_t new_size);
1.) calloc 与 malloc 的主要区别在于,calloc 返回指向内存的指针之前把它初始化为0;
2.) calloc 申请内存的方式是,给定所需元素的数量和每个元素的字节数;
3.) realloc 用于修改一个原先已经分配的内存块,如果用于扩大一个内存块,那么原来的内存块依然保存,新增的内存添加到原来的内存后面;缩小一个内存块,就把该内存块尾部的内存拿掉,剩余部分的内存原先的内容依然保留;如果原来的内存块无法改变大小,realloc 将分配另一块正确大小的内存,并把原先的那块内存内容复制到新的内存块上。因此,在使用realloc 后,不能再使用指向原来内存块的指针,而是应该改用relloac 返回的新指针。
3. 常见动态内存错误
常见的动态内存分配错误有:
1.) 忘记检查所请求的内存是否成功分配;
2.) 对NULL指针进行解除引用操作;
3.) 对分配的内存操作时越界;
4.) 释放非动态分配的内存;
5.) 试图释放一块动态分配的内存的一部分或一块动态内存被释放后被继续使用。
如:
int *pi = malloc (10 * sizeof (int));
…
free (pi + 5); // error, 不允许只释放内存的一部分
4. 内存泄漏(memory leak)
动态分配的内存在使用完后,不释放将引起内存泄漏,在共享一个通用内存池的操作系统中,内存泄漏将一点点榨干可用内存,最终使其一无所有。要摆脱这个困境,只有重启系统。在能够记住每个程序当前拥有的内存段系统上,内存泄漏也同样是一个严重的错误,一个持续分配却一点不释放内存的程序最终将耗尽可用内存,可能会导致当前已完成的工作统统丢失。
第12章 使用结构和指针
1. 链表(linded list)是种包含数据的独立数据结构(通常称为节点)的集合。链表中的每个节点通过链或指针连接在一起。通常节点是动态分配的。
2. 单链表,每个节点包含一个指向链表下一节点的指针,链表最后一个节点的指针字段值为NULL,提示链表后面不再有其他节点。根指针(root pointer)指向链表的第一个节点,它不包含任何数据。如:
typedef struct NODE {
struct NODE *link; // 指向一个NODE类型的结构
int value;
} Node;
单链表可以通过链从开始位置遍历链表直到结束位置,但链表无法从相反的方向进行遍历。如果在到最后一个节点,想回到其他节点,只能从根指针从头开始。
链表结构:
3. 在单链表中间插入新节点
在单链表中插入新节点,要求这个链表是已排序的。插入节点方法如下:
1.) 定义两个指针,一个用于记录当前节点的指针(*current),一个记录前一节点的指针(*previous);
2.) 搜索符合条件的当前节点 (现在要在当前节点和前一个节点之间插入一个新节点);
3.) 创新一个新节点,并分配内存空间;
4.) 让新节点的指针指向符合条件的当前节点;
5.) 让previous 指针(原符合条件节点的前一个节点指针)指向新节点;
在一个大于new_value值的节点前插入新节点包含new_value的值,代码实现:
a
4. 在单链表中头,尾插入新节点
上面的函数方式在头,尾节点处插入新节点会有问题:一是,在第一个节点的前面插入一个新节点,上面的函数将无法实现对根指针(root pointer)的访问;二是, 当在最后一个节点后面插入一个新节点(追加一个新节点)时,访问将越界,并且对一个NULL指针执行了间接访问。
要解决在第一个节点前插入新节点方法可以将指向root的指针作为参数传递给函数,再使用间接访问,这样函数既可以获得root的值,也可以向它存储一个新的指针值。因为root本身就是指向一个节点的指针,那么指向root的指针就是指向指针的指针。
要解决在最后一个节点后面插入一个新节点(追加一个新节点),可以对current->value的值做检查,以确保它不是一个NULL指针。修改以上函数代码为:
b
要调用这个函数可以是: result = sll_insert (&root, 12); // root本身就是指针,取指针的地址,就是指针的指针
5. 优化插入函数
由于rootp指向的并不是第一个节点本身,而是第一个节点内的link指针,现在要利用rootp在每次移动节点时,都指向每个节点的link指针。可用下图来表示:
原结构 优化后结构(rootp指针指向第一个节点中的link指针)
从图上可以看出,把原来的previous指针丢弃了,而在每次移动节点时,先让rootp(linkp)指向当前指针,再将当前指针指向节点中的link指针给linkp(此时的linkp指向下一节点了),如此往复,直到找到满足条件的节点,代码实现如下:
c
C语言的指针哲学是:“给你锤子,实际上你还可以用好几中锤子。但祝你好运!”。也就是说,C指针给我们足够的灵活性来获得代码上效率的提升,但同时要承担相应的风险。
6. 双链表
双链表中每个节点包含两个指针,一个指向前一节点的指针,和一个指向后一节点的指针。这使得我们可以以任意方向遍历双链表,甚至可以忽前忽后。双链表结构为:
typedef struct NODE {
struct NODE *fwd;
struct NODE *bwd;
int value;
} Node;
双链表中有两个根指针,一个指向链表中的第一个节点,一个指向链表中最后一个节点。这两个节点允许我们从链表的任何一端开始遍历链表。其结构可用下图表示:
注: 上图中root根指针下的value是没有值的
7. 在有序的双链表中插入新节点
往双链表中插入新节点,可能会出现以下四种情况:
1.) 新节点在链表中间位置插入
2.) 新节点在链表起始位置插入
3.) 新节点在链表结束位置插入
4.) 在一个空链表中插入
考虑以上四种情况,我们需要对根指针的fwd,bwd两个根指针,和新节点的fwd,bwd指针两个指针以及相对于新节点的前一节点fwd指针和下一节点的bwd指针做如下情况的设置:
如果在1.)和2.)情况下插入新节点,需要将新节点的fwd指针设置为指向下一节点,而下一节点的bwd指针需要指向新节点;如果在3.)和4.)情况下插入新节点,需要将新节点的fwd指针设置为NULL,且根节点的bwd指针也设置为NULL(因为没有后继节点了)。
如果在1.)和3.)情况下插入新节点,要将新节点中的bwd指针设为指向前一节点,而链表的前一节点中的fwd要设为指向新节点;如果在2.)和4.)情况下插入新节点,新节点的bwd指针要设置为NULL,且根节点的fwd指针设为指向新节点(因为新节点就是最后一个节点)。代码实现如下:
d