第14章 结构和其他数据形式
目录
- 14.1 实例问题:创建图书目录
- 14.2 建立结构声明
- 14.3 定义结构变量
- 14.4 结构数组
- 14.5 嵌套结构
- 14.6 指向结构的指针
- 14.7 向函数传递结构信息
- 14.8 把结构内容保存到文件中
- 14.9 结构:下一步是什么
- 14.10 联合简介
- 14.11 枚举类型
- 14.12 typedef简介
- 14.13 奇特的声明
- 14.14 函数和指针
- 14.18 编程练习
- 习题7
- 习题8、9
设计程序最重要的一个步骤就是选择一个表示数据的好方法。在多数情况下,使用简单的变量甚至数组都是不够的。C使用结构变量进一步增强了表示数据的能力。
14.1 实例问题:创建图书目录
- 创建的结构每个部分称为成员或字段。下面是必须掌握的3个重要技巧:
- (1)建立结构的格式或布局
- (2)声明遵循该布局的变量
- (3)获取对一个结构变量的各个部分的访问
14.2 建立结构声明
- 结构声明是描述结构如何组合的主要方法。它并没有创建一个实际的数据对象,而是描述了组成这类对象的元素。
- 关键字struct后面的是 可选标记,它是用来引用该结构的快速标记。在一个地方定义结构设计,而在其他地方定义实际的结构变量是,必须使用标记。
- 每个成员可以是任何一种C数据类型,甚至可以是其他结构。
- 可以把结构声明放在任何函数的外部,也可以放在一个函数定义的内部。如果结构声明置于一个函数内部,它的标记只能在该函数内部使用。如果是外部声明,它可以被本文件中该声明之后的所有函数使用。
C结构使您可以在一个单独的单元中,收集对象的多种信息。这对组织一个程序非常有用,因为这样可以把所有相关信息存储在一个地方,而不是存储在分散的多个变量中。
在设计结构时,开发一个与之配套的函数包通常是很有用的。例如,写一个以结构为参数的显示函数,比起每次要显示结构内容时写一堆printf()语句要强的多。而且,如果给结构添加一个成员,只需重写函数,而不用改变函数调用。
14.3 定义结构变量
- 词语“结构”有两个意思。一个意思是上一节讨论的“结构设计”,结构设计告诉编译器如何表示数据,但是它没有让计算机为数据分配空间;另一个是“结构变量”。
- 声明结构的过程和定义结构变量的过程可以被合并成一步。将声明和变量定义合并在一起,是不需要使用标记的一种情况。
- 如果你想多次使用一个结构模板,就需要使用带标记的形式;或者您也可以使用本章后面部分将要讲到的typedef。
- 初始化结构可以使用与初始化数组相似的语法,简言之,使用一个用花括号括起来的、逗号分隔的初始化项目列表进行初始化。每个初始化项目必须和要初始化的结构成员类型相匹配。
- 如果初始化一个具有静态存储时期(比如静态外部链接、静态内部链接或静态空链接)的变量,只能使用常量值。这条规则同样适用于结构。如果初始化一个具有静态存储时期的结构,初始化项目列表中的值必须是常量表达式。
- 初始化静态存储时期的变量,
必须用常量表达式的原因:如果允许非常量表达式(如运行时才能确定的值),编译器无法在程序运行前完成初始化,这与静态存储期的语义矛盾
。下面是静态存储期变量的初始化时机:
- 编译期初始化:如果使用常量表达式初始化,编译器会直接将初始值写入二进制文件的.data段(已初始化数据段)。
- 零初始化:如果未显式初始化,则会被系统自动初始化为零(或空指针/nullptr)。
- 初始化静态存储时期的变量,
- 用结构成员运算符点(.)访问成员。点(.)拥有比&更高的优先级。
- C99支持结构的指定初始化项目,其语法与数组的指定初始化项目相似。只是,结构的指定初始化项目使用点运算符和成员名(而不是方括号和索引值)来标识具体的元素。
- 正如数组一样,跟在一个指定初始化项目之后的常规初始化项目为跟在指定成员后的成员提供了初始值。
注意,静态存储时期变量的初始化时机。
14.4 结构数组
14.5 嵌套结构
14.6 指向结构的指针
- 至少有三个原因可以解释为什么使用指向结构的指针是个好注意:
- (1)就像指向数组的指针比数组本身更容易操作一样,指向结构的指针通常比结构本身更容易操作。
- (2)在早期C实现中,结构不能作为参数被传递给函数,但指向结构的指针可以。
- (3)许多奇妙的数据表示都使用了包含指向其他结构的指针的结构。
- 和数组不同,一个结构的名字不是该结构的地址,必须使用&运算符。
在一些系统中,结构的大小有可能大于它内部各成员大小之和,因为系统对数据的对齐存储需求会导致缝隙
。
- 使用指针访问结构成员:
- (1)最常用的方法:使用一个新运算符->,后跟->运算符的结构指针和后跟.(点)运算符的结构名是一样的。
- (2)&和是一对互逆的运算符,使用间接运算符结合.(点)运算符。注意,间接运算符*要放在括号内,因为点运算符比间接运算符优先级高。
14.7 向函数传递结构信息
- ANSI C 允许把结构作为参数传递,或把指向结构的指针作为参数传递。如果只关心结构的一部分,还可以将结构成员作为参数传递给函数。
- 现在的C允许把一个结构赋值给另一个结构,即使结构有一个成员是数组也照样能完成赋值。
- 结构不仅可以作为参数传递给函数,也可以作为函数返回值返回。
- 用结构、还是指向结构的指针作为参数?
- (1)把指针作为参数的方法的两个 优点是:它既工作在较早的C实现上,也工作在较新的C实现上,而且执行起来很快;只须传递一个单个地址。缺点是缺少对数据的保护。不过,ANSI C中新增的const限定词解决了这个问题。
- (2)把结构作为参数传递的一个 优点是函数处理的原始数据的副本,比直接处理原始数据安全。缺点是早期C实现可能不处理这种代码,并且这样做浪费时间和空间。把很大的结构传递给函数,但函数只使用一个或两个结构成员,这尤其浪费时间和空间。
- 通常,程序员为了追求效率而使用结构指针作为函数参数;当需要保护数据、防止意外改变数据时对指针使用const限定词。传递结构值是处理小型结构最常用的方法。
- 在结构中使用字符数组还是字符指针?
- (1)如果需要结构存储字符串,使用字符数组成员。
- (2)
结构中的指针应该用来管理那些已创建的而且在程序其他地方已经分配过空间的字符串
。结构中的指针也要初始化后才能使用,否则可能会破坏程序的数据。
- 复合文字和结构
- C99新增的复合文字的特性不仅适用于数组,也适用于结构。可以用复合文字创建一个被用来作为函数参数或被赋值给另一个结构的结构。语法是把类型名写在圆括号中,后跟一个用花括号括起来的初始化项目列表。
如果函数需要一个地址,可以把一个复合文字的地址传递给它
。- 出现在所有函数外面的复合文字具有静态存储时期,而出现在一个代码块内部的复合文字具有自动存储时期。适用于常规初始化项目列表的语法规则同样也适用于复合文字。这意味着,可以在复合文字中使用指定初始化项目。
- 声明一个伸缩型数组成员的规则:
- (1)伸缩型数组成员
必须是最后一个数组成员
。 - (2)结构中必须
至少有一个其他成员
。 - (3)
伸缩型数组就像普通数组一样被声明,除了它的方括号内是空的
。
伸缩型数组成员的特性:第一、它不存在,至少不立即存在;第二、您可以编写适当的代码使用这个伸缩型数组成员,就像它确实存在并且拥有您需要的任何数目的元素一样
。C99的意图并不是让您声明包含伸缩型数组成员的结构的变量,而是希望您声明一个指向包含伸缩型数组成员的结构的指针,然后用malloc()来分配足够的空间,以存放结构的常规内容和伸缩型数组成员需要的任何额外空间
。- 如果你用包含伸缩型数组成员的结构声明了一个变量,这个结构变量中伸缩型数组成员是不存在的,没有给它分配任何空间,无法使用。
14.8 把结构内容保存到文件中
- 由于结构可以保存多种多样的信息,所以它是建立数据库的重要工具。一个数据库文件能够包含任意数目的此类数据对象。一个结构中保存的整套信息用术语来称就是一个
记录
,单个项目称为字段
。 - 使用fread()和fwrite()函数把结构保存到文件中,是因为这两个函数可以一次性读写整个记录(结构)而不是字段。
- 读取和写入固定大小的结构是最简单的建立数据库的方法。另一种方法是使用不定大小的记录。为了便于从文件中读出这样的记录,每个记录可以用一个数值字段开始,这个字段指明该记录的大小。这种方法复杂一些,涉及到我们接下来要讲的“链接结构”以及下一章要讨论的动态存储分配。
阅读本小节时,让我想通了做上一章编程练习时一个不太明白的点:当你以二进制模式打开一个文件,然后用fwrite()函数将数据写入一个文本文件,如果数据是字符,可以正常显示;如果数据是数字,那可能显示乱码。因为不管是字符还是数字,fwrite()都是以二进制形式保存到文件,而文本文件默认是把二进制数据转换成字符显示的,数字的二进制值可能没有对应的字符,那就只能显示乱码了
。
14.9 结构:下一步是什么
结构多种应用中的一种:创建新的数据形式。计算机用户已经开发出一些比我们提到过的数组和简单结构更适用于特定问题的数据形式。这些形式的名称有队列、二叉树、堆、哈希表和图。许多这样的形式是由链接结构组成的。典型地,每个结构包括一或两项数据,再加上一或两个指向其他相同类型结构的指针。
&esnp; 现在终于对链表、树、哈希表等有了大致的概念了,以前看到这些术语,很模糊。没看这小节之前,我甚至觉得链表和队列、二叉树、堆、哈希表等是同一类,没想到是用链表构成这些数据形式。这下就通透了,感觉我要是看完数据结构那本书,可以去研究算法了。
14.10 联合简介
- 联合是能在同一个存储空间里(但不同时)存储不同类型数据的数据类型。
- 联合是以与结构同样的方式建立的,也是需要有一个联合模板和一个联合变量,可以在一步中定义它们,也可以用联合标记在两步中定义。
- 因为联合只存储一个值,所以初始化的规则与结构的初始化不同。具体地,有3种选择:可以把一个联合初始化为同样类型的另一个联合;可以初始化联合的第一个元素;或者,按照C99标准,可以使用一个指定初始化项目。
- 如同与指向结构的指针一起使用->运算符一样,可以与指向联合的指针一起来使用->运算符。
- 使用一个成员来将值保存到一个联合中,然后使用一个不同的成员来查看这些内容,这种做法有时会很有用。
14.11 枚举类型
使用枚举类型声明代表整数常量的符号名称
。通过使用关键字enum,可以创建一个新“类型”并指定它可以具有的值(实际上,enum常量是int类型的
,因此在使用int类型的任何地方都可以使用它)。枚举类型的目的是提高程序的可读性。它的语法与结构语法相同。- 默认时,枚举列表中的常量被指定为整数0、1、2等等。可以选择常量具有的整数值,只须在声明中包含期望的值。如果只对一个常量赋值,没有对后面的常量赋值,那么后面的常量会被赋予后续的值。
- 虽然枚举常量都是int类型的,但枚举变量较宽松地限定为任一种整数类型,只要该整数类型能保存这些枚举常量。
- C的某些枚举属性不能延至C++中。例如,C允许对枚举变量使用运算符++,而C++不允许。如果你的代码可能会被加入C++中,那么不应该声明枚举类型变量,而是声明为普通int类型变量。
- 枚举类型是一个整数类型,所以enum变量能像整数变量那样被用在表达式中。
- C使用术语名字空间来表示识别一个名字的程序部分。
- 名字空间是分类别的。在一个特定作用域内的结构标记、联合标记以及枚举标记都共享同一个名字空间,并且这个名字空间跟普通变量使用的名字空间是不同的。这就意味着在同一作用域内对一个变量和一个标记使用同一个名字,不会产生错误。但不能在同一作用域内使用名字相同的两个标记或名字相同的两个变量。
- C语言中,两种不同的方式使用同一标识符虽然合法但会造成混乱。C++不支持这种用法,因为C++把标记和变量名放在同一名字空间中。
14.12 typedef简介
- typedef工具是一种高级数据特性,它使您能够为某一类型创建您自己的名字。它和#define相似,但是它们具有3个不同之处。
- (1)与#define不同,
typedef给出的符号名称仅限于对类型,而不是对值
。 - (2)
typedef的解释由编译器,而不是预处理器执行
。 - (3)虽然它的范围有限,但是在受限范围内,typedef比#define更灵活。
- typedef的作用域取决于typedef语句所在的位置。如果定义是在一个函数内部,它的作用域就是局部的,限定在那个函数里。如果定义是在函数外部,它将具有全局作用域。
- 通常typedef的定义使用大写字母,以提醒用户这个类型名称实际上是一个符号缩写。不过,您也可以使用小写字母。
- 使用typedef来命名一个结构类型时,可以省去结构的标记。
- typedef经常被用来处理复杂的类型。
使用typedef时,要记住它并不创建新的类型,它只是创建了便于使用的标签
。
本小节的一段话“C标准规定sizeof和time()应返回整数类型,但它留给具体的实现来决定到底是哪种类型。不进行指定的原因是ANSI C委员会觉得没有一个对所有计算机平台来说都是最好的选择”。让我理解了typedef在提高代码可移植性的作用。不同的平台最好的选择不同,通过typedef创建一个统一的标签,各个平台实现自己最好的选择。可能还需要结合16章预处理器相关知识来实现代码的可移植性。
14.13 奇特的声明
搞清楚修饰符*、()、[]的意义,和它们的优先级就能清楚复杂的数据类型具体是什么了。
14.14 函数和指针
- 函数也有地址,这是因为函数的机器语言实现是由载入到内存的代码组成。指向函数的指针保存着函数代码起始处的地址。
- 当声明一个数据指针时,必须声明它指向的数据的类型。当声明一个函数指针时,必须声明它指向的函数类型。要指定函数类型,就要指出函数的返回类型以及函数的参量类型。
- 声明一个指向特定函数类型的指针,首先声明一个该类型的函数,然后用(*pf)形式的表达式代替函数名称;pf就成为可指向那种类型函数的指针。如果你完全理解函数指针,你可以直接写。
- 有了函数指针之后,可以把适当类型的函数的地址赋给它。在这种场合中,函数名可以用来表示函数的地址。
- 正像可以用一个数据指针来访问数据一样,也可以使用函数指针来访问函数。奇怪的是,有两个逻辑上不一致的语法规则来实现这样的操作。
- (1)第一种方法:因为pf指向ToUpper函数,*pf就是ToUpper函数,
因为表达式(*pf)(mis)与ToUpper(mis)一样
。 - (2)第二种方法:因为函数名是一个指针,可以互换地使用指针和函数名,
因此pf(mis)ToUpper(mis)一样
。
- 函数指针典型的用法是,一个函数指针可以作为另一个函数的参数,告诉第二个函数使用哪一个函数。
不能拥有一个函数的数组,但可以拥有一个函数指针的数组
。
字符串处理函数strchr()的一个巧用:while(strchr(“ulton”,ans)==NULL)与while(ans!=‘u’&&ans!=‘l’&&ans!=‘t’&&ans!=‘o’&&ans!=‘n’)作用相同。显然前面的更巧妙、简洁。
14.18 编程练习
其他的练习挺简单的,函数指针稍微复杂点,就不发代码了。
习题7
程序的时序图
题目代码
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <ctype.h>
#include <string.h>#define M 20
#define N 100struct book
{char title[M];char author[M];float value;bool present;bool modify;
};
int filecount=0; //文件中原有记录数
int endlabel=0; //结构数组中保存的记录数
int filepos; //文件记录最后的位置
struct book library[N]; //结构数组保存书籍信息char getfirst(void);
void getrecords(FILE *);
void showmodel(void);
void addmodel(void);
void deletemodel(void);
void modifymodel(void);
void searchmodel(void);
void updatemodel(FILE *);int main(void)
{// 我用的Editplus写代码,需要调用此函数让控制台能支持中文SetConsoleCP(65001); // 设置标准输入的编码方式为UTF-8SetConsoleOutputCP(65001); //设置控制台输出的编码方式为UTF-8FILE * fbrec;char select;if ((fbrec=fopen("library.bin","r+b"))==NULL){fprintf(stdout,"文件打开失败,程序结束!!!\n");exit(1);}getrecords(fbrec);do{showmodel();printf("根据菜单,输入选择:\n");select=getfirst();while (endlabel==0&&(select=='d'||select=='m'||select=='s')){printf("文件记录为空,不能选择删除、修改、查询。重新输入:\n");select=getfirst();}switch (select){case 'a':addmodel();break;case 'd':deletemodel();break;case 'm':modifymodel();break;case 's':searchmodel();break;default:break;}}while (select!='q');updatemodel(fbrec);fclose(fbrec);return 0;
}char getfirst() //获取缓冲区首个字母,且必须为菜单选择之一
{char select; do{select=getchar();if (tolower(select)!='a'&&tolower(select)!='d'&&tolower(select)!='m'&&tolower(select)!='s'&&tolower(select)!='q'){printf("只能输入字符(admsq)之一,重新输入:\n");}while (getchar()!='\n')continue;}while (tolower(select)!='a'&&tolower(select)!='d'&&tolower(select)!='m'&&tolower(select)!='s'&&tolower(select)!='q');return select;
}void getrecords(FILE * fbrec) //初始化结构数组
{int i=0;fseek(fbrec,0L,SEEK_END);if (ftell(fbrec)!=0){rewind(fbrec); //回到文件头开始读取while (fread(library[i].title,sizeof(library[i].title),1,fbrec)==1&&feof(fbrec)==0){fread(library[i].author,sizeof(library[i].author),1,fbrec);fread(&library[i].value,sizeof(library[i].value),1,fbrec);fread(&library[i].present,sizeof(library[i].present),1,fbrec);library[i].modify=false;i++;endlabel++;filecount++;}filepos=ftell(fbrec);}}
void showmodel() //显示数据和菜单
{int i;printf("********************文件记录********************\n\n");if (endlabel==0){fprintf(stdout,"文件记录中没有数据!!!!\n");}else{for (i=0;i<endlabel ;i++ ){if (library[i].present){fprintf(stdout,"%d. title:%s author:%s value:%f\n",i+1,library[i].title,library[i].author,library[i].value);}else{fprintf(stdout,"%d.\n",i+1);}}}printf("\n********************文件记录********************\n\n");printf("**********************菜单**********************\n\n");printf("a.增加记录 d.删除记录 m.修改记录 s.查找记录\n");printf("q.退出\n");printf("\n**********************菜单**********************\n");
}void addmodel() //添加模块
{int i;char title[M];char author[M];float value;printf("输入书名:\n");gets(title);printf("输入作者名:\n");gets(author);printf("输入价格:\n");scanf("%f",&value);while (getchar()!='\n'){continue;}for (i=0;i<endlabel ;i++ ) //判断是否有被删除的空行{if (library[i].present==false){break;}}if (i<endlabel) //内容添加到空行中{strcpy(library[i].title,title);strcpy(library[i].author,author);library[i].value=value;library[i].present=true;library[i].modify=true; printf("\n\n增加完毕!!!\n\n");}else if (endlabel<N) //结构数组未满,追加到结构数组后面{strcpy(library[endlabel].title,title);strcpy(library[endlabel].author,author);library[endlabel].value=value;library[endlabel].present=true;library[endlabel].modify=true; endlabel++; //结构数组中从这个下标开始,更新时要追加到文件后面printf("\n\n增加完毕!!!\n\n");}else{printf("结构数组已满,无法更新!!!\n");}}void deletemodel() //删除模块
{unsigned line;printf("输入(1-%d)之间要删除的行,或输入0放弃删除:\n",endlabel);do{while (scanf("%u",&line)!=1){printf("输入一个无符号整数:\n");while(getchar()!='\n')continue;}if (line<0||line>endlabel){printf("输入(1-%d)之间的数:\n",endlabel);}while(getchar()!='\n')continue;}while (line<0||line>endlabel);if (line==0){printf("\n\n放弃删除行!!!\n\n");}else{library[line-1].present=false; //标志位,表示该行被删除library[line-1].modify=true; //标志位,表示该行需要被更新printf("\n\n删除%u行完毕!!!\n\n",line);}}void modifymodel() //修改模块 跟删除模块类似,不赘述了
{unsigned line;char title[M];char author[M];float value;printf("输入(1-%d)之间要修改的行,或输入0放弃修改:\n",endlabel);do{while (scanf("%u",&line)!=1){printf("输入一个无符号整数:\n");while(getchar()!='\n')continue;}if (line<0||line>endlabel){printf("输入(1-%d)之间的数:\n",endlabel);continue;}else if (line!=0&&library[line-1].present==false){printf("%u行为空行,重新输入:\n",line);continue;}}while (line<0||line>endlabel);while(getchar()!='\n')continue;if (line==0){printf("\n\n放弃修改行!!!\n\n");}else{printf("输入书名:\n");gets(title);printf("输入作者名:\n");gets(author);printf("输入价格:\n");scanf("%f",&value);while (getchar()!='\n'){continue;}strcpy(library[line-1].title,title);strcpy(library[line-1].author,author);library[line-1].value=value;library[line-1].present=true;library[line-1].modify=true; printf("\n\n修改%u行完毕!!!\n\n",line);}}void searchmodel()
{unsigned line;printf("输入(1-%d)之间要查看的行,或输入0放弃查询:\n",endlabel);do{while (scanf("%u",&line)!=1){printf("输入一个无符号整数:\n");while(getchar()!='\n')continue;}while(getchar()!='\n')continue;if (line<0||line>endlabel){printf("输入(1-%d)之间的数:\n",endlabel);}}while (line<0||line>endlabel);if (line==0){printf("\n\n放弃查询!!!\n\n");}else{if (library[line-1].present==false){printf("%u. 此行为空!!!\n",line);}else{printf("%u. title:%s author:%s value:%f\n",line,library[line-1].title,library[line-1].author,library[line-1].value);}printf("\n\n%u行查询完毕!!!\n\n",line);}}void updatemodel(FILE * fbrec) //更新模块
{int i;for (i=0;i<endlabel ;i++ ){if (i<filecount&&library[i].modify==true) //找出原文件中被修改的部分进行更新{rewind(fbrec);fseek(fbrec,(long)(i*(sizeof(library[i].title)+sizeof(library[i].author)+sizeof(library[i].value)+sizeof(library[i].present))),SEEK_SET);fwrite(library[i].title,sizeof(library[i].title),1,fbrec);fwrite(library[i].author,sizeof(library[i].author),1,fbrec);fwrite(&library[i].value,sizeof(library[i].value),1,fbrec);fwrite(&library[i].present,sizeof(library[i].present),1,fbrec);}if (i>=filecount) //通过菜单中增加选项,对超出了原文件空间部分进行更新。其实最简单的就是把结构数组整个//写入文件,我觉得太傻了,就给自己找了点麻烦。{fseek(fbrec,filepos,SEEK_SET);fwrite(library[i].title,sizeof(library[i].title),1,fbrec);fwrite(library[i].author,sizeof(library[i].author),1,fbrec);fwrite(&library[i].value,sizeof(library[i].value),1,fbrec);fwrite(&library[i].present,sizeof(library[i].present),1,fbrec);filepos=ftell(fbrec);}}
}
花了很长时间,做这章的练习是花的时间最多的,没有项目经验,只能有机会就尽可能地把题目地功能完善,借此提高自己的编码能力,所以花了很长时间。时间花的多也有收获。比如,开发要优先通过参数传参,不能因为文件作用域变量,访问方便就滥用全局变量,参数能降低函数的耦合,方便函数测试,出错了也好查找错误,如果程序再大点,全局变量一多,出错了查找都难。参数也有弊端,函数参数一多,阅读不方便,而且参数占用空间再大点,消耗的栈空间就多了。总之用全局变量,还是参数传参要根据具体情况分析,参数优先。
提高了我规划设计一些简单程序的技巧。划分功能模块,针对模块开发,不要被其他模块影响的能力还要练习。即使一开始设计的模块不完美,在不断的完善功能模块的过程中也会,发现缺陷,然后再回头去完善模块就行。注意模块中数据的改变,会不会对其他模块产生影响,感觉这跟模块的封装有关。相信自己,不着急,一步步来很重要。
习题8、9
程序的时序图
分配座位模块的流程图
取消座位模块的流程图
题目代码
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <ctype.h>
#define M 20
#define N 12
#define P 4struct passenger
{char first[M];char last[M];
};
struct seat
{char id;bool available;struct passenger name;
};struct seat ffl[P][N];
struct seat sele[N]; //保存选择座位时待确认的座位信息
char cancel[N]; //保存待取消的座位信息
unsigned selCount; //待确认座位信息的个数
unsigned canCount; //待取消座位信息的个数void getData(FILE *);
unsigned getNum(void);
char getAlpha(void);
char getSeat(void);
void flightMenu(void);
void seatMenu(int);
int getEmptyNum(unsigned);
void showEmpty(unsigned);
void showAll(unsigned);
void assignSeat(unsigned);
void cancelSeat(unsigned);
void updateSeat(unsigned,FILE *);int main(void)
{// 我用的Editplus写代码,需要调用此函数让控制台能支持中文SetConsoleCP(65001); // 设置标准输入的编码方式为UTF-8SetConsoleOutputCP(65001); //设置控制台输出的编码方式为UTF-8FILE * fs;unsigned fnum,input;char choose;if ((fs=fopen("flights.dat","r+b"))==NULL){printf("打开文件失败,程序结束!!!\n");exit(1);}getData(fs);flightMenu();printf("根据菜单输入(1-5)选择航班或结束程序:\n");while ((input=getNum())!=5){switch (input){case 1:input=0;fnum=102;break;case 2:input=1;fnum=311;break;case 3: input=2;fnum=444;break;case 4:input=3;fnum=519;break;default:break;}seatMenu(fnum);printf("输入(A-G),选择功能:\n");while ((choose=getAlpha())!='G'){printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n");switch (choose){case 'A':printf("该航班剩余空座位数是:%d\n",getEmptyNum(input));break;case 'B':showEmpty(input);break;case 'C':showAll(input);break;case 'D':assignSeat(input);break;case 'E':cancelSeat(input);break;case 'F':updateSeat(input,fs);break;default:break;}printf("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n");seatMenu(fnum);printf("输入(A-G),选择功能:\n");}selCount=0; //选择座位或取消座位后,如果不报错默认为放弃操作,这时如果不将selCount和canCount清0,canCount=0; //误操作点击更新后会将放弃的数据写入结构数组和文件中flightMenu();printf("根据菜单输入(1-5)选择航班或结束程序:\n");}fclose(fs);return 0;
}void getData(FILE * fs) //初始化二维结构数组
{long pos;int i,j;int a;fseek(fs,0L,SEEK_END);pos=ftell(fs);if (pos==0) //数据文件为空,初始化数组中seat结构的ID和available{for (i=0;i<P ;i++ ){for (j=0;j<N ;j++ ){ffl[i][j].id='A'+j;ffl[i][j].available=true;}}rewind(fs);fwrite(ffl,sizeof(struct seat),N*P,fs);fflush(fs);}else{rewind(fs);fread(ffl,sizeof(struct seat),N*P,fs);}
}unsigned getNum() //获得一个1-5之间的数字
{unsigned num;do{while (scanf("%u",&num)!=1){printf("输入不合法,重新输入(1-5)之间的一个数字:\n");while(getchar()!='\n')continue;}if (num<1||num>5){printf("输入数字不在(1-5)之间,重新输入:\n");}while(getchar()!='\n')continue;}while (num<1||num>5);return num;
}
char getAlpha() //座位菜单选项 获得A-G之间的一个字符
{char ch;do{scanf("%c",&ch);while(getchar()!='\n')continue;if (toupper(ch)<'A'||toupper(ch)>'G'){printf("输入的字符不合法,输入(A-G)之间的字符:\n");}}while (toupper(ch)<'A'||toupper(ch)>'G');return toupper(ch);
}char getSeat() //座位号 获得一个A-L之间的字符或者字符Q
{char ch;do{scanf("%c",&ch);while(getchar()!='\n')continue;if ((toupper(ch)!='Q')&&(toupper(ch)<'A'||toupper(ch)>'L')){printf("输入的字符不合法,输入(A-L)之间的字符或字符Q:\n");}}while ((toupper(ch)!='Q')&&(toupper(ch)<'A'||toupper(ch)>'L'));return toupper(ch);
}void flightMenu() //显示航班菜单
{printf("***********航班菜单************\n");printf("1.航班102\n");printf("2.航班311\n");printf("3.航班444\n");printf("4.航班519\n");printf("5.结束程序\n");printf("***********航班菜单************\n\n");}void seatMenu(int fnum) //显示座位菜单
{printf(" >>>>航班%d<<<<\n",fnum);printf("***********座位菜单************\n");printf("A)显示空座位数量\n");printf("B)显示空座位列表\n");printf("C)按字母表顺序显示所有座位信息\n");printf("D)选择空座位\n");printf("E)取消座位\n");printf("F)确认座位信息修改\n");printf("G)返回航班菜单\n");printf("***********座位菜单************\n\n");
}
int getEmptyNum(unsigned input) //获得该航班空座位数量
{int i,count;count=0;for (i=0;i<N ;i++ ){if (ffl[input][i].available==true){count++;}}return count;
}void showEmpty(unsigned input) //显示空座位号
{int i;selCount=0; //选择座位或取消座位后,如果不报错默认为放弃操作,这时如果不将selCount和canCount清0,canCount=0; //误操作点击更新后会将放弃的数据写入结构数组和文件中printf("该航班的空位有:");for (i=0;i<N ;i++ ){if (ffl[input][i].available==true){printf("%4c",'A'+i);}}printf("\n");
}void showAll(unsigned input) //显示航班所有座位信息
{int i;selCount=0; //选择座位或取消座位后,如果不报错默认为放弃操作,这时如果不将selCount和canCount清0,canCount=0; //误操作点击更新后会将放弃的数据写入结构数组和文件中printf("该航班的所有座位信息如下:\n");printf("座位号 是否空 乘客名\n");for (i=0;i<N ;i++ ){if (ffl[input][i].available==true){printf("%4c 是 \n",'A'+i);}else{printf("%4c 否 %s %s\n",'A'+i,ffl[input][i].name.first,ffl[input][i].name.last);}}
}void assignSeat(unsigned input) //选择座位
{int i;char ch;selCount=0;canCount=0;showEmpty(input);printf("输入一个座位号(A-L)或Q退出:\n");while ((ch=getSeat())!='Q'){for (i=0;i<N ;i++ ) //判断航班中该座位是不是空的{if (ffl[input][i].id==ch){if (ffl[input][i].available==false){printf("该座位非空座,按空座位表重新输入:\n");break;}}}if (i!=N){continue;}for (i=0;i<selCount ;i++ ) //判断该座位是不是已被选中了{if (sele[i].id==ch){printf("该座位已被选中,从空座位表选择其他座位:\n");break;}}if(i!=selCount){continue;}sele[selCount].id=ch; //座位相关信息保存到待确认座位结构数组sele[N]中sele[selCount].available=false;printf("输入乘客姓:\n");gets(sele[selCount].name.first);printf("输入乘客名:\n");gets(sele[selCount].name.last);selCount++; //待确认座位数自增printf("输入一个座位号(A-L)或Q退出:\n");}
}void cancelSeat(unsigned input) //取消座位
{int i;char ch;canCount=0;selCount=0;showAll(input);printf("输入要取消的座位号(A-L)或Q退出:\n");while ((ch=getSeat())!='Q'){for (i=0;i<N ;i++ ) //判断航班中该座位是不是空的{if (ffl[input][i].id==ch){if (ffl[input][i].available==true){printf("该座位为空,无法取消,按座位表重新输入:\n");break;}}}if (i!=N){continue;}for (i=0;i<canCount ;i++ ) //判断该座位是不是已被选中了{if (cancel[i]==ch){printf("该座位已准备取消,从座位表选择其他座位:\n");break;}}if(i!=canCount){continue;}cancel[canCount]=ch; //要取消的座位号保存到带取消座位数组cancel[N]中canCount++; //待取消座位数自增printf("输入要取消的座位号(A-L)或Q退出:\n");}
}void updateSeat(unsigned input,FILE * fs)
{int i,j;if (selCount>0) //更新选座信息{for (i=0;i<selCount ;i++ ){for (j=0;j<N ;j++ ){if (sele[i].id==ffl[input][j].id){ffl[input][j]=sele[i]; //将选中的座位更新到二维结构数组fseek(fs,(long)((input*N+j)*sizeof(struct seat)),SEEK_SET); //定位数据文件中的位置fwrite(sele+i,sizeof(struct seat),1,fs); //将选中的座位写入到数据文件中}}}printf("选座完成!!!\n");}if (canCount>0) //更新取消座位信息{for (i=0;i<canCount ;i++ ){for (j=0;j<N ;j++ ){if (cancel[i]==ffl[input][j].id){ffl[input][j].available=true; //将二维数组中对应位置设置为空座fseek(fs,(long)((input*N+j)*sizeof(struct seat)),SEEK_SET); //定位数据文件中的位置fwrite(ffl[input]+j,sizeof(struct seat),1,fs); //将选中的座位写入到数据文件中}}}printf("取消座位完成!!!\n");}fflush(fs);
}
8、9两题我合在一起写的。有第7题的经验在,所以设计功能模块很简单,画时序图和流程图花了不少时间,这两个图挺好用的。我发现自己看待问题时,分不清什么时候该考虑整体,什么时候该抠细节。该从各个功能模块的角度考虑问题时,总是因为突然想到了某个模块内部的问题,陷进去抠细节。把自己脑子搞得很乱,思考没有逻辑性。 下次再碰到稍微复杂的练习,注意一下这个问题,找到一个自己可行的思考解决问题的方式。