第十一章 结构体与共同体 11.1 概述 C语言程序中少量变化的数据用变量来处理。数量不宜多。 批量同类型数据的处理用数组。 不同类型的数据的集合用什么数据结构来存放呢?这就是本单元要介绍的内容:用结构体类型处理不同类型数据的集合。 声明一个结构体类型的一般形式为: struct 结构体名 {成员表列}; 如: struct student //student为结构体名 { int num; //int为类型名,num为成员名 }; 11.2 定义结构体类型变量的方法 (1)间接定义法──先定义结构类型、再定义结构变量 例如,定义的学生信息结构类型std_info,定义了一个相应的结构变量student: struct std_info student; 结构变量student:拥有结构类型的全部成员,其中birthday成员是一个日期结构类型,它又由3个成员构成。 注意:使用间接定义法定义结构变量时,必须同时指定结构类型名。 (2)直接定义法──在定义结构类型的同时,定义结构变量 例如,结构变量student的定义可以改为如下形式: struct std_info {…… } student; 同时定义结构类型及其结构变量的一般格式如下: struct [结构类型名] { …… } 结构变量表; 2.说明 (1)结构类型与结构变量是两个不同的概念,其区别如同int类型与int型变量的区别一样。 (2)结构类型中的成员名,可以与程序中的变量同名,它们代表不同的对象,互不干扰。 (3)“结构类型名”和“数据项”的命名规则,与变量名相同。 (4)数据类型相同的数据项,既可逐个、逐行分别定义,也可合并成一行定义。 例如,本案例代码中的日期结构类型,也可改为如下形式: struct date { int year, month, day; }; (5)结构类型中的数据项,既可以是基本数据类型,也允许是另一个已经定义的结构类型。 例如,P284 (6)本书将1个数据项称为结构类型的1个成员(或分量)。 11.3结构体变量的引用 引用结构体变量中成员的方式为 结构体变量名.成员名 例如, student1.num表示student1变量中的num成员,即student1的num(学号)项。可以对变量的成员赋值,例如:student1.num=10010;“.”是成员(分量)运算符,它在所有的运算符中优先级最高,因此可以把student1.num作为一个整体来看待。上面赋值语句的作用是将整数10010赋给student1变量中的成员num。 在定义了结构体变量以后,当然可以引用这个变量。但应遵守以下规则: (1)不能将一个结构体变量作为一个整体进行输入和输出。 例如: 已定义student1和student2为结构体变量并且它们已有值。 printf(″%d,%s,%c,%d,%f,%\n″,student1); 但不能用以下语句整体读入结构体变量, 例如: scanf(″%d,%s,%c,%d,%f,%s″,&student1);
结构体变量的地址主要用作函数参数, 传递结构体变量的地址。 11.4 结构变量的初始化 结构变量初始化的格式,与一维数组相似: 结构变量={初值表} 不同的是:如果某成员本身又是结构类型,则该成员的初值为一个初值表。 例如,student={"000102", "张三", "男", {1980,9,20}}。 注意:初值的数据类型,应与结构变量中相应成员所要求的一致,否则会出错。 11.5 结构体数组 一个结构体变量中可以存放一组数据(如一个学生的学号、姓名、成绩等数据)。如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。结构体数组与以前介绍过的数值型数组不同之处在于每个数组元素都是一个结构体类型的数据,它们都分 别包括各个成员(分量)项。与结构变量的定义相似,结构数组的定义也分直接定义和间接定义两种方法,只需说明为数组即可。与普通数组一样,结构数组也可在定义时进行初始化。初始化的格式为: 结构数组[n]={{初值表1},{初值表2},...,{初值表n}} 11.5.3 结构体数组应用举例 例11.2对候选人得票的统计程序。设有3个候选人,每次输入一个得票的候选人的名字,要求最后输出各人得票结果。 11.6 指向结构体类型数据的指针 一个结构体变量的指针就是该变量所占据的内存段的起始地址。可以设一个指针变量,用来指向一个结构体变量,此时该指针变量的值是结构体变量的起始地址。指针变量也可以用来指向结构体数组中的元素。 通过指向结构变量的指针来访问结构变量的成员,与直接使用结构变量的效果一样。一般地说,如果指针变量pointer已指向结构变量var,则以下三种形式等价: (1)var.成员 (2)pointer->成员 (3)(*pointer).成员 /* “*pointer”外面的括号不能省!*/ 注意:在格式(1)中,分量运算符左侧的运算对象,只能是结构变量,;而在格式(2)中,指向运算符左侧的运算对象,只能是指向结构变量(或结构数组)的指针变量,否则都出错。 11.7 用指针处理链表 11.7.1 链表概述 链表是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。我们知道,用数组存放数据时,必须事先定义固定的长度(即元素个数)。比如,有的班级有100人,而有的班只有30人,如果要用同一个数组先后存放不同班级的学生数据,则必须定义长度为100的数组。如果事先难以确定一个班的最多人数,则必须把数组定得足够大,以能存放任何班级的学生数据。显然这将会浪费内存。链表则没有这种缺点,它根据需要开辟内存单元。图11.10表示最简单的一种链表(单向链表)的结构。
图11.10 链表有一个“头指针”变量,图中以head表示,它存放一个地址。该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应包括两个部分:一为用户需要用的实际数据,二为下一个结点的地址。可以看出,head指向第一个元素;第一个元素又指向第二个元素……直到最后一个元素,该元素不再指向其他元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”),链表到此结束。 可以看到链表中各元素在内存中可以不是连续存放的。要找某一元素,必须先找到上一个元素,根据它提供的下一元素地址才能找到下一个元素。如果不提供“头指针”(head),则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。打个通俗的比方:幼儿园的老师带领孩子出来散步,老师牵着第一个小孩的手,第一个小孩的另一只手牵着第二个孩子……这就是一个“链”,最后一个孩子有一只手空着,他是“链尾”。要找这个队伍,必须先找到老师,然后顺序找到每一个孩子。 可以看到,这种链表的数据结构,必须利用指针变量才能实现。即:一个结点中应包含一个指针变量,用它存放下一结点的地址。 前面介绍了结构体变量,用它作链表中的结点是最合适的。一个结构体变量包含若干成员,这些成员可以是数值类型、字符类型、数组类型,也可以是指针类型。我们用这个指针类型成员来存放下一个结点的地址。例如,可以设计这样一个结构体类型: struct student { int num; float score; struct studentnext; }; 其中成员num和score用来存放结点中的有用数据(用户需要用到的数据),相当于图11.10结点中的A,B,C,D。next是指针类型的成员,它指向struct student类型数据(这就是next所在的结构体类型)。一个指针类型的成员既可以指向其他类型的结构体数据,也可以指向自己所在的结构体类型的数据。现在,next是struct student类型中的一个成员,它又指向struct student类型的数据。用这种方法就可以建立链表。见图11.11。
图中每一个结点都属于struct student类型,它的成员next存放下一结点的地址,程序设计人员可以不必具体知道各结点的地址,只要保证将下一个结点的地址放到前一结点的成员next中即可。请注意:上面只是定义了一个struct student类型,并未实际分配存储空间。只有定义了变量才分配内存单元。 11.7.2 简单链表 下面通过一个例子来说明如何建立和输出一个简单链表。 例11.7 建立一个如图11.11所示的简单链表,它由3个学生数据的结点组成。输出各结点中的数据。 #define NULL 0 struct student { long num; float score; struct student *next; }; main() { struct student a,b,c,*head,*p; a. num=99101; a.score=89.5; b. num=99103; b.score=90; c. num=99107; c.score=85;/*对结点的num和score成员赋值*/ head=&a; /*将结点a的起始地址赋给头指针head*/ a.next=&b; /*将结点b的起始地址赋给a结点的next成员*/ b.next=&c; /*将结点c的起始地址赋给b结点的next成员*/ c.next=NULL; /*c结点的next成员不存放其他结点地址*/ p=head; /*使p指针指向a结点*/ do { printf("%ld %5.1f\n",p->num,p->score);/*输出p指向的结点的数据*/ p=p->next; /*使p指向下一结点*/ } while(p!=NULL); /*输出完c结点后p的值为NULL*/ } ①各个结点是怎样构成链表的。②没有头指针head行不行?③p起什么作用?没有它行不行?开始时使head指向a结点,a.next指向b结点,b.next指向c结点,这就构成链表关系。“c.next=NULL” 的作用是使c.next不指向任何有用的存储单元。在输出链表时要借助p,先使p指向a结点,然后输出a结点中的数据,“p=p->next” 是为输出下一个结点做准备。p->next的值是b结点的地址,因此执行“p=p->next”后p就指向b结点,所以在下一次循环时输出的是b结点中的数据。本例是比较简单的,所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。 (2) calloc函数 其函数原型为void *calloc(unsigned n,unsigned size);其作用是在内存的动态存储区中分配n个长度为size的连续空间。函数返回一个指向分配域起始地址的指针;如果分配不成功,返回NULL。 用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为Size。 (3) free函数 其函数原型为void free(void *p);其作用是释放由p指向的内存区,使这部分内存区能被其他变量使用。p是最近一次调用calloc或malloc函数时返回的值。free函数无返回值。 以前的C版本提供的malloc和calloc函数得到的是指向字符型数据的指针。 ANSI C提供的malloc和calloc函数规定为void类型。 11.7.4 建立动态链表
设3个指针变量:head、p1、p2,它们都是用来指向struct student类型数据的。先用malloc函数开辟第一个结点,并使p1、p2指向它。然后从键盘读入一个学生的数据给p1所指的第一个结点。我们约定学号不会为零,如果输入的学号为0,则表示建立链表的过程完成,链表为“空”时的head指向DULL(即head不指向任何结点,链表中无结点),以后增加一个结点就使head指向该结点。 如果输入的p1->num不等于0,则输入的是第一个结点数据(n=1),令head=p1,即把p1的值赋给head,也就是使head也指向新开辟的结点(图11.13 )。p1所指向的新开辟的结点就成为链表中第一个结点。然后再开辟另一个结点并使p1指向它,接着输入该结点的数据(见图11.14(a))。如果输入的p1->num≠0,则应链入图11.13第2个结点(n=2),由于n≠1,则将p1的值赋给p2->next,此时p2指向第一个结点,因此执行“p2->next=p1” 就将新结点的地址赋给第一个结点的next成员,使第一个结点的next成员指向第二个结点(见图11.14(b))。接着使p2=p1,也就是使p2指向刚才建立的结点,见图11.14(c)。接着再开辟一个结点并使p1指向它,并输入该结点的数据(见图11.15(a)),在第三次循环中,由于n=3(n≠1),又将p1的值赋给p2->next,也就是将第3个结点连接到第2个结点之后,并使p2=p1,使p2指向最后一个结点(见图11.15(b))。
图11.13
图11.14
图11.15 再开辟一个新结点,并使p1指向它,输入该结点的数据(见图11.16(a))。由于p1->num的值为0,不再执行循环,此新结点不应被连接到链表中。此时将NULL赋给p2->next,见图11.16(b)。建立链表过程至此结束,p1最后所指的结点未链入链表中,第3个结点的next成员的值为NULL,它不指向任何结点。虽然p1指向新开辟的结点,但从链表中无法找到该结点。
图11.16 建立链表的函数可以如下: #define NULL 0 #define LEN sizeof(struct student) struct student { long num; float score; struct studentnext; }; int n; /*n为全局变量,本模块中各函数均可使用它*/ truct student *creat(void)/*定义函数。此函数带回一个指向链表头的指针*/ { struct studenthead; struct studentp1,*p2; n=0; p1=p2=( struct student*) malloc(LEN); /*开辟一个新单元*/ scanf("%ld,%f",&p1->num,&p1->score); head=NULL; while(p1->num!=0) { n=n+1; if(n==1)head=p1; else p2->next=p1; p2=p1; p1=(struct student)malloc(LEN); scanf("%ld,%f",&p1->num,&p1->score); } p2->next=NULL; return(head); } 函数首部在括弧内写void,表示本函数没有形参,不需要进行数据传递。 可以在main函数中调用creat函数: main() {… creat();/*调用creat函数后建立了一个单向动态链表*/ } 调用creat函数后,函数的值是所建立的链表的第一个结点的地址(请查看return语句)。 请注意: (1) 第1行为#define命令行,令NULL代表0,用它表示“空地址”。第2行令LEN代表struct student类型数据的长度,sizeof是“求字节数运算符”。 (2) 第9行定义一个creat函数,它是指针类型,即此函数带回一个指针值,它指向一个struct student类型数据。实际上此creat函数带回一个链表起始地址。 (3) malloc(LEN)的作用是开辟一个长度为LEN的内存区,LEN已定义为sizeof(struct student),即结构体struct student的长度。malloc带回的是不指向任何类型数据的指针(void *类型)。而p1、p2是指向struct student类型数据的指针变量,因此必须用强制类型转换的方法使指针的基类型改变为struct student类型,在malloc(LEN)之前加了“(struct student *)”,它的作用是使malloc返回的指针转换为指向struct student类型数据的指针。注意“*”号不可省略,否则变成转换成struct student类型了,而不是指针类型了。 (4) 最后一行return后面的参数是head(head已定义为指针变量,指向struct student类型数据)。因此函数返回的是head的值,也就是链表的头地址。 (5) n是结点个数。 (6) 这个算法的思路是让p1指向新开的结点,p2指向链表中最后一个结点,把p1所指的结点连接在p2所指的结点后面,用“p2->next=p1”来实现。 我们对建立链表过程做了比较详细的介绍,读者如果对建立链表的过程比较清楚的话,对下面介绍的删除和插入过程也就比较容易理解了。 11.7.5 输出链表 将链表中各结点的数据依次输出。这个问题比较容易处理。例11.7中已初步介绍了输出链表的方法。首先要知道链表第一个结点的地址,也就是要知道head的值。然后设一个指针变量p,先指向第一个结点,输出p所指的结点,然后使p后移一个结点,再输出。直到链表的尾结点。 例11.9编写一个输出链表的函数print。 void print(struct student *head) { struct student*p; printf("\nNow,These %d records are:\n",n); p=head; if(head!=NULL) do { printf("%ld %5.1f\n",p->num,p->score); p=p->next; }while(p!=NULL); } 算法可用图11.17表示。 其过程可用图11.18表示。p先指向第一结点,在输出完第一个结点之后,p移到图中p'虚线位置,指向第二个结点。程序中p=p->next的作用是将p原来所指向的结点中next的值赋给p,而p->next的值就是第二个结点的起始地址。将它赋给p,就是使p指向第二个结点。
图11.17
图11.18 head的值由实参传过来,也就是将已有的链表的头指针传给被调用的函数,在print函数中从head所指的第一个结点出发顺序输出各个结点。 11.7.6 对链表的删除操作 已有一个链表,希望删除其中某个结点。怎样考虑此问题的算法呢?先打个比方:一队小孩(A、B、C、D、E)手拉手,如果某一小孩(C)想离队有事,而队形仍保持不变。只要将C的手从两边脱开,B改为与D拉手即可,见图11.19。图11.19(a)是原来的队伍,图11.19(b)是C离队后的队伍。
图11.19 与此相仿,从一个动态链表中删去一个结点,并不是真正从内存中把它抹掉,而是把它从链表中分离开来,只要撤消原来的链接关系即可。 例11.10写一函数以删除动态链表中指定的结点。 以指定的学号作为删除结点的标志。例如,输入99103表示要求删除学号为99103的结点。解题的思路是这样的:从p指向的第一个结点开始,检查该结点中的num值是否等于输入的要求删除的那个学号。如果相等就将该结点删除,如不相等,就将p后移一个结点,再如此进行下去,直到遇到表尾为止。
可以设两个指针变量p1和p2,先使p1指向第一个结点(图11.20(a))。如果要删除的不是第一个结点,则使p1后指向下一个结点(将p1->next赋给p1),在此之前应将p1的值赋给p2,使p2指向刚才检查过的那个结点,见图11.20(b)。如此一次一次地使p后移,直到找到所要删除的结点或检查完全部链表都找不到要删除的结点为止。如果找到某一结点是要删除的结点,还要区分两种情况:①要删的是第一个结点(p1的值等于head的值,如图11.20(a)那样),则应将p1->next赋给head。见图11.20(c)。这时head指向原来的第二个结点。第一个结点虽然仍存在,但它已与链表脱离,因为链表中没有一个结点或头指针指向它。虽然p1还指向它,它仍指向第二个结点,但仍无济于事,现在链表的第一个结点是原来的第二个结点,原来第一个结点已“丢失” ,即不再是链表中的一部分了。② 如果要删除的不是第一个结点,则将p1->next赋给p2->next,见图11.20(d)。p2->next原来指向p1指向的结点(图中第二个结点),现在p2->next改为指向p1->next所指向的结点(图中第三个结点)。p1所指向的结点不再是链表的一部分。还需要考虑链表是毡?无结点)和链表中找不到要删除的结点的情况。图11.21表示解此题的算法。
图11.21 删除结点的函数del如下: struct studentdel(struct student *head,long num) { struct student *p1,*p2; if (head==NULL){printf("\nlist null!\n");return (head);} p1=head; while(num!=p1->num && p1->next!==NULL) /*p1指向的不是所要找的结点,并且后面还有结点*/ { p2=p1;p1=p1->next;}/*p1后移一个结点 */ if(num==p1->num) /找到了*/ { if(p1==head)head=p1->next; /*若p1指向的是首结点,把第二个结点地址赋予head/ else p2->next=p1->next; /*否则将下一结点地址赋给前一结点地址*/ printf("delete:%ld\n",num); n=n-1; } else printf("%ld not been found!\n",num); /*找不到该结点*/ return(head); } 函数的类型是指向struct student类型数据的指针,它的值是链表的头指针。函数参数为head和要删除的学号num。head的值可能在函数执行过程中被改变(当删除第一个结点时)。 11.7.7 对链表的插入操作 对链表的插入是指将一个结点插入到一个已有的链表中。若已有一个学生链表,各结点是按其成员项num(学号)的值由小到大顺序排列的。今要插入一个新生的结点,要求按学号的顺序插入。为了能做到正确插入,必须解决两个问题:① 怎样找到插入的位置;② 怎样实现插入。如果有一群小学生,按身高顺序(由低到高)手拉手排好队。现在来了一名新同学,要求按身高顺序插入队中。首先要确定插到什么位置。可以将新同学先与队中第1名小学生比身高,若新同学比第1名学生高,就使新同学后移一个位置,与第2名学生比,如果仍比第2名学生高,再往后移,与第3名学生比……直到出现比 第i名学生高,比第i+1名学生低的情况为止。显然,新同学的位置应该在第i名学生之后,在第i+1名学生之前。在确定了位置之后,让第i名学生与第i+1名学生的手脱开,然后让第i名学生的手去拉新同学的手,让新同学另外一只手去拉第i+1名学生的手。这样就完成了插入,形成了新的队列。根据这个思路来实现链表的插入操作。先用指针变量p0指向待插入的结点,p1指向第一个结点。见图11.22(a)。将p0->num与p1->num相比较,如果p0->num>p1->num,则待插入的结点不应插在p1所指的结点之前。此时将p1后移,并使p2指向刚才p1所指的结点,见图11.22(b)。再将p1->num与p0->num比。如果仍然是p0->num大,则应使p1继续后移,直到p0->num≤p1->num为止。这时将p0所指的结点插到p1所指结点之前。但是如果p1所指的已是表尾结点,则p1就不应后移了。如果p0->num比所有结点的num都大,则应将p0所指的结点插到链表末尾。 如果插入的位置既不在第一个结点之前,又不在表尾结点之后,则将p0的值赋给p2->next,使p2->next指向待插入的结点,然后将p1的值赋给p0->next,使得p0->next指向p1指向的变量。见图11.22(c)。可以看到,在第一个结点和第二个结点之间已插入了一个新的结点。 如果插入位置为第一个结点之前(即p1等于head时),则将p0赋给head,将p1赋给p0->next。见图11.22(d)。如果要插到表尾之后,应将p0赋给p1->next,NULL赋给p0->next,见图11.22(e)。以上算法可用图11.23表示。
图11.22
图11.23 例11.11插入结点的函数insert如下。 struct student*insert(struct student *head,struct student *stud) { struct student *p0,*p1,*p2; p1=head;/*使p1指向第一个结点*/ p0=stud; /*p0指向要插入的结点*/ if(head==NULL) /*原来的链表是空表*/ { head=p0; p0->next=NULL;} /*使p0指向的结点作为头结点*/ else { while((p0->num>p1->num) && (p1->next!=NULL)) { p2=p1;/*使p2指向刚才p1指向的结点*/ p1=p1->next; } /*p1后移一个结点*/ if(p0->num<p1->num) { if(head==p1) head=p0;/*插到原来第一个结点之前*/ else p2->next=p0; /*插到p2指向的结点之后*/ p0->next=p1; } else { p1->next=p0; p0->next=NULL;}}/*插到最后的结点之后*/ n=n+1; /*结点数加1*/ return(head); } } 函数参数是head和stud。stud也是一个指针变量,从实参传来待插入结点的地址给stud。语句p0=stud的作用是使p0指向待插入的结点。函数类型是指针类型,函数值是链表起始地址head。 11.7.8 对链表的综合操作 将以上建立、输出、删除、插入的函数组织在一个C程序中,即将例11.8~11.11中的4个函数顺序排列,用main函数作主调函数。可以写出以下main函数(main函数的位置在以上各函数的后面)。 main() { struct student *head,stu; long del-num; printf("input records:\n"); head=creat();/*返回头指针*/ print(head); /*输出全部结点*/ printf("\ninput the deleted number:"); scanf("%ld",&del-num); /*输入要删除的学号*/ head=del(head,del-num);/*删除后链表的头地址*/ print(head);/*输出全部结点*/ printf("\ninput the inserted record:"); 8 /*输入要插入的结点*/ scanf("%ld,%f",&stu.num,&stu.score); head=insert(head,&stu); /*返回地址*/ print(head);/*输出全部结点*/ } 此程序运行结果是正确的。它只删除一个结点,插入一个结点。但如果想再插入一个结点,重复写上程序最后4行,共插入两个结点。运行结果却是错误的。 运行结果如下: input records: (建立链表) 99101,90 99103,98 99105.76 0,0 Now,These 3 records are: 99101 90.0 99103 98.0 99105 76.0 input the deleted number:99103(删除) delete:99103 Now,These 2 records are: 99101 90.099105 76.0 input the inserted record:99102,90 (插入第一个结点) Now,These 3 records are: 99101 90.0 99102 90.0 99105 76.0 input the inserted record:89104,99(插入第二个结点) Now,These 4 records are: 99101 90.0 99104 99.0 99104 99.0 99104 99.0 … (无终止地输出99104的结点数据) 出现以上结果的原因是stu是一个有固定地址的结构体变量。第一次把stu结点插入到链表中。第二次若再用它来插入第二个结点,就把第一次结点的数据冲掉了。实际上并没有开辟两个结点。main 函数如下: main() {struct student *head,*stu; long del-num; printf("input records:\n"); head=creat(); print(head); printf("\ninput the deleted number:"); scanf("%ld",&del-num); while(del-num!=0) {head=del(head,del-num); print(head); printf("input the deleted number:"); scanf("%ld",&del-num);} printf("\ninput the inserted record:"); stu=(struct student)malloc(LEN); scanf("%ld,%f",&stu->num,&stu->score); while(stu->num!=0) { head=insert(head,stu); print(head); printf("input the inserted record:"); stu=(struct student)malloc(LEN); scanf("%ld,%f",&stu->num,&stu->score); } } stu定义为指针变量,在需要插入时先用malloc函数开辟一个内存区,将其起始地址经强制类型转换后赋给stu,然后输入此结构体变量中各成员的值。对不同的插入对象,stu的值是不同的,每次指向一个新的struct student变量。在调用insert函数时,实参为head和stu,将已建立的链表起始地址传给insert函数的形参,将stu(即新开辟的单元的地址)传给形参stud,返回的函数值是经过插入之后的链表的头指针(地址)。 运行情况如下: input records: 99101,99 99103,87 99105,77 0,0 Now,These 3 records are: 9910199.0 99103 87.0 99105 77.0 input the deleted number:99103 delete:99103 Now,these 2 records are: 99101 99.099105 77.0 input the deleted number:99105 delete:99105 Now,These 1 records are: 99101 99.0 input the deleted number:0 input the inserted record:99104,87 Now,These 2 records are: 99101 99.0 99104 87.0 input the inserted record:99106,65 Now,These 3 records are: 99101 99.0 99104 87.0 99106 65.0 input the inserted record:0,0 11.8 共用体 11.8.1 共用体的概念 有时需要使几种不同类型的变量存放到同一段内存单元中。例如,可把一个整型变量、一个字符型变量、一个实型变量放在同一个地址开始的内存单元中(见图11.24)。以上3个变量在内存中占的字节数不同,但都从同一地址开始(图中设地址为1000)存放。也就是使用覆盖技术,几个变量互相覆盖。这种使几个不同的变量共占同一段内存的结构,称为“共用体”类型的结构。 定义共用体类型变量的一般形式为
图11.24 union 共用体名 { 成员表列 }变量表列; 例如: union data { int i; char ch; float f; }a,b,c; 也可以将类型声明与变量定义分开: union data { int i; char ch; float f; }; union data a,b,c; 即先声明一个union data类型,再将a、b、c定义为union data类型。当然也可以直接定义共用体变量,如: union { int i; char ch; float f; }a,b,c; 可以看到,“共用体”与“结构体”的定义形式相似。但它们的含义是不同的。 结构体变量所占内存长度是各成员占的内存长度之和。每个成员分别占有其自己的内存单元。 有些C变量所占的内存长度等于最长的成员的长度。例如,上面定义的“共用体”变量a、b、c各占4个字节(因为一个实型变量占4个字节),而不是各占2+1+4=7个字节。 11.8.2 共用体变量的引用方式 只有先定义了共用体变量才能引用它。而且不能引用共用体变量,而只能引用共用体变量中的成员。例如,前面定义了a、b、c为共用体变量,下面的引用方式是正确的: a.i(引用共用体变量中的类型变量i) a.ch(引用共用体变量中的字符变量ch) a.f (引用共用体变量中的实型变量f) 不能只引用共用体变量,例如: printf("%d",a) 是错误的,a的存储区有好几种类型,分别占不同长度的存储区,仅写共用体变量名a,难以使系统确定究竟输出的是哪一个成员的值。应该写成printf("%d",a.i)或printf("%c",a.ch)等。 11.8.3 共用体类型数据的特点 在使用共用体类型数据时要注意以下一些特点: (1) 同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一种,而不是同时存放几种。也就是说,每一瞬时只有一个成员起作用,其他的成员不起作用,即不是同时都存在和起作用。 (2) 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。如有以下赋值语句: a.i=1; a.c='a'; a.f=1.5; 在完成以上3个赋值运算以后,只有a.f是有效的,a.i和a.c已经无意义了。此时用printf("%d",a.i)是不行的,而用printf("%f",a.f)是可以的,因为最后一次的赋值是向a.f赋值。因此在引用共用体变量时应十分注意当前存放在共用体变量中的究竟是哪个成员。 (3) 共用体变量的地址和它的各成员的地址都是同一地址。例如:&a、&a.i、&a.c、&a.f都是同一地址值,其原因是显然的。 (4) 不能对共用体变量名赋值,也不能企图引用变量名来得到一个值,又不能在定义共用体变量时对它初始化。例如,下面这些都是不对的: ① union {int i; char ch; float f; }a={1,'a',1.5};(不能初始化) ② a=1; (不能对共用体变量赋值) ③ m=a; (不能引用共用体变量名以得到一个值) (5) 不能把共用体变量作为函数参数,也不能使函数带回共用体变量,但可以使用指向共用体变量的指针(与结构体变量这种用法相仿)。 (6) 共用体类型可以出现在结构体类型定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型定义中,数组也可以作为共用体的成员。 例11.12设有若干个人员的数据,其中有学生和教师。学生的数据中包括:姓名、图11.25号码、性别、职业、班级。教师的数据包括:姓名、号码、性别、职业、职务。可以看出,学生和教师所包含的数据是不同的。现要求把它们放在同一表格中,见图11.25。如果“job”项为“s”(学生),则第5项为class(班)。即Li是501班的。如果“job”项是“t”(教师),则第5项为position(职务)。Wang是prof(教授)。显然对第5项可以用共用体来处理(将class和position放在同一段内存中)。
图11.25
图11.26 要求输入人员的数据,然后再输出。可以写出下面的算法(见图11.26)。按此写出程序。为简化起见。只设两个人(一个学生、一个教师)。 struct { int num; char name[10]; char sex; char job; union { int class; char position[10]; }category; }person[2]; main() { int n,i; for(i=0,i<2;i++) { scanf("%d %s %c %c",&person[i].num, person[i].name,&person[i].sex, &person[i].job); if(person[i].job=='s') scanf("%d", &person[i].category.class); else if (person[i].job=='t') scanf("%s",person[i].category.position); else printf("input error!"); } printf("\n"); printf("No. Namesex job class/position\n"); for(i=0;i<2;i++) { if(person[i].job=='s') printf("%\|6d%\|10s %\|3c %\|3c %\|6d\n", person[i].num,person[i].name, person[i].sex,person[i].job,person[i].category.class); else printf("%\|6d%\|10s %\|3c %\|3c %\|6s\n", person[i].num, person[i].name, person[i].sex,person[i].job,person[i].category.position); } } 运行情况如下: 101 Li f s 501 102 Wang m t professor No. Name sex job class/position 101 Li f s 501 102 Wang m t professor 可以看到:在main函数之前定义了外部的结构体数组person,在结构体类型声明中包括了共用体类型,category(分类)是结构体中一个成员名,在这个共用体中成员为calss和position,前者为整型,后者为字符数组(存放“职务”的值——字符串)。 11.9 枚 举 类 型 枚举类型是ANSI C新标准所增加的。 如果一个变量只有几种可能的值,可以定义为枚举类型。所谓“枚举”是指将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。声明枚举类型用enum开头。例如: enum weekday{sun,mon,tue,wed,thu,fri,sat}; 声明了一个枚举类型enum weekday,可以用此类型来定义变量。如:enum weekday workday,week-end; workday和week-end被定义为枚举变量,它们的值只能是sun到sat之一。例如: workday=mon; week-end=sun; 是正确的。 当然,也可以直接定义枚举变量,如: enum{sun,mon,tue,wed,thu,fri,sat} workday,week-end; 其中sun、mon、…、sat等称为枚举元素或枚举常量。它们是用户定义的标识符。这些标识符并不自动地代表什么含义。例如,不因为写成sun,就自动代表“星期天”。其实不写sun而写成sunday也可以。用什么标识符代表什么含义,完全由程序员决定,并在程序中作相应处理。 说明: (1) 在C编译中,对枚举元素按常量处理,故称枚举常量。它们不是变量,不能对它们赋值。例如:sun=0;mon=1;是错误的。 (2) 枚举元素作为常量,它们是有值的,C语言编译按定义时的顺序使它们的值为0,1,2,…。 在上面定义中,sun的值为0,mon的值为1……sat为6。如果有赋值语句: workday=mon; workday变量的值为1。这个整数是可以输出的。如:printf("%d",workday);将输出整数1。 也可以改变枚举元素的值,在定义时由程序员指定,如:enum weekday{sun=7,mon=1,tue,wed,thu,fri,sat}workday,week-end;定义sun为7,mon=1,以后顺序加1,sat为6。 (3) 枚举值可以用来做判断比较。如 if(workday==mon)… if(workday>sun)… 枚举值的比较规则是按其在定义时的顺序号比较。如果定义时未人为指定,则第一个枚举元素的值认作0。故mon大于sun,sat>fri。 (4) 一个整数不能直接赋给一个枚举变量。如: workday=2; 是不对的。它们属于不同的类型。应先进行强制类型转换才能赋值。如: workday=(enum weekday)2; 它相当于将顺序号为2的枚举元素赋给workday,相当于workday=tue; 甚至可以是表达式。如: workday=(enum weekday)(5-3); 例11.13口袋中有红、黄、蓝、白、黑5种颜色的球若干个。每次从口袋中先后取出3个球,问得到3种不同色的球的可能取法,打印出每种排列的情况。 球只能是5种色之一,而且要判断各球是否同色,应该用枚举类型变量处理。 设取出的球为i、j、k。根据题意,i、j、k分别是5种色球之一,并要求i≠j≠k。可以用穷举法,即一种可能一种可能地试,看哪一组符合条件。算法可用图11.27表示。
图11.27 用n累计得到3种不同色球的次数。外循环使第1个球i从red变到black。中循环使第2个球j也从red变到black。如果i和j同色则不可取,只有i、j不同色(i≠j)时才需要继续找第3个球,此时第3个球k也有5种可能(red到black),但要求第3个球不能与第1个球或第2个球同色,即k≠i,k≠j。满足此条件就得到3种不同色的球。输出这种3色组合方案。然后使n加1。外循环全部执行完后,全部方案就已输出完了。最后输出总数n。
图11.28 下面的问题是如何实现图11.28中的“输出一种取法”。这里有一个问题:如何输出“red”、“blue”……等单词。不能写成printf("%s",red)来输出“red”字符串。可以采用图11.28的方法。 为了输出3个球的颜色,显然应经过3次循环,第1次输出i的颜色,第2次输出j的颜色,第3次输出k的颜色。在3次循环中先后将i、j、k赋予pri。然后根据pri的值输出颜色信息。在第1次循环时,pri的值为i,如果i的值为red,则输出字符串“red”,其他的类推。 程序如下: main() { enum color {red,yellow,blue,white,black}; enum color i,j,k,pri; int n,loop; n=0; for (i=red;i<=black;i++) for (j=red;j<=black;j++) if (i!=j) { for (k=red;k<=black;k++) if ((k!=i) && (k!=j)) { n=n+1; printf("%-4d",n); for (loop=1;loop<=3;loop++) { switch (loop) { case 1: pri=i;break; case 2: pri=j;break; case 3: pri=k;break; default:break; } switch (pri) { case red:printf("%-10s","red"); break; case yellow: printf("%-10s","yellow"); break; case blue: printf("%-10s","blue"); break; case white: printf("%-10s","white"); break; case black: printf("%-10s","black"); break; default :break; } } printf("\n"); } } printf("\ntotal:%5d\n",n); } 运行结果如下: 1redyellowblue 2redyellowwhite 3redyellowblack ………… 58blackwhitered 59blackwhiteyellow 60blackwhiteblue total:60 有人说,不用枚举变量而用常数0代表“红”,1代表“黄”……不也可以吗?是的,完全可以。但显然用枚举变量更直观,因为枚举元素都选用了令人“见名知意”的标识符,而且枚举变量的值限制在定义时规定的几个枚举元素范围内,如果赋予它一个其他的值,就会出现出错信息,便于检查。 11.10 用typedef定义类型 除了可以直接使用C提供的标准类型名(如int、char、float、double、long等)和自己声明的结构体、共用体、指针、枚举类型外,还可以用typedef声明新的类型名来代替已有的类型名。 用typedef声明新的类型名来代替已有的类型名。 声明INTEGER为整型 typedef int INTEGER 声明结构类型 Typedef struct { int month; int day; int year;}DATE; 声明NUM为整型数组类型 : typedef int NUM[100]; 声明STRING为字符指针类型: typedef char *STRING; 声明POINTER为指向函数的指针类型,该函数返回整型值 : typedef int (*POINTER)() 用typedef定义类型的方法: ① 先按定义变量的方法写出定义体(如:int i)。 ② 将变量名换成新类型名(例如:将i换成COUNT)。 ③ 在最前面加typedef(例如:typedef int COUNT)。 ④ 然后可以用新类型名去定义变量。 用typedef定义类型的方法(举例): ① 先按定义数组变量形式书写:int n[100]; ② 将变量名n换成自己指定的类型名:int NUM[100]; ③ 在前面加上typedef,得到typedef int NUM[100]; ④ 用来定义变量:NUM n; 说明: (1)用typedef可以声明各种类型名,但不能用来定义变量。 (2) 用typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型。 (3) 当不同源文件中用到同一类型数据时,常用typedef声明一些数据类型,把它们单独放在一个文件中,然后在需要用到它们的文件中用#include命令把它们包含进来。 (4) 使用typedef有利于程序的通用与移植。 (5) typedef与#define有相似之处,例如:typedef int COUNT;#define COUNT int的作用都是 用COUNT代表int。但事实上,它们二者是不同的。#define是在预编译时处理的,它只能作简单的字符串替换,而typedef是在编译时处理的。实际上它并不是作简单的字符串替换,而是采用如同定义变量的方法那样来声明一个类型。
|
|