1 定义和使用结构体变量
1.1 自定义建立结构体类型
好处:反映出成员之间的内在联系。
在程序中建立所需要结构体类型。例如:
指定了一个新的结构体类型struct student。
struct是声明结构体类型时所必须使用的关键字,不能省。
声明一个结构体类型的形式为:
struct 结构体名
{成员表列};
结构体类型的名字是由一个关键字struct和结构体名二者组合而成的(如struct student)。结构体名由用户指定的,由称“结构体标记”。
大括号内是该结构体的子项,成为结构体的成员。对各成员都进行类型声明,即:
类型名 成员名;
成员表列也称“域表”。成员名命名规则与变量名相同。
成员可以是另一个结构体变量。例如:
1.2 定义结构体类型变量
为了能在程序中使用结构体类型的数据,应当定义结构体类型的变量。可以采用以下3种方法定义结构体类型变量。
1 先声明结构体类型,再定义该类型的变量,例如:
与 int a,b;类似。
2 在声明类型的同时定义变量
一般形式为:
struct 结构体名
{
成员表列
}变量名表列;
适合小程序使用。
3 不指定类型名而直接定义结构体类型变量,很少用
一般形式:
struct
{
成员表列
}变量名表列;
小结:
(1)分清结构体类型与结构体变量两个概念。
(2)结构体类型中的成员名可以与程序中的变量名相同,但二者不代表统一对象,易混淆。
1.3 结构体变量的初始化和引用
在定义结构体变量是,可以进行赋值,即初始化。然后可以引用这个变量,输出它的成员的值。
例题:把一个学生的信息放在一个结构体变量中,然后输出这个学生的信息。
思路:定义一个结构体类型,包含有关学生的信息成员,然后定结构体变量,赋初值(即一个学生的信息),最后输出该结构体变量的各成员(即该学生的信息)。
编写程序和运行结果:
分析:
(1)指定了一个结构体名为student的结构体类型,有3个成员。
(2)在声明类型的同时定义了结构体变量stu1,这个变量具有struct student类型所规定的结构。
(3)在定义变量同时,初始化变量。将1111,“zhang san”,'M’按顺序赋给stu1变量中的成员num,name,sex。
(4)最后用printf函数输出变量中个成员的值。
引用结构体变量应遵守以下规则:
(1)引用结构体变量中成员的值,引用方式为:
结构体变量名.成员名
例如:stu1.num表示变量stu1中的num成员。就是stu1的num(学号)。
还可以对变量成员赋值,例如:
“.”是成员运算符,它在所有的运算符中优先级最高,因此可以把stu1.num作为一个整体来看待。
注意:只能对结构体变量中的各个成员分别进行输入和输出。
(2)如果成员本身又属一个结构体类型,则要用若干个成员运算符,一级一级地找到最低的一级的成员。只能对最低级的成员进行赋值或存取以及运算。如果在结构体中包含另一个结构体struct date类型的变量birthday, 则引用成员的方式为:
stu1.birthday.year (结构变量stu1中的成员birthday中的成员year)
(3)对结构体变量的成员可以像普通变量一样进行各种运算(根据其类型决定可以进行的运算)。例如:
stu2.score=stu1.score;(值运算)
sum=stu1.score+stu2.score(加法运算)
stu1.age++;(自加运算)
由于“.”运算符的优先级最高, 因此stu1.age++是对stu1.age进行自加运算,而不是先对age进行自加运算。
(4)同类的结构体变量可以互相赋值,如:
stu1=stu2; //假设stu1和stu2已定义为同类型的结构变量
(5)可以引用结构体变量成员的地址,也可以引用结构体变量的地址。例如:
scanf("%d",&stu1.num);//输出stu1.num的值
printf("%o",&stu1);//输出结构变量stu1的首地址
例题:
输人两个学生的学号姓名和成绩,输出成绩较高的学生的学号、姓名和成绩。
解题思路:
(1)定义两个结构相同的结构体变量student l和student 2;
(2)分别输入两个学生的学号姓名和成绩;
(3)比较两个学生的成绩,如果学生1的成绩高于学生2的成绩,就输出学生1的全部信息,如果学生2的成绩高于学生1的成绩,就输出学生2的全部信息。如果二者相等,输出两个学生的全部信息。
编写程序:
分析:(1) stu1和stu2是struct student类型的变量。在三个成员中分别存放学号、姓名和成绩。
(2) 用scanf函数输入结构体变量时, 必须分别输人它们的成员的值,注意在scanf函数中在成员stu1.num和stu1.score的前面都有地址符&, 而在stu1.name前面没有&, 这是因为name是数组名,本身就代表地址,故不能画蛇添足地再加一个&。
2 结构体数组
说明:
定义结构体数组一般形式是:
(1)struct结构体名
{成员表列} 数组名[数组长度];
例如:
(2) 也可以先声明一个结构体类型(如struct student) , 然后再用此类型定义结构结构体数组:
结构体类型 数组名[数组长度];
如:
对结构体数组初始化的形式是在定义数组的后面加上
={初值表列};
如:
例题:
有3个候选人,每个选民只能投票选一人,要求编写一个统计选票的程序,先后输人被选人的名字,最后输出各人得票结果。
思路:
需要设一个结构体数组,数组中包含3个元素,每个元素中的信息应包括候选人的姓名(字符型)和得票数(整型)。然后将输入的姓名与务数组元素中的“姓名”成员比较,如果相同,就给这个元素中的“得票数”成员的值加1,最后输出所有元素的信息。
编写程序和运行结果:
分析:
(1)想了解更多有关字符函数的函数点击这里
(2)初始化结构体变量时,必须按照成员顺序赋值。如:
leader[3]={“li”,0,“zhang”,0,“fun”,0};
不能写成
leader[3]={0,“li”,0,“zhang”,0,“fun”};
原因是:
先定义name成员,后定义count成员。
(3)定义了一个结构体数组leader,它有3个元素,每一个元素包含两成员name(姓名)和count(票数)。
统计
(4)在主函数中定义字符数组leader_name, 用它存放被投的候选人的姓名。在每次循环中输入一个被投的候选人姓名,然后把它与结构体数组中3个候选人姓名相比, 看它和哪一个候选人的名字相同。注意leader_name是和leader数组第j个元素的name成员相比。若j为某一值时, 输入的姓名与leader[j] .name相等, 就执行“leader[] ·count++”, 由于成员运算符“.”优先于自增运算符“++”, 因此它相当于(leader[j].count) ++, 使leader[j] 成员count的值加1。在输人和统计结束之后, 将3人的名字和得票数输出。
3 结构体指针
所谓结构体指针就是指向结构体数据的指针,一个结构体变量的起始地址就是这个结构体变量的指针。但是,针变量的基类型必须与结构体变量的类型相同。
例题:通过指向结构体变量的指针变量输出结构体变量中成员的信息。
解题思路:在以上的基础上,本题要解决两个问题:(1)怎样对结构体变量成员的赋值;(2)怎样通过指向结构体变量的指针访问结构体变量中成员。
编写程序和运行结果:
分析:
(1)在主函数中声明了struct student类型,然后定义一个struct student类型的变量stu1,又定义一个指针变量p-它指向一个struct student类型的数据。
(2)将结构体变量stu1的起始地址赋给指针变量p,也就是使p指向stu1 ,然后对stu1的各成员赋值。
(3)第一个printf函数是通过结构构体变量名stu1访问它的成员,输出stu1l的各个成员的值。第二个printf是通过指向结构体变量的指针变量访间它的成员, 使用的是( * p) .num这样的形式。
( * p)表示p指向的结构体变量,( * p) .num是p指向的结构体变量中的成员num。
注意 * p两侧的括号不可省,因为成员运算符“.“优先于“ * " 运算符,。为了使用方便和使之直观, C语言元许把p->num代替( * p).num, 它表示p所指向的结构体变量中的num成员。同样,p一>name等价于( * p) .name。
->称为指向运算。如果p指向一个结构体变量,以下3种形式等价:
①结构体变量.成员名
②( * p)成员名
③p->成员名
指向结构体变量的指针变量,也可以用来指向结构体数组元素。
4 用结构体变量和结构体变量的指针作函数参数
将一个结构体变量传递给留一个函数,有3个办法:
(1)用结构体变量的成员作参数。例如, 用stu[1] .num或stu[2] .name作函数实参, 将实参值传给形参。
(2)用结构体变量作实参。用结构体变量作实参时,采取的也是“值传递”的方式,将结构体变量所占的内存单元的内容全部顺序传递给形参,形参也必须是同类型的结构体变量。
(3)用指向结构体变量(或数组)的指针作实参,将结构体变量(或数组)的地址传给形参。
例题:
有3个结构体变量stu, 内含学生学号、姓名和3门课程的成绩。要求输出平均成绩最高的学生的信息(包括学号、姓名、3门课程成绩和平均成绩)。
解题思路:按照功能函数化的思想,分别用3个函数来实现不同的功能:
(1) 用input函数来输人数据和求各学生平均成绩。
(2) 用max函数来找平均成绩最高的学生。
(3) 用print函数来输出成绩最高学生的信息。在主函数中先后调用这3个函数,用指向结构体变量的指针作实参。最后得到结果。
编写程序:
#include <stdio.h>
#define N 3//学生个数为3
struct student//定义结构体类型
{
int num;//学号
char name[20];//姓名
float score[3];//用于存储3门课程的成绩
float aver;//平均分
};
int main(){
void input(struct student stu[]);//声明input函数
struct student max(struct student stu[]);//声明max函数
void print(struct student stu);//声明print函数
struct student stu[N];//定义结构体数组
struct student *p;//定义结构体变量指针
p=stu;//stu数组的首元素地址存储到结构体变量指针p,即p指向数组stu首元素地址
input(p);//调用input函数,p为实参
print(max(p));//先调用max函数,实参为p,max函数返回stu[m],是一个结构体数组的元素,即平均分最高的学生信息
//print函数的实参就是stu[m]
return 0;
}
void input(struct student stu[]){//定义input函数,结构体数组名为形参
printf("input students NO,name,score:\n");
for(int i=0;i<N;i++)//输入3名学生的信息
{scanf("%d%s%f%f%f",&stu[i].num,stu[i].name,&stu[i].score[0],&stu[i].score[1],&stu[i].score[2]);
stu[i].aver=(stu[i].score[0]+stu[i].score[1]+stu[i].score[2])/3.00;//算出平均分
}
}
struct student max(struct student stu[]){//定义max函数,结构体数组名为形参
int k;//用k存放平均分最高的学生在数组中的序号
for(int i=0;i<N;i++)//找出平均分成绩最高的学生在数组中的序号
if(stu[i].aver>stu[k].aver) k=i;
return stu[k];//返回该学生的结构元素
}
void print(struct student stu1){//定义print函数,形参是结构体变量中各成员的值
printf("\nthe highest aver student info:");
printf("NO:%d\nname:%s\nscore: %5.2f %5.2f %5.2f\naver:%5.2f\n",stu1.num,stu1.name,stu1.score[0],stu1.score[1],stu1.score[2],stu1.aver);
}
运行结果:
程序分析:
(1) 结构体类型struct student中包括num(学号) 、name(姓名) 、数组score(三门课成绩) 和aver(平均成绩) 。在输入数据时只输人学号、姓名和三门课成绩, 未给aver赋值。aver的值是在input函数中计算出来的。
(2) 在主函数中定义了结构体struct student类型的数组stu和指向struct student类型数据的指针变量p, 使p指向stu数组的首元素stu[0] 。在调用input函数时, 用指针变量p作为函数实参, input雨数的形参是struct student类型的数组stu。
(3)在调用input数时, 将主函数中的stu数组的首元素的地址传给形参数组stu。使形参数组stu与主角数中的stu数组具有相同的地址, 因此在input函数中向形参数组stu输人数据就等于向主数中的stu数组输人数据。
在用scanf承数输人数据后, 立即计算出该学生的平均成绩:stu[i] .aver代表序号为i的学生的平均成绩。请注意for循环体的范围, input函数无返回值, 它的作用是给stu数组各元素赋予确定的值。
(4) 在主函数中调用print函数, 实参是max( p) 。其调用过程是先调用max函数(以p为实参) , 得到max§ 的值, 然后用它调用print数。
现在先分析调用max函数的过程:指针变量p主函数中的stu数组的首元素的地址传给形参数组stu, 使形参数组stu与主函效中的stu数组具有相同的地址。在max函数中对形参数组的操作就是对主函数中的stu数组的操作, 在max函数中, 将每各人平均成绩与当前的最高平均成绩”比较, 将平均成绩最高的学生在数组stu中的序号存放在变量m中, 通过return语句将stu[m] 的值返回主函数。
请注意:stu[m] 是一个结构体数组的元素。max函数的类型为struct student类型。
(5) 用max( p) 的值(是结构体数组的元素) 作为实参调用print函数。
print函数的形参stu是struet student类型的变量。在调用时进行虚实结合, 把stu[m] 的值(是结构体元素) 传递给形参stud, 这时传递的不是地址, 而是结构体变量中的信息。
在print函数中输出结构体变量中各成员的值。
(6)以上3个函数的调用,情况各不相同。
调用input函数时, 实参是指针变量p, 形参是结构体数组名,传递的是结构体元素的地址,函数无返回值。
调用max函数时, 实参是指针变量p, 形参是结构体数组名, 传递的是结构体元素的地址,函数的返回值是结构体类型数据。
调用print函数时,实参是结构体变量(结构体数组元素) , 形参是结构体变量,传递的是结构体变量中各成员的值,函数无返回值。
5 用指针处理链表
结构体变量是一个重要的用途是和指针相结合,构造线性链表。
5.1 什么是线性链表
线性链表是动态地进行存储分配的一种数据结构。它没有固定的大小,根据需要随时开辟存储单元,在用完后可以随时释放存储单元,下图表示最简单的一种链表(单向链表)的结构。
链表有一个"头指针”变量, 图中以head表示, 它存放一个地址, 该地址指向链表中的个元素。
链表中的各元素称为“结点”,每个结点都应包括两个部分:用户需要用的实际数和下一个结点的地址, head指向第一个结点。 第一个结点中有一个指针变量指向第二个结点……直到最后一个结点,该结点不再指向其他结点,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”) , 链表到此结束。要找某一结点、必须先找到上一个结点,根据它提供的下一结点地址才能找到下一个结点,如果不提供“头指针”(head) , 则整个链表都无法访问。
例如:
其中成员num和score用来存放结点中的有用数据(用户需要用到的数据) 。next是指针类型的成员。
为了构造链表,应当把next定义为指向struct student类型数据的指针变量, 这时, next既是struct student类型中的一个成员, 又指向struct student类型的数据, 每一个结点都属于struct student类型, next存放下一结点的地址。
可以不必知道各结点的具体地址,只要保证将下一个结点的地址放到前一结点的成员next中即可。
5.2 建立简单的静态链表
例题:建立简单的静态链表,它由3个学生数据的结点组成。输出各结点中的数据。
编写程序:
#include <stdio.h>
#define NULL 0//定义NULL的值为0
struct student//声明结构体类型为struct student
{
int num;
float score;
struct student *next;//next是指针变量,也是struct student类型中的一个成员
};
int main(){
struct student a,b,c;//定义a,b,c为结构体变量作为链表的节点
struct student *head,*p;//定义结构体变量的指针
a.num=10101;//对结点a的num赋值
a.score=89.5;//对结点a的score赋值
b.num=10102;
b.score=90;
c.num=10103;
c.score=95;
head=&a;//head指向a,即将结点a的起始地址赋给头指针head
a.next=&b;//next存放下一结点的地址,即next成员存放b结点地址
b.next=&c;//next存放下一结点的地址,即next成员存放c结点地址
c.next=NULL;//存放c结点地址为0
p=head;//使p指向a节点
do
{
printf("%d %5.2f\n",(*p).num,(*p).score);//输出p指向的结点数据
//等价于printf("%d %5.2f\n",p->num,p->score);
p=(*p).next;//使p指向下一个结点
//等价于p=p->next;
}while(p!=NULL);//输出完c结点后p的值为NULL,循环终止
return 0;
}
运行结果:
分析:
(1)开始时使head指向a结点, a.next指向b结点, b.next指向c结点, 这就构成链表关系。“c.next=NULL”的作用是使c.next不指向任何有用的存储单元,这就形成了一个简单的链表。
(2)在输出链表时要借助p,先使p指向a结点,然后输出a结点中的数据,“p=(*p).next”是为输出下一个结点作准备。(*p).next是p当前指向的对象中的成员next, 当前p的值是b结点的地址, 因此执行“p=(*p).next"后p就指向b结点,所以在下一次循环时输出的是b结点中的数据。
(3)所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。
5.3 建立动态链表
建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输人各结点数据,并建立起前后相连的关系。
例题:
建立一个有两名学生学号和成绩数据的单向动态链表。
思路:
(1)定义结构体变量,其成员包括学号、成绩和指针变量。
(2)动态地开辟一个新单元(动态开辟内存单元用malloc函数) 。使指针变量p和head指向此结点。
(3)向此结点输人数据。
(4)再开辟第2个新结点,并使指针变量p指向此结点。
(5) 使第2个结点中的指针变量的值为NULL, 即不指向任何对象, 链表到此为止。
(6)输出两个结点中的数据。
编写程序:
#include <stdio.h>
#include <malloc.h>//用malloc函数开辟新单元时需要此头文件
#define LEN sizeof(struct student)//LEN代表struct student类型数据的字节数
struct student//声明结构体类型为struct student
{
int num;
float score;
struct student *next;//next是指针变量,也是struct student类型中的一个成员
};
int main(){
struct student *head,*p;//定义结构体变量的指针
//建立链表
head=p=(struct student *) malloc(LEN);//开辟一个新单元,让p和head指向它
scanf("%d %f",&(*p).num,&(*p).score);//输入第一个结点的数据
//等价于scanf("%d %f",&p->num,&p->score);
p=(struct student *) malloc(LEN);//开辟一个新单元,让p指向它
scanf("%d %f",&p->num,&p->score);//输入第2个结点的数据
//等价于scanf("%d %f",&(*p).num,&(*p).score);
(*head).next=p;//让第1个结点中的next成员指向第2个结点
//等价于head->next=p;
(*p).next=NULL;//让第2个结点中的next成员指向第2个结点
//等价于p->next=NULL;
//输出两个结点中的数据
p=head;//让p指向第1个结点
printf("\n结点1:%d,%5.2f\n",(*p).num,(*p).score);//输出第1个结点中的数据
//等价于printf("\n结点1:%d,%5.2f\n",p->num,p->score);
p=(*p).next;//让p指向第2个结点
//等价于p=p->next;
printf("\n结点2:%d,%5.2f\n",(*p).num,(*p).score);//输出第2个结点中的数据
//等价于printf("\n结点2:%d,%5.2f\n",p->num,p->score);
return 0;
}
运行结果:
输入
10101 88
10102 99
分析:
(1)在C语言中, 开辟内存单元需要用malloc函数。如果有malloc(10) , 表示要向系统申请分配一个占10字节的内存空间,此函数的返回值是该段内存单元的起始地址。
malloc(LEN) 的作用是开辟一个长度为struct student类型数据长度的内存空间。由于malloc丽数返回的地址(指针)是(void*)类型的,即不指向一个特定的类型的对象,因此,对其返回值进行强制类型转换,即
(struct student *) malloc(LEN);
使它能指向struct student类型的数据。
(2) 由于p指向新结点,用scanf函数输人数据时, p->num就是此结点(是结构体变量) 中的num成员, p->score是此结点中的score成员。
(3)再开辟一个新结点(即第2个结点),并把此结点的起始地址赋给p,即p此时指向了第2个结点,用scanf函数输人第2个结点中的数据。
(4) “head->next=p;”的作用是把p的值(即第2个结点的起始地址) 赋给第1个结点中的next指针, 这样就使第1个结点的next指针指向第2个结点。
(5) “p->next=NULL;”的作用是使第2个结点中的next指针的值为NULL,在stdio.h头文件中已定义NULL为0, 这就使next指向地址为0的单元, 系统不将0地址分配给任何对象, 因此, 第2个结点中的next指针不指向任何对象, 故第2个结点就是链表中最后的结点。
(6) 程序最后4行的作用是输出链表, “p=head”使p指向第1个结点, 输出第1个结点中成员的值, 然后“p=p->next”把p当前所指向的结点中的next指针的值赋给p,而此时第1个结点中的next指针是指向第2个结点的, 也就是使p指向第2个结点, 因此最后的printf语句中的p->num就是第2个结点的num成员。
6 提高部分
6.1 共有体类型
使几个不同的变量共占同一段内存的结构,称为为“ 共有体”类型的结构。
定义共用类型变量的一般形式为:
union 共有体名
{
成员表列
}变量表列;
例如:
共用体变量所占的内存长度等于最长的成员的长度。例如,上面定义的“共用体”变量a、b、各占4字节(因为一个实型变量占4字节),而不是各占2+1+4=9字节不能引月用共用体变量,只能引用共用体变量中的成员。
例如,前面定义了a、b、c为共用体变量,下面的引用方式是正确的:
6.2 枚举类型
枚举是将变量的值一一列举出来,变量的值只限于列举出来的值得范围内。
**声明枚举类型用enum开头。**例如:
声明了一个枚举类型enum weekday。
定义一个枚举类型的变量,如:
等价于
workday,weeek_end是枚举变量,他们的值只能是sun到sat之一。如:
sun,sat,mon,tue,wed,thu,fri,sat称为枚举元素或枚举变量,是用户定义的标识符。
例题:
口袋中有红、黄、蓝、白、黑5种颜色的球若干个。每次从口袋中先后取出3个球,问得到3种不同色的球的可能排列,输出每种排列的情况。
解题思路:
球只能是5种色之一,而且要判断各球是否同色,应该用枚举类型变量处理。
设取出的球为i、j、k。根据题意, i、j、k分别是5种色球之一, 并要求i!=j!=k。可以用穷举法,即把每一种排列都试一下,看哪一组符合条件。
7 小结
1.C语言中的数据类型包括两类:一类是系统已经定义好的标准数据类型(如int,char, float, double等) , 可以直接用它们去定义变量; 另一类是用户根据需要在一定的框架范围内自己设计的类型,先要向系统作出声明,然后才能用它们定义变量。其中最常用的有结构体类型,此外还有共用体类型和枚举类型。
2. 结构体类型是把若干个数据组成一个整体,这些数据可以是不同类型的。声明结构体类型的一般形式是:
strcut 结构体名
{成员列表}
注意:
struct是声明结构体类型一要要写的关键字,结构体类型名应该是“struct 结构体名”。
3.同类结构体变量可以互相赋值,但不能企图用结构体变量名对结构体变量进行整体输人和输出。可以对结构体变量中的成员进行赋值,比较、输人和输出等操作。
引用结构体变量中的成员的方式有:(1) 结构体变量.成员名, 如student.age。
(2) (*指针变量) .成员名 如:
(*p) .age.其中p指向结构体变量。
(3) p->成员名, 如:
p->age, 其中p指向结构体变量。
4.结构体变量的指针就是结构体变量的起始地址,注意它的基类型是结构体类型的数据。
5.把结构体变量和指向结构体变量的指针结合起来,可以建立动态数据结构(如链表)。
开辟动态内存空间用malloc函数, 函数的返回值是所开辟的空间的起始地址。利用所开辟的空间作为链表的一个结点,这个结点是一个结构体变量,其成员由两部分组成:一部分是实际的有用数据,另一部分是一个指向结构体类数据的指针变量,利用它指向下一个结点。
6.共用体与结构体不同,其各成员不是分别占独立的存储单元,而是共享同一段存储空间。
7.枚举类型是把可能的值全部一一列出,枚举变量的值只能是其中之一。