第十一章 结构体与共用体
11.1 概 述
数据的基本类型:整型、实型、字符型等
构造类型:数组
如何描述一个学生的基本信息?
学号, 名称, 性别, 年龄, 成绩, 地址
C语言提供了这样一种数据结构,结构体 ( structure)
它相当于, 记录, 。
num name sex age score addr
10010 LiFun M 18 87.5 Beijing
例如, struct student
{ int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
定义一个结构体类型的一般形式为,
struct 结构体名
{
成员表列
};
, 成员表列, 对各成员都应进行类型说明,即,
类型标识符 成员名;
也可以把, 成员表列, 称为, 域表,,每一个成员
称为结构体中的一个域。
1 1.2 定义结构体类型变量的方法
要定义一个结构体类型的变量, 可以采取以
下三种方法 。
一, 先定义 结构体类型 再定义 变量名
如已定义 结构体类型 struct student,可以
用它来定义变量,
struct student student1,student2;
为了使用方便,人们通常用一个符号常量代表一个结构
体类型。在程序开头,用
# define STUD struct student -预处理命令
在程序中,STUD与 struct student完全等效 。
STUD
{ int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
可以直接用 STUD定义变量,
STUD student1,student2;
二、在定义类型的同时定义变量
例如,struct student
{ int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1,student2;
既定义了类型,又定义了两个 struct student类型的
变量 student1,student2。
三、直接定义结构类型变量
其一般形式为,
struct --无结构体名
{
成员表列
}
变量名表列;
关于结构体类型,有几点要说明,
1.类型与变量是不同的概念,
只能对变量赋值、存取或运算,而不能对类型赋值、
存取或运算。
在编译时,对类型是不分配空间的,只对变量分
配空间。
2,对结构体中的成员(即, 域, ),可以单独使用,
它的作用与地位相当于普通变量;
strcpy(student1.name,”zhang”)
3,成员名可以与程序中的变量名相同;
4.成员也可以是一个结构体变量。
如,struct date
{ int month;
int day;
int year; };
struct student
{ int num;
char name[20 ];
char sex;
int age;
struct date birthday;
char addr[30 ];
} student1,student2;
说明:先定义一个结构体da te类型,它代表, 日
期,,包括三个成员。
1 1.3 结构体类型变量的引用
引用方式,结构体变量名,成员名
student.num=10010;
注:, ·”是成员(分量)运算符,它在所有的运算
符中优先级最高。
引用结构体变量应遵守以下规则,
1, 不能将一个结构体变量作为一个整体进行
输入和输出 。
printf(”%d,%s,%c,%d,%f,%s\n,”,student1);
只能对结构体变量中的各个成员分别输出。
2 ·如果成员本身又属一个结构体类型,则要用
若干个成员运算符,一级一级地找到最低的
一级的成员。只能对最低级的成员进行赋值
或存取以及运算。
student1.num
student1.name
student1.birthday.month
Student1.birthday.day
Student1.birthday.year
注意,不能用 student1.birthday 来访问 student1变
量中的成员 birthday,因为 birthday本身是一个结
构体变量。
3.对成员变量可以像普通变量一样进行各种
运算(根据其类型决定可以进行的运算)。
例如,
student2.score=student1.score;
sum=student1.score+student2.score;
student1.age++;
++student1.age;
4.可以引用成员的地址,也可以引用结构体
变量的地址。
例如,
scanf( "% d”,&student1.num);
(输入 student1.num的值)
printf(, %o”,&student1);
(输出 student1的首地址)
11.4 结构体变量的初始化
例如,
Struct student
{ long int num;
char name[20];
char sex;
char addr[ 20];
}a= {20034101,,Lilin”,?M?,“WS Road”};
在定义时赋初值。
例 1 p264_b1
1 1.5 结构体数组
定义,数组中的每一个元素是结构体, 则该
数组是结构体数组 。
例如,
struct student
{ int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct student stu[3];
二、结构体数组的初始化
如,struct student
{ int num;
char name[20];
char sex;
int age;
float score;
char add[30];

Stu[3]={{1,“ZHANG,,?M ’,18,87.5,“WS
Road"},{…},{…}};
三、举例
p266_b2
1 1.6 指向结构体类型数据的指针
一个结构体变量的指针就是该变量所占据的内存段的起始地址 。
11.6.1 指向结构体变量的指针
例 3 # include,string.h"
main()
{ struct student
{long int num;
char name[20 ];
char sex;
float score; };
struct student stu_1,*p;
p =&stu_1;
stu_1.num=89101
strcpy(stu_1.name,” Li lin, );
stu_1.sex=?M ’ ;
stu_1.score=89,5;
printf(”No:% ld\ nname:% s\nsex:% c\n score:%
f\n”,stu_1.num,stu_1.name,stu_1.sex,
stu_1.score) ;
printf("\nNo.:%1d\nname:%s\nsex:%c\nscore:%f\n",
(*p).num,(*p).name,(*.p).sex,(*p).score);
} p 89101
“Lilin”
?M?
89.5
说明,
( *p)表示p指向的结构体变量,
(*p).num 是p指向的结构体变量中的成员 num。
*p.num 表示?
在C语言中,可以把 (*p).num用 p ->num 来代替,
表示p所指向的结构体变量中的num成员 。
①结构体变量,成员名;
② (*p ).成员名;
③p ->成员名
p ->n++ 得到p指向的结构体变量中的成员n的
值, 用完该值后使它加1 。
++p ->n 得到p指向的结构体变量中的成员n的
值使之加1 ( 先加 ) 。
11,6.2指向结构体数组的指针
[例 11.4]
struct student
{ int num;
char name[20];
char sex;
int age; };
struct student stu[3] ={
{{10101,” Li Lin,, ‘ M ’,18},
{1010 2,” Zhang Fun,, ’ M ‘,19},
{10104,"wang Min",?F?,20}};
main()
{ struct student *p;
printf(”No,Name sex age\n”);
for(p =stu; p< stu+3;p ++)
printf(, %5d%-20s%2c%4d\n”,
p->num,p->name,p->sex,p->age);
}
1 1.6,3用指向结构体的指针作函数参数
如何传递一个结构体变量的值给函数?
方法一,用结构体变量的成员作参数。
方法二,用结构体变量作实参。
方法三,用指向结构体变量的指针作实参。
例 5
1 1.7 用指针处理链表
1 1.7,1链表概述
链表是一种常见的重要的数据结构。它是动态地进
行存储分配的一种结构。我们知道,用数组存放数据
时,
必须事先定义固定的长度 ( 即元素个数 ) 。 比如, 有的
班级有100人, 而有的班只有30人, 如果要用同
一个数组先后存放不同班级的学生数据, 则必须定义
长度为 100的数组 。 如果事先难以确定一个班的最多
人数, 则必须把数组定得足够大, 以能存放任何班级
的学生数据 。 显然这将会浪费内存 。 链表则没有这种
缺点, 它根据需要开辟内存单元 。 图10, 11表示
最简单的一种链表 ( 单向链表 ) 的结构 。 链表有一个
,头指针, 变量, 图中以head表示, 它存放一个
地址 。
该地址指向一个元素。链表中每一个元素称为, 结点,,
每个结点都应包括两个部分:一为用户需要用的实际
数据,二为下一个结点的地址。课以看出,head指向
第一个元素;第一个元素又指向第二个元素; ……,
直到最后一个元素,该元素不再指向其它元素,它称
为 ‘ 表尾,,它的地址部分放一个, NULL, (表
示, 空地址, )。链表到此结束。
可以看到:链表中各元素在内存中可以不是连续存
放的。要找某一元素,必须先找到上一个元素,根据
它提供的下一元素地址才能找到下一个元素。
如果不提供, 头指针, ( head),则整个链表都无法访
问。链表如同一条铁链一样,一环扣一环,中间是不
能断开的。打个通俗的比方:幼儿园的老师带领孩子
出来散步,老师牵着第一个小孩的手,第一个小孩的
另一只手牵着第二个孩子,……,这就是一个, 链,,
最后一个孩子有一只手空着,他是, 链尾, 。要找这
个队伍,必须先找到老师,然后顺序找到每一个孩子。
可以看到,这种链表的数据结构,必须利用指针变
量才能实现
。即:一个结点中应包含一个指针变量,用它存放下一
结点的地址。
前面介绍了结构体变量, 它包含若干成员 。 这些成
员可以是数值类型, 字符类型, 数组类型, 也可以是
指针类型 。 这个指针类型可以是指向其它结构体类型
数据, 也可以指向它所在的结构体类型 。 例如,
struct student
{ int num ;
float s core ;
struct student *next ;
ne xt是成员名,它是指针类型的,它指向str
uct student类型数据(这就是next
所在的结构体类型)。用这种方法可以建立链表。见
图10.12。
其中每一个结点都属于s truc t studen
t类型,它的成员nex t存放下一结点的地址,程序
设计人员可以不必具体知道地址值,
只要保证将下一个结点的地址放到前一结点的成员ne
xt中即可。
请注意:上面只是定义了一个struct st
udent类型,并未实际分配存储空间。前面讲过,
链表结构是动态地分配存储的,即在需要时才开辟一
个结点的存储单元。怎样动态地开辟和释放存储单元
呢?C语言编译系统的库函数提供了以下有关函数。
1,malloc(size) 在内存的动态存
储区中分配一个长度为size的连续空间。
此函数的值(即, 返回值 ")是一个指针,它的值是该
分配域的起始地址。如果此函数未能成功地执行,则
返回值为0。
2,calloc(n,size) 在内存的动态区存储中分配n
个长度为size的连续空间。函数返回分配域的起
始地址;如果分配不成功,返回0。
3,free(ptr) 释放由ptr指向的内存区。pt
r是最近一次调用ca11或ma11oc函数时返
回的值。
上面三个函数中,参数n和size为整型,pt
r为字符型指针。
请注意:许多C版本提供的 malloc和 call0c函数得
到的是指向字符型数据的指针。新标准C提供的ma
110c和ca11 oc函数规定为void *类型。
有了本节所介绍的初步知识,下面就可以对链表进
行操作了(包括建立链表、插入或删除链表中一个结
点等)。有些概念需要在后面的应用中逐步建立和掌
握。
1 1.7.2建立链表
所谓建立链表是指从无到有地建立起一个链表,即一个
一个地输入各结点数据,并建立起前后相链的关系。
下面通过一个例子来说明如何建立一个链表。
[例1 1.7]写一函数建立一个有5名学生数据
的单向链表。
先考虑实现此要求的算法(见图10,13)。
设三个指针变量:h ead、p1、p2,它们都
指向结构体类型数据。先用mal1oc函数开辟一
个结点,并使p1、p2指向它。
然后从键盘读人一个学生的数据给pl所指的结点。我
们约定学号不会为零,如果输入的学号为0,则表示
建立链表的过程完成,该结点不应连接到链表中。先
使head的值为NU LL(即等于0),这是链表为
,空, 时的情况(即head不指向任何结点,链表
中无结点),以后增加一个结点就使head指向该
结点。
如果输入的pl一 >num不等于0,而且输入的是
第一个结点数据(n =1)时,则令head=p1,
即把p 1的值赋给head,也就是使head也指向
新开辟的结点(图10.14)。 P1所指向的新开
辟的结点就成为链表中第一个结点。然后再开辟另一
个结点并使p1指向它,接着读入该结点的数据(见
图10.15(a))。如果输入的 p1->num !=0,
则应链入第2个结点(n=2),由于n !=1,则将
p1的值赋给 p2-->next,也就是使第一个结点的nex t
成员指向第二个结点(见图10.15( b))。接
着使 p2=p1,
也就是使p2指向刚才建立的结点,见图10.15
(c)。再开辟一个结点并使pl指向它,并读入该
结点的数据(见图10.16(a)),在第三次循
环中,由于n=3(n !=1),又将pl的值赋给p
2一 >next,也就是将第3个结点连接到第2个结点
之后,并使p2 =p1,使p2指向最后一个结点
(见图1 1.16( b)。
再开辟一个新结点,并使pl指向它,
输入该结点的数据(见图1 1.17(a))。由于p
l一 >num的值为0,不再执行循环,此新结点不
应被连接到链表中。此时将NULL赋给p2一 >n
ext,见图10.17( b)。建立链表过程至此结
束,pl最后所指的结点未链入链表中,第3个结点
的next成员的值为NULL,它不指向任何结点。
虽然pl指向新开辟的结点,但从链表中无法找到该
结点。
建立链表的函数可以如下,
#d efine NULL 0
#define LEN sizeo f(struct
stu dent)
struct stud ent
{1on g num ;
f loat score;
struct student *ne xt ;
};
int n ;
struct student *creat()
/ *此函数带回一个指向链表头的指针 */
{struct student *head;
struct student * p1,*p2;
n=0 ;
p1=p2=(struct student *)ma
l1oc(LEN);/ *开辟一个新单元 */
scanf( "% ld,% f",& pl一 >num,& p
l一 >score);
head=NULL,
while (pl一 >num!=0)
{n =n十1;
if(n==1) head=pl;
else p2一 >next=p1;
p2 =pl;
pl=(struct student *)
malloc(LEN);
scanf( "%1d,% f",& p1-->num,&pl一 >
score);
}
p2一 >next=NU LL;
return(head) ;
}
请注意,
(1)第一行为 #define命令行,令NUL
L代表0,用它表示, 空地址, 。第二行令LEN代
表struct student结构体类型数据的
长度,
sizeof是, 求字节数运算符, 。
(2)第9行定义一个creat函数,它是指针
类型,即此函数带回一个指针值,它指向一个str
uct st udent类型数据。实际上此cre
at函数带回一个链(3)malloc(LEN)
的作用是开辟一个长度为LEN的内存区,LEN已
定义为sizeof(struct studen
t),即结构体struct student的长
度。在一般系统中,malloc带回的是指向字符
型数据的指针。
而p1、p2是指向struct student类
型数据的指针变量,二者所指的是不同类型的数据,
这是不行的。因此必须用强制类型转换的方法使之类
型一致,在malloc (LEN)之前加了
,(struct student *),,它的作
用是使malloc返回的指针转换为指向stru
ct student类型数据的指针。注意, *”
号不可省略,否则变成转换成struct stu
dent类型了,而不是指针类型了。
(4)最后一行return后面的参数是head
(head已定义为指针变量,指向 struct s
tudent类型数据)。因此函数返回的是hea
d的值,也就是链表的头地址。
(5)n是结点个数。
(6)这个算法的思路是:让pl指向新开的结点,
p2指向链表中最后一个结点,把pl所指的结点连
接在 p2所指的结点后面,用, p2一 >next =pl,
来实现。
我们对建立链表过程作了比较详细的介绍,
读者如果对建立链表的过程比较清楚的话,对下面介绍
的删除和插入过程也就比较容易理解了。
1 1.7.3输出链麦
将链表中各结点的数据依次输出。这个问题比较容
易处理。首先要知道链表头元素的地址,也就是要知
道head的值。然后设一个指针变量 p,先指向第一
个结点,输出p所指的结点,然后使p后移一个结点,
再输出。直到链表的尾结点。
「例10.8]写出输出链表的函数print。
void print(head)
struct student *head ;
{struct student *p;
printf(, \nNow,These%d
records are:\ n",n);
p=head;
if(head!=NULL)
d o
{printf( "%ld%5,1f\ ",p一 >num,
p —>score);
p =p一 >next;
}while(p! =NULL);
算法可用图1 1.18表示。
其过程可用图1 1.19表示。p先指向第一结点,
在输出完第一个结点之后,p移到图中p '虚线位置,
指向第二个结点。程序中p =p一 >next的作用是:
将p原来所指向的结点中next的值赋给p,
而 p一 >next的值就是第二个结点的起始地址。将它
赋给 p就是使p指向第二个结点。
head的值由实参传过来,也就是将已有的链表
的头指针传给被调用的函数,在print函数中从
head所指的第一个结点出发顺序输出各个结点。
1 1.7.4 对链麦的删除操作
已有一个链表,希望删除其中某个结点。怎样考虑
此问题的算法呢,先打个比方,
一队小孩 (A,B.C.D.E)手拉手,如果某一小孩(C)
想离队有事,而队形仍保持不变。只要将C的手从两
边脱开,B改为与 D拉手即可,见图10,20。图1
0.20(a)是原来的队伍,图10,20( b)是
c离队后的队伍。
与此相仿, 从一个链表中删去一个结点, 并不是真正
从内存中把它抹掉, 而是把它从链表中分离开来, 即
改变链接关系即可 。
[ 例1 1,9 ] 写一函数以删除指定的结点 。
我们以指定的学号作为删除结点的标志。例如,输入8
9103表示要求删除学号为89103的结点。解
题的思路是这样的:从p指向的第一个结点开始,检
查该结点中的num值是否等于输入的要求删除的那
个学号。如果相等就将该结点删除,如不相等,就将
可以设两个指针变量pl和p2,先使pl指向
第一个结点(图10.2 1(a))。如果要删除的
不是第一个结点,
则使pl后指向下一个结点(将pl一 >next赋给
pl),在此之前应将pl的值赋给p2,使p2指
向刚才检查过的那个结点,见图10。21( b),如
此一次一次地使p后移,直到找到所要删除的结点或
检查完全部链表都找不到要删除的结点为止。如果找
到某一结点是要删除的结点,还要区分两种情况:①
要删的是第一个结点(pl的值等于head的值,
如图10,21(a)那样),则应将pl一 >nex
t赋给head。见图10.21(c)。
这时 he ad指向原来第二个结点。第一个结点虽然仍存
在,但它已与链表脱离,因为链表中没有一个元素或
头指针指向它。虽然p1还指向它,它仍指向第二个
结点,但仍无济于事,现在链表的第一个结点是原来
第二个结点,原来第一个结点, 丢失, 。②如果要删
除的不是第一个结点,则将pl一 >next赋给p
2一 >next,见图10.21( d),p2一 >ne
xt原来指向pl指向的结点(图中第二个结点),
现在p2一 >next改为指向p1一 >next所指
向的结点
(图中第三个结点)。pl所指向的结点不再是链表的
一部分。
还需要考虑链表是空表(无结点)和链表中找不到
要删除的结点的情况。
图 11。 22表示解此题的算法,
删除结点的函数del如下,
struct student *del(he
ad,num)
struct studen t *head;
1ong num;
{struct s tudent *p1,*p2;
if(head ==NULL) {printf
(, \nlist null !\n”); goto en
d;}
p 1=head;
whi1e (num!=pl一 >num&&
pl一 >next!一NULL)/ *pl指向的不是
所要找的结点,并且后面还有结点点 */
{p2 =p1;pl =pl一 >next;}/ *
后移一个结点 */
if (num==pl一 >num) / *找到了 */
{if (n1==head) head =p
l一 >next;/ *若pl指向的是头结点,把第二
个结点地址赋予head */
e1se p2一 >next =pl一 >ne
xt;/ *否则将下一结点地址赋给前一结点地址 *\
printf( "delete:% ld\n”,
num);
n =n-1;
}
else printf(, %ld no
t been found! \n“,num);/ *
找不到该结点 */
end,
return ( head ) ;
}
函数的类型是指向struct student
类型数据的指针,
它的值是链表的头指针。函数参数为head和要删除
的学号num。head的值可能在函数执行过程中
被改变(当删除第一个结点时)。
1 1.7。5对链表的插入操作
将一个结点插入到一个已有的链表中。设已有的链
表中各结点中的成员项num(学号)是按学号由小
到大顺序排列的。
用指针变量p0指向待插入的结点,pl指向第一
个结点。见图10.23(a)。
将p0一 >num与pl一 >num相比较,如果p0一
>num>pl一 >num,则待插入的结点不应插在
pl所指的结点之前。此时将pl后移,并使p2指
向刚才pl所指的结点,见图10.23( b)。再
将p1一 >num与p0一 >num比。如果仍然是p
0一 >num大,则应使pl继续后移,直到p0一 >
num <=p1一 >num为止。这时将p0所指的结
点插到pl所指结点之前。但是如果p1所指的已是
表尾结点,则pl就不应后移了。如果p0一 >nu
m比所有结点的num都大,
则应将p0所指的结点插到链表末尾。
如果插入的位置既不在第一个结点之前,又不在
表尾结点之后,则将p0的值赋给p2一 >next,
即使p2一 >next指向待插入的结点,然后将p
l的值赋给p0 ->next,即使得p0一 >nex
t指向pl指向的变量。见图10.23( c)。可
以看到,在第一个结点和第二个结点之间已插入了一
个新的结点。
如果插入位置为第一结点之前(即pl等于hea
d时),
则将p0赋给head,将p1赋给p 0->next。
见图10.23( d)。如果要插到表尾之后,应将
p0赋给pl一 >next,NULL赋给p0一 >n
ext,见图10.23(e)。
以上算法可用图10.24表示。
[例1 1,10]插入结点的函数insert如
下。
struct student *insert
(head,stud)
struct student *head,
*stud,
{struct student *p0,*p1,*
p2;
pl =head; /*使pl指向第一个结
点 */
p0=stud;/ *p0指向要插入的结
点 */
if (head= =NULL)/ *原来
是空表 */
{head =p0;p0一 >next =N
ULL;}/ *使p0指向的结点作为第一个结点 */
else
{while ((p0一 >num>p
l一 >num) &&(pl一 >next!=NULL))
{p2= p1; p1=pl一 >nex t; }/ *p
2指向刚才pl指向的结点,p1后移一个结点 */
if(p0 ->num<=pl一)num)
{if (head ==pl) hea
d=p0;/ *插到原来第一个结点之前 */
else p2 ->next =p0;/ *插到p2指向的结点
之后 */
p0 —> next =p1; }
else
{pl一 >next =p0;p0一 >next =N
ULL; }}/ *插到最后的结点 之后 */
n =n+1; / *结点数加1 */
return ( head ) ;
}
函数参数是head和stud,stud也是一个指针
变量,从实参传来待插入结点的地址给stud。语
句p0 =stud的作用是使p0指向待插入的结点。
函数类型是指针类型,函数值是链表起始地址he
ad。
将以上建立、输出、删除、插入的函数组织在一个
程序中,用main函数作主调函数。可以写出以下
main函数。
main()
{struct student *head,stu ;
1ong de1 _num;
printf(, input records:
\ n");
head=creat(); / *返回头指针 *

print(head); / *输出全部结点 */
printf(, \ninput the del
eted 0number:, );
scanf(, %ld,, &de1 _mum);/
*输入要删除的学号 */
head=del(head,de1 _nu
m);/ *删除后的头地址 */
print(head); / *输出全部结点 *

prinif(, /ninput the inser
ted record,")/ *输入要插入的记录 */
scanf(, %ld,% f",&stu,num,
&stu,score);
head =insert(head,&stu);
/ *返回地址 */
print(head);
}
此程序运行结果是正确的。
它只删除一个结点,插入一个结点。但如果想再插入一
个结点,重复写上程序最后四行,即共插入两个结点。
运行结果却是错误的。
input records:(建立链表)
89101,90
89103,98
89105,76
0,0
Now,These 3 records are,
89101 90.0
89103 98.0
89105 76.0
inpu the deleted num ber,
89103 (删除)
delete,89103
Now,These 2 rec ords are,
89101 9 0,0 89105 7
6.0
in put the inserted reco
rd:89102
90 ( 插入第一个结点 )
Now, These 3 records are,
89101 90, 0
89102 90, 0
89105 76.0
input the inserted reco
rd:89104,99/(插入第二个结点)
Now,The 4 records are,
89101 90.0
89104 99.0
89 104 99。0
89104 99.0
,.,
,.,
(无终止地输出89104的结点数据)
请读者将main与insert函数结合起来考
察为什么会产生以上运行结果。
出现以上结果的原因是:stu是一个有固定地址
的变量。第一次把stu结点插入到链表中。第二次
若再用它来插入第二个结点,就把第一次结点的数据
冲掉了。
实际上并没有开辟两个结点。读者可根据insert
函数画出此时链表的情况。为了解决这个问题,必须
在每插入一个结点时新开辟一个内存区。我们修改m
ain函数,使之能删除多个结点(直到输入要删的
学号为0),能插入多个结点(直到输入要插入的学
号为0)。
main函数如下,
main()
{struct student *head,
*stu;
1on g de1 _num;
printf( "input records:
/n”);
head=creat();
print (head);
printf(, /ninput the del
eted number,");
scanf(, % 1d”,&de l_num);
while (de1 _num!=0)
{head =del(head,de l_num) ;
print(head) ;
printf(, input the del
eted number,");
scanf(, % ld",&de l_num);
printf(, \ninput the i
nserted record,");
stu=(struct student *)mal
loc(LEN) ;
scanf(, % 1d,% f,”,&stu一 >num,
&stu一 >scor );
while (stu一 >num!=0)
{ head=insert ( head, st
u ),
print ( head ) ;
prinif( "input the insert
ed record,");
s tu=(struct student *)ma
lloc(LEN) ;
s canf(, % 1d,% f,&stu一 >num,
&stu一 >score) ;
}
}
sum定义为指针变量,在需要插入时先用ma lloc函
数开辟一个内存区,将其起始地址经强制类型转换后
赋给 stu,然后输入此结构体变量中各成员的值。对
不同的插入对象,stu的值是不同的,每次指向一个
新的结构体变量。在调用 insert函数时,实参为h ea
d和 stu,将已建立的链表起始地址传给 insert函数的
形参,将 stu(既新开辟的单元的地址)传给形参
stud,函数值返回经过插入之后的链表的头指针(地
址),
运行情况如下,
input records,
89101,99
89103,87
89105,77
0,0
Now,These 3 reco rds
are。
89101 99.0
89103 87.0
89105 77.0
input the deleted number:
89103
delete:8 9103
Now,These 2 records a
re,
89101 99.0
89105 77.0
input the de1eted number:
89105
delete:89105
Now,These l recor ds ar
e,
89101 99.0
input the de1eted numbe
r:0
1nput the inserted recor
d:89104,87
NOw,These 2 records ar
e,
89101 99。0
89104 87.0
input the inserted rec
ord:89106,65
Now,These 3 records are,
891 01 99, 0
89104 87, 0
89106 65, 0
in put the inserted rec
ord:0, 0
对这个程序请读者仔细消化 。
指针的应用领域很宽广, 除了单向链表之外, 还有
环形链表, 双向链表 。 此外还有队列, 树, 栈, 图等
数据结构 。
有关这些问题的算法可以学习, 数据结构 >>课程, 在此
不作详述 。
$1 1.8 共用体
1 1.8,1 共用体的概念
有时需要使几种不同类型的变量存放到同一段内存
单元中。例如,可把一个整型变量、一个字符型变量、
一个实型变量放在同一个地址开始的内存单元中(见
图10,25)。以上三个变量在内存中占的字节数
不同,但都从同一地址开始(图中设地址为1000)
存放。 1000
i
ch
f
也就是使用覆盖技术,几个变量互相覆盖。这种使几个
不同的变量共占同一段内存的结构,称为, 共用体,
类型的结构。
, 共用体, 类型变量的定义形式为,
union 共用体名
{ 成员表列 ;
} 变量表列;
例如,
un ion data
{ int i;
ch ar ch ;
float f ;
}a,b,c ;
也可以将类型定义与变量定义分开,
un ion da ta
{int i;
c har c h;
float f ;
};
union data a,b,c;
即先定义一个union data类型,
再将 a bc定义为 union d ata类型。当然也可以直接
定义共用体变量,如,
union
{int i;
char f;
}a,b,c;
可以看到,,共用体, 与, 结构体, 的定义形式相
似。但它们的含义是不同的。
结构体变量所占内存长度是各成员占的内存长度之
和。每个成员分别占有其自己的内存单元。共用体变
量所占的内存长度等于最长的成员的长度。例如,上
面定义的, 共用体 "变量 a,b,c各占4个字节(因为一
个实型变量占4个字节),而不是各占2+ 1+4 =7
个字节。
目前国内有关C语言的书多把uniOn直译为“联
合”。作者认为,译为“共用体”
更能反映这种结构的特点, 即几个变量共用一个内存区 。
而, 联合, 这一名词;在一般意义上容易被理解为
,将两个或若干个变量联结在一起,, 难以表达这种
结构的特点 。 日本就是用, 共用体 "这一术语的 。
1 1.8.2共用体变量的引用方式
只有先定义了共用体变量才能引用它。而且不能引
用共用体变量,而只能引用共用体变量中的成员。例
如,前面定义了 a,b,c为共用体变量,下面的引用方
式是正确的,
a,i(引用共用体变量中的整型变量 i)
a,ch(引用共用体变量中的字符变量ch)
a,f(引用共用体变量中的实型变量f)
不能只引用共用体变量,例如,
printf( " %d",a)
是错误的,a的存储区有好几种类型,分别占不同长度
的存储区,仅写共用体变量名 a,难以使系统确定究竟
输出的是哪一个成员的值。应该写成prinif(,
% d,a,i)或printf(, %c",a,ch)等
1 1.8,3共用体类型数据的特点
在使用共用体类型数据时要注意以下一些特点,
1.同一个内存段可以用来存放几种不同类型的成
员,但在每一瞬时只能存放其中一种,而不是同时存
放儿种。也就是说,每一瞬时只有一个成员起作用,
其它的成员不起作用,即不是同时都存在和起作用。
2.共用体变量中起作用的成员是最后一次存放的
成员,在存入一个新的成员后原有的成员就失去作用。
如有以下赋值语句)a,i=1;
a,ch ='a';
a,f=1.5 ;
在完成以上三个赋值运算以后,只有a,f是有效的,a,
i和a,c已经无意义了。此时用 printf(, %d,,
a,i)是不行的,而用p rin tf(, % f”,a.f)
是可以的,因为最后一次的赋值是向a,f赋值。因此
在引用共用体变量时应十分注意当前存放在共用体变
量中的究竟是哪个成员。
3.共用体变量的地址和它的各成员的地址都是同一地
址。例如,&a.&a.i&a.ch.&a.f都是同一地址值,其
原因是显然的。
4.不能对共用体变量名赋值,也不能企图引用变
量名来得到成员的值,又不能在定义共用体变量时对
它初始化。例如,下面这些都是不对的,
①union
{int i;
ch ar ch ;
float f ;
}a= {1,?a?,1.5}; (不能初始化)
②a=1; (不能对共用体变量赋值)
③m=a; (不能引用共用体变量名以得到值)
5.不能把共用体变量作为函数参数,也不能使函
数带回共用体变量,但可以使用指向共用体变量的指
针(与结构体变量这种用法相仿)。
6,共用体类型可以出现在结构体类型定义中,也可以
定义共用体数组。反之,结构体也可以出现在共用体
类型定义中,数组也可以作为共用体的成员。
「例1 1,11]设有若干个人员的数据,其中有学生和
教师。学生的数据中包括:姓名,
号码, 性别, 职业, 班级 。 教师的数据包括:姓名, 号
码, 性别, 职业, 职务 。 可以看出, 学生和教师所包
含的数据是不同的 。 现要求把它们放在同一表格中,
见图10, 26 。
如果, j ob”项为, s, (学生),则第5项为cla
ss(班)。即Li是50 1班的。如果, j ob, 项是
,T, (教师),则第5项为 position(职务)。w
an g是p ro f(教授)。显然对第5项可以用共用
体来处理(将c lass和pOsiti0n放在同一
段内存中)。
要求输入人员的数据,然后再输出。可以写出下面
的算法(见图1 1.27)。按此写出程序。为简化
起见。只设两个人(一个学生、一个教师)
/* example 11.11 */
#define N 2
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<N;i++)
{
printf("Inupt the num name sex and job(s or t) \n");
scanf ("%d%s %c %c",
&person[i].num,person[i].name,&person[i].sex,&person[i]
.job);
if (person[i].job =='s')
{printf("input class num\n");
scanf ("%d",&person[i].category.class);}
else if (person[i].job =='t')
{printf("input position\n");
scanf("%s",person[i].category.position);}
else printf("input error!");
}
printf("\n");
printf("No,name sex job class/position\n");
for (i=0;i<N;i++)
{if (person[i].job=='s')
printf("%-6d%- 10s%-3c%-3c %-
10d\n",person[i].num,
person[i].name,person[i].sex,person[i].job,
person[i].category.class);
else printf("%-6d%-10s%-3c%-3c %-
10s\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
可以看到:在ma in函数之前定义了外部的结构体数组
person,在结构体类型定义中包括了共用体类
型,category(分类)是结构体中一个成员
名,在这个共体中一个成员为class和posi
tion,前者为整型,后者为字符数组(存放, 职
务, 的值 ——字符串)。
这种共用体变量的用法是很有用的 。 例如, 可以设
一个单元 data,内放一个常量, 此常量可以是整型,
实型或字符型, 双精度等 。
在程序中根据, 类型标志, 来决定按何种类型处理。
如,
struct
{..,
un ion
{ int I;
char ch ;
float f;
double d ;
}data;
int ty pe;
)a;
switch(a,type)
case 0,printf(, %d \n”,a,data.i); break;
Case 1:printf(, %c \n",a,data.ch); break;
Case 2:printf(, % f\n”a,data.f); break;
Case 3:printf(, %f \n”,a,data.d); break;
}
结构体中的typ e作为, 类型标志,,如果在共用体
成员中存放整数,则使ty pe=0;若存放字符,则
使typ e=1;若存放实型,则使typ=2;若
存放双精度型,使typ e=3。然后在switch
语句中根据a,type的值来决定按哪一种类型输出。
这在写系统软件时,用来处理符号表是有用的。在符
号表中可以包含符号名、类型和值。根据不同情况进
行不同处理。
1 1.9 枚举类型
枚举类型是 ANSI C新标准所增加的。
如果一个变量只有几种可能的值,可以定义为 枚举类型 。所
谓, 枚举, 是指将变量的值一一列举出来,变量的值只限于列
举出来的值的范围内。
定义枚举类型用 enum(enumerate)开头。例如
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 weekday
{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);
[例1 1.12 】 口袋中有红、黄、蓝、白、黑五种
颜色的球若干个。每次从口袋中取出3个球,问得到
三种不同色的球的可能取法,打印出每种组合的三种
颜色。
球只能是5种色之一,而且要判断各球是否同色,应该
用枚举类型变量处理。
设取出的球为 I,j,k.根据题意,I,j,k分别是5种
色球之一,并要求 I!=j!=k.可以用穷举法,即一种可
能一种可能地试,看哪一组符合条件。
算法可用图10。28表示。
用n累计得到三种不同色球的次数。外循环使第一个
球i从red变到black。中循环使第二个球j
也从red变到black。如果i和j同色则不可
取;
只有 I,j不同色 (I!=j)时才需要继续找第三个球,此时第三
个球k也有5种可能(red到black),但要
求第三个球不能与第一个球或第二个球同色,即
k !=I,k!=j.满足此条件就得到三种不同色的球。输出
这种三色组合方案。然后使 n加1。外循环全部执行
完后,全部方案就已输
出完了 。 最后输出总数 n,
下面的问题是如何实现图10, 28中的, 输出一
种取法, 。 这里有一个问题,
如何输出, red,,, blue,, ……等单词。不
能写成printf( "% s",red)来输出, r
ed, 字符串。可以采用图10.29的方法。
为了输出3个球的颜色,显然应经过三次循环,第
一次输出i的颜色,第二次输出j的颜色,第三次输
出k的颜色。在三次循环中先后将 I,j,k赋予pri。
然后根据pri的值输出颜色信息。在第一次循环时,
pri的值为1,如果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<=balack;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)
{casered: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("\ntoal:%5d\n",n);
}
运行结果如下,
1 red yellow blue
2 red yellow white
3 red yellow black
4 red blue yellow
5 red blue white
6 red blue black
7 red white yellow
8 red white blue
9 red white black
10 red black yellow
。。。。。。。。。。。。。。。。。
60 black white blue
tota1:60
有人说,不用枚举变量而用常数0表, 红,, 1表
,黄 ………,不也可以吗?是的,完全可以。但显然用
枚举变量更直观,因为枚举元素都选用了令人, 见名
知意, 标识符,而且枚举变量的值限制在定义时规定
的几个枚举元素范围内,如果赋予它一个其它的值,
就会出现出错信息,便于检查。
$11.10 用 typdef定义类型
除了可以直接使用C提供的标准类型名(如
int.char,float.double.long等)和自己定义的结
构体、共用体、指针、枚举类型外还可以用 typedef
定义新的类型名来代替已有的类型名如,
typedef int INTEGER;
typedef float REAL;
指定用 INTEGER 代表 int类型,REAL代表 float,这样,
以下两行等价,
① int i,j;; float a,b;
② INTEGER i,j; REAL a,b;
这样可以使熟悉F0RTRAN的人能用 INTEGER 和
REALL定义变量,以适应他们的习惯。
如果在一个程序中,一个整型变量用来计数,可以,
Typedef int COUNT;
COUNT i,j;
即将变量 i.j定义为 COUNT类型,而 COUNT等价于 int,因此 i、j是
整型。但在程序中将 i,j定为 COUNT类型,可以使人更一目了然
地知道它们是用于计数的。
可以定义结构体类型,
typedef struct
{int mont;
int day;
int year;
} DATE;
定义新类型名 DATE,它代表上面定义的一个结构体类型。这时就
可以用 DATE定义变量,
DATE birthday;(不要写成 struct DATA birthday;)
DATA *p ; (p为指向此结构体类型数据的指针)
还可以进一步,
1 typedef NUM[ 100];(定义 NUM为整型数
组类型)
NUM n; (定义n为整型数组变量)
2,typedef char *STRING; ( 定义 STRING为字符指
针类型 )
STRING p, s[10 ]; ( p为字符指变量, s为指针
数组 )
3, typed int (*POINTER)() ( 定义 POINTER
为指向函数的指针类型, 该函数返回整型值 )
POINTER p1,p2; (p1,p2为 POINTER 类型的指针变量 )
归纳起来, 定义一个新的类型名的方法是,
① 先按定义变量的方法写出定义体(如,int i;)。
②将变量名换成新类型名(如:将i换成 COUNT)。
③在最前面加 typedef(如,typedef int COUNT)。
④然后可以用新类型名去定义变量。
再以定义上述的数组类型为例来说明,
①先按定义数组变量形式书写,int n[100];
② 将变量名n换成自己指定的类型名,int NUM[100]
③ 在前面加上 typedef,得到 typedef int NUM[100];
④ 用来定义变量,NUM n,
同样,对字符指针类型,也是:①char *p;
② char * STRING;③ typedef *STRING;
④ STRING p,s[10];
习惯上常把用 typedef定义的类型名用大写字母表
示,以便与系统提供的标准类型标识符相区别。
说明,
1.用 typedef可以定义各种类型名,但不能
用来定义变量。用 typedf可以定义出数组类型、字符
串类型,使用比较方便。如定义数组,原来是用
int a [10],b[10 ],c[10,d[10 ];
由于都是一维数组,大小也相同,可以先将此数组类型
定为一个名字,
ty pedef int ARR [10];
然后用ARR去定义数组变量,
ARRa,b,c,d;
ARR为数组类型,它包含10个元素。因此,
a,b,c,d都被定义为一维数组,含10个元素。
可以看到,用 typedef可以将数组类型和数组
变量分离开来,利用数组类型可以定义多个数组变量。
同样可以定义字符串类型、指针类型等。
2.用 typedef只是对已经存在的类型增加一
个类型名,而没有创造新的类型。
例如,前面定义的整型类型 COUNT,它无非是对in
t型另给一个新名字。又如
typedef int NUM[10];
无非是把原来用 "int n[10 ]; "定义的数组变量的类
型用一个新的名字NUM表示出来。无论用哪种方式
定义变量,效果都是一样的。
3,typedef与#define有相似之处,

typd ef int COUNT;
和 #define COUNT in t
的作用都是用 COUNT代表int。但事实上,它们二
者是不同的。#define是在预编译时处理的,
它只能作简单的字符串替换,而typedef是在
编译时处理的。实际上它并不是作简单的字符串替换,
例如
typedef int NUM [10 ]
并不是用NUM [10]去代替int,而是采用如同
定义变量的方法那样来定义一个类型
(就是前面介绍过的将原来的变量名换成类型名 ),
4.当不同源文件中用到同一类型数据(尤其是像数
组、指针、结构体、共用体等类型数据)时,常用t
y Pedef定义一些数据类型,把它们单独放在一
个文件中,然后在需要用到它们的文件中用#inc
lude命令把它们包含进来。
5, 使用ty Pedef有利于程序的通用与移植 。
有时程序会依赖于硬件特性, 用typedef便于
移植 。 例如,
有的计算机系统1nt型数据用两个字节,数值范围为
一32768~32767,而另外一些机器则以4
个字节存放一个整数,数值范围为 + -21亿。如果
把一个C程序从一个以4个字节存放整数的计算机系
统移植到以2个字节存放整数的系统,按一般办法需
要将定义变量中的每个 int改为1ong。例如,
将, int a,b,c;”,如果程序中有多处用in
t定义变量,则要改动多处。现可以用一个type
def定义,
typ ed ef int INTEG ER ;
在程序中用INTEGER定义变量。在移植时只需改
动 typedef定义体即可,
typ ed ef long INTEGER ;