从数组到链表2

从数组到链表2
理想的情况是,用户可以不确定地添加数据(或者不断添加数据直到用完内存量),而不是先指定要输入多少项,也不用程序分配多余的空间。这可以通过在输入每一项后调用malloc()分配正好能储存该项的空间。如果用户输入3部影片,程序就调用malloc()3次;如果用户输入300部影片,程序就调用malloc()300次。
不过我们又制造了一个麻烦。比较一下一种方法是调用malloc()一次,为300个filem结构请求分配足够的空间;另一种方法是调用malloc()300次,分别为每个file结构请求分配足够的空间。前者分配的是连续的内存块,只需要一个单独的指向struct变量的指针,该指针指向已分配块中的第一个结构。简单的数组表示法让指针访问块中的每个结构,如前面代码段所示。第二种方法的问题是,无法保证每次调用malloc()都能分配到连续的内存块。这意味着结构不一定被连续储存。如图1,因此,与第一种方法储存一个指向300个结构块的指针相比,你需要储存300个指针,每个指针指向一个单独存储的结构。
一种解决方法是创建一个大型的指针数组,并在分配新结构时逐个给这些指针赋值,但是我们不打算使用这种方法:

#define TSIZE 45 //储存片名的数组大小
#define FMAX 500 //影片的最大数量
struct film{
	char title[TSIZE];
	int rating;
};
...
struct film *movies[FMAX];//结构指针数组
int i;
...
movies[i] = (struct film *)malloc(sizeof(struct film));

如果用不完500个指针,这种方法节约了大量的内存,因为内含500个指针的数组比内含500个结构的数组所占的内存少得多。尽管如此,如果用不到500个指针,还是浪费了不少空间。而且,这样还是有500个结构的限制。

还有一种更好的方法。每次使用malloc()为新结构分配空间时,也为新指针分配空间。但是还得需要另一个指针来跟踪新分配的指针,用于跟踪新指针的指针本身,也需要一个指针来跟踪,以此类推。要重新定义结构才能解决这个潜在的问题,即每个结构中包含指向next结构的指针。然后,当创建新结构时,可以把该结构的地址储存在上一个结构中。简而言之,可以这样定义film结构:

#define TSIZE 45 //储存片名的数组大小
struct film{
	char title [TSIZE];
	int rating;
	struct film *next;
};

虽然结构不能含有与本身类型相同的结构,但是可以含有指向同类型结构的指针。
这种定义是在定义链表linked list的基础,链表中的每一项都包含着在何处能找到下一项的信息。

在学习链表前,我们先从概念上理解一个链表。假设用户输入的片名是Monkey,等级为10.程序将为film类型的结构分配空间,把字符串Monkey拷贝到结构中的title成员中,然后设置rating成员为10.为了表明该结构后面没有其他结构,程序要把next成员指针设置为NULL(NULL是一个定义在stdio.h头文件中的符号常量,表示空指针)。当然,还需要一个单独的指针储存第一个结构的地址,该指针被称为头指针(head pointer)。头指针指向链表中的第一项。
现在假设用户输入第二部电影及其评级,如Happy Day和8.程序为第二个film类型结构分配空间,把新结构的地址储存在第一个结构中的next成员中(擦写了之前储存在该成员中的NULL),这样链表中第一个结构中的next指针指向第二个结构。然后程序把Happy Day和8拷贝到新结构中,并把第2个结构中的next成员设置为NULL,表明该结构时链表中的最后一个结构。
每加入一部新电影,就以相同的方式来处理。新结构的地址将储存在上一个结构中,新信息存在新结构中,而且新结构中的next成员设置为NULL。从而建立起链表。

假设要显示这个链表,每显示一项,就可以根据该项中易储存的地址来定位下一个待显示的项。

从概念上了解了链表的工作原理,接着我们来实现它。用链表来储存电影信息。

/* films2.c -- 使用结构链表*/
#include<stdio.h>
#include<string.h>//提供strcpy()原型
#include<stdlib.h>//提供malloc()原型
#define TSIZE 45 //储存片名的数组大小
struct film {
	char title[TSIZE];
	int rating;
	struct film* next; //指向链表中的下一个结构
};
char* s_gets(char *st, int n);
int main(void)
{
	struct film* head = NULL;
	struct film* prev, * current;
	char input[TSIZE];
	//收集并储存信息
	puts("Enter first movie title:");
	while (s_gets(input, TSIZE) != NULL && input[0] != '\0')
	{
		current = (struct film*)malloc(sizeof(struct film));
		if (head == NULL)//第一个结构
			head = current;
		else
			prev->next = current;
		current->next = NULL;
		strcpy(current->title, input);
		puts("Enter your rating <0-10>");
		scanf("%d", &current->rating);
		while (getchar() != '\n')
			continue;
		puts("Enter next movie title(empty line to stop):");
		prev = current;
	}
	//显示电影列表
	if (head == NULL)
		printf("No data entered");
	else
		printf("Here is the movie list:\n");
	current = head;
	while (current != NULL)
	{
		printf("Movie: %s Rating: %d\n",
			current->title, current->rating);
		current = current->next;
	}
	//完成任务,释放已分配的内存
	current = head;
	while (current != NULL)
	{
		free(current);
		head = 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');//查找换行符
		if (find)			//如果地址不是NULL
			*find = '\0';	//在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue;//处理剩余输入行
	}return ret_val;
}

我们来讨论这段代码:
显示链表
显示链表从设置一个指向第一个结构的指针(名为current)开始。由于头指针(名为head)已经指向链表中的第一个结构,所以可以用下面的代码来完成
current = head ;
然后,可以使用指针表示法访问结构的成员:
printf(“Movie: %s Rating: %d\n”, current->title, current->rating);
下一步是根据储存在该结构中next成员中的信息,重新设置current指针指向链表中的下一个结构。 代码如下
current =current->next;
完成这些之后,再重复整个过程。当显示到链表中最后一个项时,current将被设置为NULL,因为这是链表最后一个结构中next成员的值。
while(current != NULL)
{
printf(“Movie : %s Rating: %d\n”, current->title,current->rating);
current=current->next;
}

遍历链表时,为何不直接使用head指针,而要重新创建一个新指针current?这是因为如果使用head会改变head中的值,程序就找不到链表的开始处


创建链表
创建链表涉及下面3步:
(1)使用malloc()为结构分配足够的空间;
(2)储存结构的地址;
(3)把当前信息拷贝到结构中

如无必要不用创建一个结构,所以程序使用临时存储区(input数组)获取用户输入的电影名。如果用户通过键盘模拟EOF或输入一行空行,将退出下面的循环:
while(s_gets(input, TSIZE) != NULL&& input[0] != ‘\0’ )
如果用户进行输入,程序就分配一个结构的空间,并将其地址赋给指针变量current:
current = (struct film )malloc (sizeof(struct film));
链表中第一个结构的地址应储存在指针变量head中。随后每个结构的地址应储存在其前一个结构的next成员中。因此,程序要知道它处理的是否是第一个结构。最简单的方法是在程序开始时,把head指针初始化NULL。然后,程序可以使用head的值进行判断:
if(head == NULL)
head = current;
else
prev ->next =current;

在上面的代码中,指针prev指向上一次分配的结构。
接下来,必须为结构成员设置合适的值。尤其是,把next成员设置为NULL,表明当前结构时链表的最后一个结构。还要把input数组中的电影名拷贝到tile成员中,而且要给rating成员提供一个值。
由于s_gets()限制了只能输入TSIZE-1个字符,所以用strcpy()函数把input数组中的字符串拷贝到title成员很安全。
最后,还要为下一次输入做好准备。尤其是,要设置prev指向当前结构。因为在用户输入下一部电影且程序为新结构分配空间后,当前结构将成为新结构的下一个结构,所以程序在循环末尾这样设置该指针:
prev = current ;
释放链表
在许多环境中,程序结束时都会自动释放malloc()分配的内存。但是,最好还是成对调用malloc()和free()。因此,程序在清理内存时为每个已分配的结构都调用了free()函数:
current = head;
while (current !=NULL)
{
current = head;
head =current->next;
free(current);
}

反思总结
该程序还存在不足。例如,程序没有检查malloc()是否成功请求到内存,也无法删除链表中的项。这些不足可以弥补。例如,添加代码检查malloc()的返回值是否是NULL(返回NULL说明未获得所需内存)。如果程序要删除链表中的项,还要编写更多的代码。
这种用特定方法解决特定问题,并且在需要时才添加相关功能的编程方式通常不是最好的解决方案。另一方面,通常都无法预料程序要完成的所有任务。随着编程项目越来越大,一个程序员或编程团队事先计划好一切模式,越来越不现实。很多成功的大型程序都是由成功的小型程序逐步发展而来。
如果要修改程序,首先应该强调最初的设计,并简化其他细节。程序代码中没有遵循这个原则,它把概念模型和代码细节混在一起。
例如,该程序的概念模型是在一个链表中添加项,但是程序却把一些细节(如malloc()和current->next指针)放在最明显的位置,没有突出接口。
如果程序能以某种方式强调给链表添加项,并隐藏具体的处理细节(如调用内存管理函数和设置指针)会更好。把用户接口和代码细节分开的程序,更容易理解和更新。

注:本博客内容是从《C Primer Plus》中摘录并自我总结学习理解的。
侵删。

扫描二维码关注公众号,回复: 10814318 查看本文章
发布了29 篇原创文章 · 获赞 27 · 访问量 2952

猜你喜欢

转载自blog.csdn.net/Eumenides_Suki/article/details/103748483