第 11章 结构体与共用体
11.1 概述
11.2 定义结构体类型变量的方法
11.3 结构体变量的引用
11.4 结构体变量的初始化
11.5 结构体数组
11.6 指向结构体类型数据的指针
11.7 用指针处理链表
11.8 共用体
11.9 枚举类型
11.10 用 typedef定义类型习题
11.1 概述迄今为止,已介绍了基本类型 (或称简单类型 )的变量 (如整型、实型、字符型变量等 ),也介绍了一种构造类型数据 ——数组,数组中的各元素是属于同一个类型的。
但是只有这些数据类型是不够的。有时需要将不同类型的数据组合成一个有机的整体,以便于引用。这些组合在一个整体中的数据是互相联系的。例如,一个学生的学号、姓名、性别、年龄、成绩、家庭地址等项。这些项都与某一学生相联系。见图 11.1。可以看到性别 (sex)、年龄 (age)、成绩 (score)、地址 (addr)是属于学号为 10010和名为,Li Fun”的学生的。如果将
num,name,sex,age,score,addr分别定义为互相独立的简单变量,难以反映它们之间的内在联系。
应当把它们组织成一个组合项,在一个组合项中包含若干个类型不同 (当然也可以相同 )的数据项。 C
语言允许用户自己指定这样一种数据结构,它称为结构体 (structure)。它相当于其他高级语言中的
“记录”。
图 11.1
假设程序中要用到图 11.1所表示的数据结构,但是 C
语言没有提供这种现成的数据类型,因此用户必须要在程序中建立所需的结构体类型。例如:
struct student
{int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
注意不要忽略最后的分号。上面由程序设计者指定了一个新的结构体类型 struct student(struct是声明结构体类型时所必须使用的关键字,不能省略 ),
它向编译系统声明这是一个“结构体类型”,它包括 num,name,sex,age,score,addr等不同类型的数据项。应当说明 struct student是一个类型名,它和系统提供的标准类型 (如 int,char、
float,double等 )一样具有同样的地位和作用,都可以用来定义变量的类型,只不过结构体类型需要由用户自己指定而已。
声明一个结构体类型的一般形式为
struct结构体名
{成员表列 };
“结构体名” 用作结构体类型的标志,它又称“结构体标记” (structure tag) 。上面的结构体声明中 student就是结构体名 (结构体标记 )。大括弧内是该结构体中的各个成员,
由它们组成一个结构体。例如,上例中的 num,name、
sex等都是成员。对各成员都应进行类型声明,即类型名成员名也可以把“成员表列”称为“域表”。每一个成员也称为结构体中的一个域。成员名定名规与变量名同。
“结构体”这个词是根据英文单词 structure译出的。有些 C
语言书把 structure直译为“结构”。作者认为译作“结构”
会与一般含义上的“结构”混淆 (例如,数据结构、程序结构、控制结构等 )。日本把 structure译作“结构体”或
“构造体”,作者认为译作“结构体”比译作“结构”更确切一些,不致与一般含义上的“结构”混淆。
11.2 定义结构体类型变量的方法前面只是指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元。为了能在程序中使用结构体类型的数据,应当定义结构体类型的变量,并在其中存放具体的数据。可以采取以下三种方法定义结构体类型变量。
1,先声明结构体类型再定义变量名如上面已定义了一个结构体类型 struct student,可以用它来定义变量。如,
struct student student1,student2
结构体类型名 结构体变量名 ;
定义了 student1和 student2为 struct student类型的变量,即它们具有 struct student类型的结构。如图 11.2所示。
图 11.2
在定义了结构体变量后,系统会为之分配内存单元。
例如 student1和 student2在内存中各占 59个字节
(2+20+1+2+4+30=59)。
应当注意,将一个变量定义为标准类型 (基本数据类型 )
与定义为结构体类型不同之处在于后者不仅要求指定变量为结构体类型,而且要求指定为某一特定的结构体类型 (例如 struct student类型 )。因为可以定义出许许多多种具体的结构体类型。而在定义变量为整型时,只需指定为 int型即可。
如果程序规模比较大,往往将对结构体类型的声明集中放到一个文件 (以,h为后缀的“头文件” )中。
哪个源文件需用到此结构体类型则可用 #include命令将该头文件包含到本文件中。这样做便于装配,
便于修改,便于使用。
2,在声明类型的同时定义变量例如:
struct student
{int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
}student1,student2;
它的作用与第一种方法相同,即定义了两个 struct
student类型的变量 student1,student2。这种形式的定义的一般形式为
struct 结构体名
{
成员表列
}变量名表列;
3,直接定义结构类型变量其一般形式为
struct
{
成员表列
}变量名表列;即不出现结构体名。
关于结构体类型,有几点要说明:
(1) 类型与变量是不同的概念,不要混同。只能对变量赋值、存取或运算,而不能对一个类型赋值、
存取或运算。在编译时,对类型是不分配空间的,
只对变量分配空间。
(2) 对结构体中的成员 (即“域” ),可以单独使用,
它的作用与地位相当于普通变量。关于对成员的引用方法见 11.3节。
(3) 成员也可以是一个结构体变量。
如:
struct date /*声明一个结构体类型 */
{int month;
int day;
int year;
};
struct student
{int num;
char name[20];
char sex;
int age;
struct date birthday; /*birthday是 struct date
类型 */
char addr[30];
}student1,student2;
先声明一个 struct date类型,它代表“日期”,包括 3个成员,month(月 ),day(日 ),year(年 )。然后在声明 struct student类型时,将成员 birthday
指定为 struct date类型。 struct student的结构见图
11.3所示。已声明的类型 struct date与其他类型 (如
int,char)一样可以用来定义成员的类型。
(4)成员名可以与程序中的变量名相同,二者不代表同一对象。例如,程序中可以另定义一个变量 num,它与 struct student中的 num是两回事,互不干扰。
图 11.3
11.3 结构体变量的引用在定义了结构体变量以后,当然可以引用这个变量。
但应遵守以下规则,
(1) 不能将一个结构体变量作为一个整体进行输入和输出。例如,已定义 student1和 student2为结构体变量并且它们已有值。不能这样引用,
printf ("%d,%s,%c,%d,%f,%s\n",student1);
只能对结构体变量中的各个成员分别进行输入和输出。引用结构体变量中成员的方式为结构体变量名,成员名例如,student1.num表示 student1变量中的 num成员,
即 student1的 num(学号 )项。可以对变量的成员赋值,例如,
student1.num=10010;
“.”是成员 (分量 )运算符,它在所有的运算符中优先级最高,因此可以把 student 1.num作为一个整体来看待。上面赋值语句的作用是将整数 10010赋给
student 1变量中的成员 num。
(2) 如果成员本身又属一个结构体类型,则要用若干个成员运算符,一级一级地找到最低的一级的成员。
只能对最低级的成员进行赋值或存取以及运算。
例如,对上面定义的结构体变量 student1,可以这样访问各成员,
student1.num
student1.birthday.month
注意,不能用 student1.birthday来访问 student1变量中的成员
birthday,因为 birthday本身是一个结构体变量。
(3) 对结构体变量的成员可以像普通变量一样进行各种运算
(根据其类型决定可以进行的运算 )。例如:
student2,score=student1,score;
sum=student1,score+student2,score;
student1,age++;
++student1,age;
由于“.”运算符的优先级最高,因此 student1,age++是对
student1,age进行自加运算,而不是先对 age进行自加运算。 (4) 可以引用结构体变量成员的地址,也可以引用结构体变量的地址。如,scanf("%d",&student1.num); (输入 student1,num的值 )printf("%o",&student1); (输出
student1的首地址 )但不能用以下语句整体读入结构体变量,
如:
scanf("%d,%s,%c,%d,%f,%s",&student1);
结构体变量的地址主要用于作函数参数,传递结构体的地址。
11.4 结构体变量的初始化和其他类型变量一样,对结构体变量可以在定义时指定初始值。
例 11.1对结构体变量初始化。
main()
{struct student
{long int num;
char name[20];
char sex;
char addr[20];
}a={89031,"Li Lin",'M',"123 Beijing
Road"};
printf("NO.:%ld\nname:%s\nsex:%c\naddress:%
s\n",a.num,a.name,a.sex,a.addr);
}
运行结果如下:
No.,89031
name,Li Lin
sex,M
address,123 Beijing Road
11.5 结构体数组一个结构体变量中可以存放一组数据 (如一个学生的学号、姓名、成绩等数据 )。如果有 10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。结构体数组与以前介绍过的数值型数组不同之处在于每个数组元素都是一个结构体类型的数据,它们都分别包括各个成员 (分量 )项。
11.5.1 定义结构体数组和定义结构体变量的方法相仿,只需说明其为数组即可。如:
struct student
{int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct student stu[3];
以上定义了一个数组 stu,其元素为 struct student类型数据,数组有 3个元素。也可以直接定义一个结构体数组,如:
struct student
{int num;

}stu[3];

struct
{int num;

}stu[3];
图 11.4
数组各元素在内存中连续存放,见图 11.5示意。
11.5.2结构体数组的初始化与其他类型的数组一样,
对结构体数组可以初始化。
如:
struct student
{int num;
char name[20];
char sex;
int age;
图 11.5
float score;
char add[30];
}
stu[3]={{10101,,Li Lin”,‘ M?,18,87,5,
,103 Beijing Road”},{10102,,Zhang Fun”,
‘ M?,19,99,,130 Shanghai Road”},{10104,
“W ang Min”,‘ F?,20,78,5,,1010
Zhongshan Road”}};
定义数组 stu时,元素个数可以不指定,即写成以下形式,stu[ ]= {…},{…},{…} ;编译时,系统会根据给出初值的结构体常量的个数来确定数组元素的个数。
当然,数组的初始化也可以用以下形式:
struct student
{int num;

};
struct student stu[ ]={{…},{…},{…}} ;
即先声明结构体类型,然后定义数组为该结构体类型,在定义数组时初始化。从以上可以看到,结构体数组初始化的一般形式是在定义数组的后面加上,={初值表列 };
11.5.3 结构体数组应用举例下面举一个简单的例子来说明结构体数组的定义和引用。
例 11.2对候选人得票的统计程序。设有 3个候选人,
每次输入一个得票的候选人的名字,要求最后输出各人得票结果。
程序如下:
#include <string.h>
struct person
{char name[20];
int count;
}
leader[3]={"Li",0,"Zhang",0,"Fun",0};
main()
{int i,j;
char leader-name[20];
for (i=1; i< =10; i++)
{scanf("%s",leader-name);
for(j=0; j< 3; j++)
if(strcmp(leader-name,leader[j].name)==0)
leader[j].count++:
}
printf("\n");
for(i=0; i< 3; i++)
printf("%5s:%d\n",leader[i].name,
leader[i].count);
}
运行情况如下:
Li
Li
Fun
Zhang
Zhang
Fun
Li
Fun
Zhang
Li
Li∶ 4
Zhang∶ 3
Fun∶ 3
程序定义一个全局的结构体数组 leader,它有 3个元素,每一元素包含两个成员 name(姓名 )和 count(票数 )。在定义数组时使之初始化,使 3位候选人的票数都先置零。见图 11.6。
图 11.6
在主函数中定义字符数组 leader-name,它代表被选人的姓名,在 10次循环中每次先输入一个被选人的具体人名,然后把它与 3个候选人姓名相比,看它和哪一个候选人的名字相同。注意 leader-name
是和 leader[j],name相比,leader[j]是数组 leader
的第 j个元素,它包含两个成员项,leader-name应该和 leader数组第 j个元素的 name成员相比。
若 j为某一值时,输入的姓名与 leader[j],name相等,
就执行,leader[j],count++”,由于成员运算符
,·”优先于自增运算符,++”,因此它相当于
(leader[j],count)++,使 leader[j]的成员 count的值加 1。在输入和统计结束之后,将 3人的名字和得票数输出。
11.6 指向结构体类型数据的指针一个结构体变量的指针就是该变量所占据的内存段的起始地址。可以设一个指针变量,用来指向一个结构体变量,此时该指针变量的值是结构体变量的起始地址。指针变量也可以用来指向结构体数组中的元素。
11.6.1 指向结构体变量的指针下面通过一个简单例子来说明指向结构体变量的指针变量的应用。
例 11.3指向结构体变量的指针的应用。
#include <string.h>
main()
{struct student
{long num;
char name[20];
char sex;
float score;
};
struct student stu-1;
struct student * 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\nscore:%f\n
",stu-1.num,stu-1.name,stu-1.sex,stu-
1.score);
printf("No.:%ld\nname:%s\nsex:%c\nscore:%f\n",
(*p).num,(*p).name,(*p).sex,(*p).score);
}
在主函数中声明了 struct student类型,然后定义一个 struct student类型的变量 stu-1。同时又定义一个指针变量 p,它指向一个 struct student类型的数据。在函数的执行部分将结构体变量 stu-1的起始地址赋给指针变量 p,也就是使 p指向 stu-1(见图
11.7),然后对 stu-1的各成员赋值。谝桓鰌 rintf函数是输出 stu-1的各个成员的值。用 stu-1,num表示 stu-1中的成员 num,余类推。第二个 printf函数也是用来输出 stu-1各成员的值,但使用的是
(*p),num这样的形式。
(*p)表示 p指向的结构体变量,(*p),num是 p指向的结构体变量中的成员 num。注意 *p两侧的怂惴“.”优先于,*”
运算符,*p,num就等价于 *(p,num)了。
图 11.7
程序运行结果如下:
No.,89101
name,Li Lin
sex,M
score,89,500000
No,89101
name,Li Lin
sex,M
score,89,500000
可见两个 printf函数输出的结果是相同的。
在 C语言中,为了使用方便和使之直观,可以把
(*p),num改用 p—> num来代替,它表示 *p所指向的结构体变量中的 num成员。同样,(*p).name
等价于 p—> name。也就是说,以下三种形式等价:
① 结构体变量.成员名
② (*p).成员名
③ p->成员名上面程序中最后一个 printf函数中的输出项表列可以改写为 p->num,p->name,p->sex,p->score 其中 ->称为指向运算符。
请分析以下几种运算:
p->n得到 p指向的结构体变量中的成员 n的值。
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},{10102,,Zhang Fun”,‘ M?,19},
{10104,“W ang Min”,‘ F?,20}};
main()
{struct student*p;
printf(" No.Namesexage\n");
for (p=stu; p< stu+3; p++)
printf("%5d %-20s %2c %4d\n",p->num,p-
>name,p->sex,p->age);
}
运行结果如下:
No,Name sex age
10101 Li Lin M 18
10102 Zhang Fun M 19
10104 W ang Min F 20
p是指向 struct student结构体类型数据的指针变量。
在 for语句中先使 p的初值为 stu,也就是数组 stu的起始地址,见图 11.8中 p
的指向。在第一次循环中输出 stu[0]的各个成员值。
然后执行 p++,使 p自加 1。
p加 1意味着 p所增加的值为结构体数组 stu的一个元素所占的字节数 (在本例中为 2+20+1+2=25字节 )。执行 p++后 p的值等图 11.8
于 stu+1,p指向 stu[1]的起始地址,见图 11.8中
p?的指向。在第二次循环中输出 stu[1]的各成员值。
在执行 p++后,p的值等于 stu+2,它的指向见图
11.8中的 p″。再输出 stu[2]的各成员值。
在执行 p++后,p的值变为 stu+3,已不再小于 stu+3
了,不再执行循环。
注意以下两点:
(1) 如果 p的初值为 stu,即指向第一个元素,则 p加 1
后 p就指向下一个元素的起始地址。例如:
(++p)->num 先使 p自加 1,然后得到它指向的元素中的 num成员值 (即 10102)。
(p++)->num 先得到 p->num的值 (即 10101),然后使 p自加 1,指向 stu[1]。
请注意以上二者的不同。
(2) 程序已定义了 p是一个指向 struct student类型数据的指针变量,它用来指向一个 struct student型的数据 (在例 11.4中 p的值是 stu数组的一个元素 (如
stu[0],stu[1])的起始地址 ),不应用来指向 stu数组元素中的某一成员。例如,下面的用法是不对的:
p=stu[1],name
编译时将给出警告信息,表示地址的类型不匹配。
千万不要认为反正 p是存放地址的,可以将任何地址赋给它。如果地址类型不相同,可以用强制类型转换。例如:
p=(struct student *)stu[0],name;
此时,p的值是 stu[0] 元素的 name成员的起始地址。
可以用,printf("%s",p);”输出 stu[0]中成员
name的值,但是,p仍保持原来的类型。执行
,printf("%s",p+1);”,则会输出 stu[1]中 name
的值。执行 p+1时,p的值增加了结构体 struct
student的长度。
11.6.3 用结构体变量和指向结构体的指针作函数参数将一个结构体变量的值传递给另一个函数,有 3种方法:
(1) 用结构体变量的成员作参数。例如,用
stu[1],num或 stu[2],name作函数实参,将实参值传给形参。用法和用普通变量作实参是一样的,属于“值传递”方式。应当注意实参与形参的类型保持一致。
(2) 用结构体变量作实参。老版本的 C系统不允许用结构体变量作实参,ANSI C取消了这一限制。但是用结构体变量作实参时,采取的是“值传递”的方式,将结构体变量所占的内存单元的内容全部顺序传递给形参。形参也必须是同类型的结构体变量。在函数调用期间形参也要占用内存单元。这种传递方式在空间和时间上开销较大,如果结构体的规模很大时,
开销是很可观的。此外,由于采用值传递方式,如果在执行被调用函数期间谋淞诵尾?也是结构体变量 )的值,
该值不能返回主调函数,这往往造成使用上的不便。
因此一般较少用这种方法。
(3) 用指向结构体变量 (或数组 )的指针作实参,将结构体变量 (或数组 )的地址传给形参。
例 11.5有一个结构体变量 stu,内含学生学号、姓名和 3门课的成绩。要求在 main函数中赋以值,在另一函数
print中将它们打印输出。今用结构体变量作函数参数。
#include <string.h>
#define FORMAT "%d\n%s\n%f\n%f\n%f\n"
struct student
{int num;
char name[20];
float score[3];
};
main()
{void print(struct student);
struct student stu;
stu,num=12345;
strcpy(stu,name,"Li Li");
stu,score[0]=67,5;
stu,score[1]=89;
stu,score[2]=78,6;
print(stu);
}
void print(struct student stu)
{printf(FORMAT,stu.num,stu.name,
stu.score[0],stu.score[1],stu.score[2]);
printf("\n");
}
运行结果为:
12345
Li Li
67,500000
89,000000
78,599998
struct student被定义为外部类型,这样,同一源文件中的各个函数都可以用它来定义变量的类型。
main函数中的 stu定义为 struct student类型变量,
print函数中的形参 stu也定义为 struct student类型变量。在 main函数中对 stu的各成员赋值。在调用
print函数时以 stu为实参向形参 stu实行“值传递”。
在 print函数中输出结构体变量 stu各成员的值。
例 11.6将上题改用指向结构体变量的指针作实参。
可以在上面程序的基础上作少量修改即可。请注意程序注释。
#define FORMAT "%d\n%s\n%f\n%f\n%f\n"
struct student
{int num;
char name[20];
float score[3];
}
stu={12345,"Li Li",67.5,89,78.6};
main()
{ void print(struct student *); /*形参类型修改成指向结构体的指针变量 */
print(&stu); /*实参改为 stu的起始地址 */
}
void print(struct student *p) /*形参类型修改了 */
{printf(FORMAT,p->num,p->name,p-
>score[0],p->score[1],p->score[2]); /*用指针变量调用各成员之值 */
printf("\n");
}
此程序改用在定义结构体变量 stu
时赋初值,这样程序可简化些。
print函数中的形图 11.9参 p被定义为指向 struct student类型数据的指针变量。注意在调用 print
函数时,用结构体变量 stu的起始地址 &stu作实参。在调用函数时将该地址传送给形参 p(p是指针变量 )。这样 p就指向 stu,
见图 11.9。在 print函数中输出 p
所指向的结构体变量的各个成员值,它们也就是 stu的成员值。
图 11.9
main函数中的对各成员赋值也可以改用 scanf函数输入。即用 scanf("%d%s%f%f%f",&stu,num,
stu,name,&stu,score[0],&stu,score[1],
&stu,score[2]);
输入时用下面形式输入:
12345Li-Li67,58978,6
注意:输入项表列中 stu,name前没有,&”符号,
因为 stu,name是字符数组名,本身代表地址,不应写成 &stu,name。
用指针作函数参数比较好,能提高运行效率。
11.7 用指针处理链表
11.7.1 链表概述
链表是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。我们知道,用数组存放数据时,必须事先定义固定的长度 (即元素个数 )。比如,有的班级有 100人,而有的班只有 30
人,如果要用同一个数组先后存放不同班级的学生数据,则必须定义长度为 100的数组。如果事先难以确定一个班的最多人数,则必须把数组定得足够大,以能存放任何班级的学生数据。显然这将会浪费内存。链表则没有这种缺点,它根据需要开辟内存单元。图 11.10表示最简单的一种链表
(单向链表 )的结构。
图 11.10
链表有一个“头指针”变量,图中以 head表示,它存放一个地址。该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应包括两个部分:一为用户需要用的实际数据,二为下一个结点的地址。可以看出,head指向第一个元素;第一个元素又指向第二个元素 …… 直到最后一个元素,该元素不再指向其他元素,它称为“表尾”,它的地址部分放一个
,NULL”(表示“空地址” ),链表到此结束。
可以看到链表中各元素在内存中可以不是连续存放的。要找某一元素,必须先找到上一个元素,根据它提供的下一元素地址才能找到下一个元素。
如果不提供“头指针” (head),则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。打个通俗的比方:幼儿园的老师带领孩子出来散步,老师牵着第一个小孩的手,
第一个小孩的另一只手牵着第二个孩子 …… 这就是一个“链”,最后一个孩子有一只手空着,他是“链尾”。要找这个队伍,必须先找到老师,
然后顺序找到每一个孩子。
可以看到,这种链表的数据结构,必须利用指针变量才能实现。即:一个结点中应包含一个指针变量,用它存放下一结点的地址。
前面介绍了结构体变量,用它作链表中的结点是最合适的。一个结构体变量包含若干成员,这些成员可以是数值类型、字符类型、数组类型,也可以是指针类型。我们用这个指针类型成员来存放下一个结点的地址。例如,可以设计这样一个结构体类型:
struct student
{int num;
float score;
struct student next;
};
其中成员 num和 score用来存放结点中的有用数据 (用户需要用到的数据 ),相当于图 11.10结点中的 A,
B,C,D。 next是指针类型的成员,它指向 struct
student类型数据 (这就是 next所在的结构体类型 )。
一个指针类型的成员既可以指向其他类型的结构体数据,也可以指向自己所在的结构体类型的数据。现在,next是 struct student类型中的一个成员,它又指向 struct student类型的数据。用这种方法就可以建立链表。见图 11.11。
图 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结点中的数据。
本例是比较简单的,所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。
11.7.3 处理动态链表所需的函数前面讲过,链表结构是动态地分配存储的,即在需要时才开辟一个结点的存储单元。怎样动态地开辟和释放存储单元呢? C语言编译系统的库函数提供了以下有关函数。
1,malloc函数其函数原型为
void *malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为 si
z e的连续空间。此函数的值 (即“返回值” )是一个指向分配域起始地址的指针 (基类型为 void)。如果此函数未能成功地执行 (例如内存空间不足 ),则返回空指针 (NULL)。
2,calloc函数其函数原型为
void *calloc(unsigned n,unsigned siz e);
其作用是在内存的动态区存储中分配 n个长度为 siz
e的连续空间。函数返回一个指向分配域起始地址的指针;如果分配不成功,返回 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 建立动态链表所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,
即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。
例 11.8写一函数建立一个有 3名学生数据的单向动态链表。
先考虑实现此要求的算法
(见图 11.12)。 图 11.12
设 3个指针变量,head,p1,p2,它们都是用来指向
struct student类型数据的。先用 malloc函数开辟第一个结点,并使 p1,p2指向它。然后从键盘读入一个学生的数据给 p1所指的第一个结点。我们约定学号不会为零,如果输入的学号为 0,则表示建立链表的过程完成,该结点不应连接到链碇小O仁筯
ead的值为 NULL(即等于 0),这是链表为“空”时的情况 (即 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 siz eof(struct student)
struct student
{long num;
float score;
struct student next;
};
int n; /*n为全局变量,本模块中各函数均可使用它 */
struct student *creat(void)/*定义函数。此函数带回一个指向链表头的指针 */
{struct student head;
struct student p1,*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类型数据的长度,siz eof是“求字节数运算符”。
(2) 第 9行定义一个 creat函数,它是指针类型,即此函数带回一个指针值,它指向一个 struct student
类型数据。实际上此 creat函数带回一个链表起始地址。
(3) malloc(LEN)的作用是开辟一个长度为 LEN的内存区,LEN已定义为 siz eof(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后移一个结点,再如此进行下去,直到遇到表尾为止。
图 11.20
可以设两个指针变量 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 student del(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:"); / 输入要插入的结点 */
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)存放。也就是使用覆盖技术,几个变量互相覆盖。这种使几个不同的变量共占同一段内存的结构,称为“共用体”类型的结构。
定义共用体类型变量的一般形式为
union 共用体名图 11.24
{ 成员表列
}变量表列;
例如:
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(职务 )。W ang是
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 W ang m t professor
No,Name sex job class/position
101 Li f s 501
102 W ang 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 int INTEGER;
typedef float REAL;
指定用 INTEGER代表 int类型,REAL代表 float。这样,以下两行等价:
① int i,j; float a,b;
② INTEGER i,j; REAL a,b;
这样可以使熟悉 FORTRAN的人能用 INTEGER和
REAL定义变量,以适应他们的习惯。
如果在一个程序中,一个整型变量用来计数,可以:
typedef int COUNT;
COUNT i,j;
即将变量 i,j定义为 COUNT类型,而 COUNT等价于 int,因此 i,j是整型。在程序中将 i,j定为
COUNT类型,可以使人更一目了然地知道它们是用于计数的。
可以声明结构体类型:
typedef struct
{int month;
int day;
int year;
}DATE;
声明新类型名 DATE,它代表上面指定的一个结构体类型。这时就可以用 DATE定义变量:
DATE birthday; (不要写成 struct DATE
birthday; )
DATE *p; (p为指向此结构体类型数据的指针 )
还可以进一步:
① typedef int NUM[100]; (声明 NUM为整型数组类型 )
NUM n; (定义 n为整型数组变量 )
② typedef char *STRING; (声明 STRING为字符指针类型 )STRING p,s[10]; (p为字符指针变量,
s为指针数组 )
③ typedef 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 char *STRING;
④ STRING p,s[10];习惯上常把用 typedef声明的类型名用大写字母表示,以便与系统提供的标准类型标识符相区别。
说明:
(1) 用 typedef可以声明各种类型名,但不能用来定义变量。用 typedef可以声明数组类型、字符串类型,使用比较方便。如定义数组,原来是用 int
a[10],b[10],c[10],d[10];由于都是一维数组,
大小也相同,可以先将此数组类型声明为一个名字,typedef int ARR[10];然后用 ARR去定义数组变量:
ARR a,b,c,d;
ARR为数组类型,它包含 10个元素。因此,a,b、
c,d都被定义为一维数组,含 10个元素。
可以看到,用 typedef可以将数组类型和数组变量分离开来,利用数组类型可以定义多个数组变量。
同样可以定义字符串类型、指针类型等。
(2) 用 typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型。
例如,前面声明的整型类型 COUNT,它无非是对
int型另给一个新名字。又如,typedef int
NUM[10];无非是把原来用,int n[10];”定义的数组变量的类型用一个新的名字 NUM表示出来。
无论用哪种方式定义变量,效果都是一样的。
(3) typedef 与 #define 有相似之处,如:
typedef int COUNT;
和 #define COUNT int的作用都是用 COUNT代表
int。但事实上,它们二者是不同的。 #define是在预编译时处理的,它只能作简单的字符串替换,
而 typedef是在编译时处理的。实际上它并不是作简单的字符串替换,例如:
typedef int NUM[10];
并不是用,NUM[10]”去代替,int”,而是采用如同定义变量的方法那样来声明一个类型 (就是前面介绍过的将原来的变量名换成类型名 )。
(4) 当不同源文件中用到同一类型数据 (尤其是像数组、指针、结构体、共用体等类型数据 )时,常用
typedef声明一些数据类型,把它们单独放在一个文件中,然后在需要用到它们的文件中用 #include
命令把它们包含进来。
(5) 使用 typedef有利于程序的通用与移植。有时程序会依赖于硬件特性,用 typedef便于移植。例如,
有的计算机系统 int型数据用两个字节,数值范围为 -32768~ 32767,而另外一些机器则以 4个字节存放一个整数,数值范围为 ± 21亿。如果把一个 C程序从一个以 4个字节存放整数的计算机系统移植到以 2个字节存放整数的系统,按一般办法需要将定义变量中的每个 int改为 long。例如,将
,int a,b,c;”改为,long a,b,c;”,
如果程序中有多处用 int定义变量,则要改动多处。
现可以用一个 INTEGER来声明 int:
typedef int INTEGER;
在程序中所有整型变量都用 INTEGER定义。在移植时只需改动 typedef定义体即可:
typedef long INTEGER;
习题
11.1 定义一个结构体变量 (包括年、月、日 )。计算该日在本年中是第几天?注意闰年问题。
11.2 写一个函数 days,实现上面的计算。由主函数将年、月、日传递给 days函数,计算后将日子数传回主函数输出。
11.3 编写一个函数 print,打印一个学生的成绩数组,
该数组中有 5个学生的数据记录,每个记录包括
num,name,score[3],用主函数输入这些记录,
用 print函数输出这些记录。
11.4 在上题的基础上,编写一个函数 input,用来输入 5个学生的数据记录。
11.5 有 10个学生,每个学生的数据包括学号、姓名、
3门课的成绩,从键盘输入 10个学生数据,要求打印出 3门课总平均成绩,以及最高分的学生的数据
(包括学号、姓名,3门课成绩、平均分数 )。
11.6 编写一个函数 new,对 n个字符开辟连续的存储空间,此函数应返回一个指针 (地址 ),指向字符串开始的空间。 new(n)表示分配 n个字节的内存空间。
见图 11.29。
图 11.29
11.7 写一函数 free,将上题用 new函数占的空间释放。 free(p)表示将 p(地址 )指向的单元以后的内存段释放。
11.8 已有 a,b两个链表,每个链表中的结点包括学号、成绩。要求把两个链表合并,按学号升序排列。
11.9 有两个链表 a和 b,设结点中包含学号、姓名。
从 a链表中删去与 b链表中有相同学号的那些结点。
11.10 建立一个链表,每个结点包括:学号、姓名、
性别、年龄。输入一个年龄,如果链表中的结点所包含的年龄等于此年龄,则将此结点删去。