理想的情况是,用户可以不确定地添加数据(或者不断添加数据直到用完内存量),而不是先指定要输入多少项,也不用让程序分配多余的空间。这可以通过在输入每一项后调用malloc()分配正好能存储该项的空间。如果用户输入3部影片,程序就调用malloc()3次;如果用户输入300部影片,程序就调用malloc()300次。 不过,我们又制造了另一个麻烦。比较一下,一种方法是调用malloc()一次,为300个film结构请求分配足够的空间;另一种方法是调用malloc()300次,分别为每个file结构请求分配足够的空间。前者分配的是连续的内存块,只需要一个单独的指向struct变量(film)的指针,该指针指向已分配块中的第1个结构。简单的数组表示法让指针访问块中的每个结构,如前面代码段所示。第2种方法的问题是,无法保证每次调用malloc()都能分配到连续的内存块。这意味着结构不一定被连续存储(见图17.1)。因此,与第1种方法存储一个指向300个结构块的指针相比,你需要存储300个指针,每个指针指向一个单独存储的结构。
一种解决方法是创建一个大型的指针数组,并在分配新结构时逐个给这些指针赋值,但是我们不打算使用这种方法:
#define TSIZE 45 /* size of array to hold titles */#define FMAX 500 /* maximum number of film titles */struct film { char title[TSIZE]; int rating;};... struct film * movies[FMAX]; /* array of pointers to structures */ int i; ... movies[i] = (struct film *) malloc (sizeof (struct film));
如果用不完500个指针,这种方法节约了大量的内存,因为内含500个指针的数组比内含500个结构的数组所占的内存少得多。尽管如此,如果用不到500个指针,还是浪费了不少空间。而且,这样还是有500个结构的限制。
还有一种更好的方法。每次使用malloc()为新结构分配空间时,也为新指针分配空间。但是,还得需要另一个指针来跟踪新分配的指针,用于跟踪新指针的指针本身,也需要一个指针来跟踪,以此类推。要重新定义结构才能解决这个潜在的问题,即每个结构中包含指向next结构的指针。然后,当创建新结构时,可以把该结构的地址存储在上一个结构中。简而言之,可以这样定义film结构:
#define TSIZE 45 /* size of array to hold titles */struct film { char title[TSIZE]; int rating; struct film * next;};
虽然结构不能含有与本身类型相同的结构,但是可以含有指向同类型结构的指针。这种定义是定义链表(linked list)的基础,链表中的每一项都包含着在何处能找到下一项的信息。 在学习链表的代码之前,我们先从概念上理解一个链表。假设用户输入的片名是Modern-Times,等级为10。程序将为film类型的结构分配空间,把字符串ModernTimes拷贝到结构中的title成员中,然后设置rating成员为10。为了表明该结构后面没有其他结构,程序要把next成员指针设置为NULL(NULL是一个定义在stdio.h头文件中的符号常量,表示空指针)。当然,还需要一个单独的指针存储第1个结构的地址,该指针被称为头指针(head-pointer)。头指针指向链表中的第1项。图17.2演示了这种结构(为节约图片空间,压缩了title成员中的空白)。
现在,假设用户输入第2部电影及其评级,如Midnight in Paris和8。程序为第2个film类型结构分配空间,把新结构的地址存储在第1个结构的next成员中(擦写了之前存储在该成员中的NULL),这样链表中第1个结构中的next指针指向第2个结构。然后程序把Midnight in Paris和8拷贝到新结构中,并把第2个结构中的next成员设置为NULL,表明该结构是链表中的最后一个结构。图17.3演示了这两个项。
每加入一部新电影,就以相同的方式来处理。新结构的地址将存储在上一个结构中,新信息存储在新结构中,而且新结构中的next成员设置为NULL。从而建立起如图17.4所示的链表。
假设要显示这个链表,每显示一项,就可以根据该项中已存储的地址来定位下一个待显示的项。然而,这种方案能正常运行,还需要一个指针存储链表中第1项的地址,因为链表中没有其他项存储该项的地址。此时,头指针就派上了用场。
1 使用链表
从概念上了解了链表的工作原理,接着我们来实现它。修改了程序films2.c,用链表而不是数组来存储电影信息。
The films2.c Program
/* films2.c -- using a linked list of structures */#include #include /* has the malloc prototype */#include /* has the strcpy prototype */#define TSIZE 45 /* size of array to hold title */struct film { char title[TSIZE]; int rating; struct film * next; /* points to next struct in list */};char * s_gets(char * st, int n);int main(void){ struct film * head = NULL; struct film * prev, * current; char input[TSIZE];/* Gather and store information */ puts("Enter first movie title:"); while (s_gets(input, TSIZE) != NULL && input[0] != '0') { current = (struct film *) malloc(sizeof(struct film)); if (head == NULL) /* first structure */ head = current; else /* subsequent structures */ prev->next = current; current->next = NULL; strcpy(current->title, input); puts("Enter your rating <0-10>:"); scanf("%d", ¤t->rating); while(getchar() != 'n') continue; puts("Enter next movie title (empty line to stop):"); prev = current; }/* Show list of movies */ if (head == NULL) printf("No data entered. "); else printf ("Here is the movie list:n"); current = head; while (current != NULL) { printf("Movie: %s Rating: %dn", current->title, current->rating); current = current->next; }/* Program done, so free allocated memory */ current = head; while (current != NULL) { free(current); current = current->next; } printf("Bye!n"); return 0;}char * s_gets(char * st, int n){ char * ret_val; char * find; ret_val = fgets(st, n, stdin); if (ret_val) { find = strchr(st, 'n'); // look for newline if (find) // if the address is not NULL, *find = '0'; // place a null character there else while (getchar() != 'n') continue; // dispose of rest of line } return ret_val;}
该程序用链表执行两个任务。第1个任务是,构造一个链表,把用户输入的数据存储在链表中。第2个任务是,显示链表。显示链表的任务比较简单,所以我们先来讨论它。
1..1 显示链表
显示链表从设置一个指向第1个结构的指针(名为current)开始。由于头指针(名为head)已经指向链表中的第1个结构,所以可以用下面的代码来完成:
current = head;
然后,可以使用指针表示法访问结构的成员:
printf("Movie: %s Rating: %dn", current->title, current->rating);
下一步是根据存储在该结构中next成员中的信息,重新设置current指针指向链表中的下一个结构。代码如下:
current = current->next;
完成这些之后,再重复整个过程。当显示到链表中最后一个项时,current将被设置为NULL,因为这是链表最后一个结构中next成员的值。
while (current != NULL){ printf("Movie: %s Rating: %dn", current->title, current->rating); current = current->next;}
遍历链表时,为何不直接使用head指针,而要重新创建一个新指针(current)?因为如果使用head会改变head中的值,程序就找不到链表的开始处。
1.2 创建链表
创建链表涉及下面3步:
(1)使用malloc()为结构分配足够的空间;
(2)存储结构的地址;
(3)把当前信息拷贝到结构中。
如无必要不用创建一个结构,所以程序使用临时存储区(input数组)获取用户输入的电影名。如果用户通过键盘模拟EOF或输入一行空行,将退出下面的循环:
while (s_gets(input, TSIZE) != NULL && input[0] != '0')
如果用户进行输入,程序就分配一个结构的空间,并将其地址赋给指针变量current:
current = (struct film *) malloc(sizeof(struct film));
链表中第1个结构的地址应存储在指针变量head中。随后每个结构的地址应存储在其前一个结构的next成员中。因此,程序要知道它处理的是否是第1个结构。最简单的方法是在程序开始时,把head指针初始化为NULL。然后,程序可以使用head的值进行判断:
if (head == NULL) /* first structure */ head = current;else /* subsequent structures */ prev->next = current;
在上面的代码中,指针prev指向上一次分配的结构。 接下来,必须为结构成员设置合适的值。尤其是,把next成员设置为NULL,表明当前结构是链表的最后一个结构。还要把input数组中的电影名拷贝到title成员中,而且要给rating成员提供一个值。如下代码所示:
current->next = NULL;strcpy(current->title, input);puts("Enter your rating <0-10>:");scanf("%d", ¤t->rating);
由于sgets()限制了只能输入TSIZE-1个字符,所以用strcpy()函数把input数组中的字符串拷贝到title成员很安全。 最后,要为下一次输入做好准备。尤其是,要设置prev指向当前结构。因为在用户输入下一部电影且程序为新结构分配空间后,当前结构将成为新结构的上一个结构,所以程序在循环末尾这样设置该指针:
prev = current;
程序是否能正常运行?下面是该程序的一个运行示例:
Enter first movie title: Spirited Away Enter your rating <0-10>: 9 Enter next movie title (empty line to stop): The Duelists Enter your rating <0-10>: 8 Enter next movie title (empty line to stop): Devil Dog: The Mound of Hound Enter your rating <0-10>: 1 Enter next movie title (empty line to stop):
Here is the movie list: Movie: Spirited Away Rating: 9 Movie: The Duelists Rating: 8 Movie: Devil Dog: The Mound of Hound Rating: 1 Bye!
1.3 释放链表
在许多环境中,程序结束时都会自动释放malloc()分配的内存。但是,最好还是成对调用malloc()和free()。因此,程序在清理内存时为每个已分配的结构都调用了free()函数:
current = head;while (current != NULL){ free(current); current = current->next;}
2 反思
films2.c程序还有些不足。例如,程序没有检查malloc()是否成功请求到内存,也无法删除链表中的项。这些不足可以弥补。例如,添加代码检查malloc()的返回值是否是NULL(返回NULL说明未获得所需内存)。
如果程序要删除链表中的项,还要编写更多的代码。 这种用特定方法解决特定问题,并且在需要时才添加相关功能的编程方式通常不是最好的解决方案。另一方面,通常都无法预料程序要完成的所有任务。随着编程项目越来越大,一个程序员或编程团队事先计划好一切模式,越来越不现实。很多成功的大型程序都是由成功的小型程序逐步发展而来。 如果要修改程序,首先应该强调最初的设计,并简化其他细节。
程序films2.c中的程序示例没有遵循这个原则,它把概念模型和代码细节混在一起。例如,该程序的概念模型是在一个链表中添加项,但是程序却把一些细节(如,malloc()和current->next指针)放在最明显的位置,没有突出接口。如果程序能以某种方式强调给链表添加项,并隐藏具体的处理细节(如调用内存管理函数和设置指针)会更好。把用户接口和代码细节分开的程序,更容易理解和更新。学习下面的内容就可以实现这些目标。