用户自定义数据类型 一、结构体 之前我们学过的数据类型(int、char、float等)是C语言提供的基本数据类型。实际上我们也可以根据自身的需求自定义一些数据类型。这些数据类型称为“构造数据类型”。 在实际生活中,有些数据是有内在关联的。例如一个学生有学号、姓名、性别、成绩等内在关联数据。我们可以将这些内在关联数据组合起来,构成一个新的变量类型,在这个类型中包含学号、姓名、性别、成绩等项,这样使用起来就方便多了。 C语言允许用户自己建立由不同数据组合而成的组合型数据类型,称为“结构体”(struct)。 1、声明一个结构体 声明一个结构体的一般形式为 struct 结构体名 { 成员表列; }; 例如: struct Student { int number; char name[64]; char sex; int age; int score; }; 其中,struct是C语言声明结构体类型时的关键字,不可省略。 注意: 1)结构体、共用体和枚举类型一般首字母大写,以表示与系统提供的类型名和自定义的变量名区分开。 2)花括号内是该结构体所包含的子项,称为结构体的成员(member)。成员表列(member list)又称为“域表”(field list),每一个成员是结构体的一个域。 3)成员可以是另一个结构体类型。例如: struct Date { int year; int month; int day; }; struct Student { int num; char name[64]; char sex; int age; struct Date birthday; int score; }; 练习:声明以下结构体 1)学生成绩单:成员表列包括:学生学号、学生姓名、语文成绩、数学成绩、英语成绩 2)产品生产批号:成员表列包括:产品批次、产品生产日期(年、月、日)、产地 3)货物信息:成员表列包括:货物长、宽、高、重量、颜色、始发地、目的地 答案: 1) struct Score { int number; char name[64]; int chinese_score; int math_score; int english_score; }; 2) struct Date { int year; int month; int day; }; struct Goods { int pici; struct Date shengchanriqi; char chandi[64]; }; 3) struct Freight { int length; int width; int height; int weight; char color; char src[64]; char dst[64]; }; /************************************结构体类型占内存大小*********************************************/ 首先我们先介绍一个概念:偏移量(offset)。偏移量指的是某块内存基于其一块基准地址(whence)的差,单位为字节。例如,对于一个int数组来说: int a[10]; 若我们选取数组首地址作为基准地址,则a[1]的偏移量为4,a[2]的偏移量为8…… 内存地址对齐,指的是CPU在访问内存时排列、访问数据的方式。其中又细分为基本数据对齐和结构体数据对齐两种情况。 基本类型数据对齐就是数据在内存中的偏移地址必须是一个字的倍数。由于当代计算机大多数是32位系统,则一个字的大小为32位4字节。因此变量在内存中都是按4字节对齐数据,CPU也只对4的倍数的地址进行读取数据。通常情况下编译器会帮助我们管理地址对齐。 而结构体数据对齐却是另一种情况。结构体各成员的地址对齐方式各有不同,并且按照出现的顺序依次递增: char,1字节对齐 short,2字节对齐 int,4字节对齐 float,4字节对齐 double,4字节对齐(Linux系统) 或 8字节对齐(windows系统) 结构体成员的地址对齐会优先按最小的数据类型char(1字节),当有占用较大内存的数据类型出现时,地址对齐数会增加并且不再降低。若在占用内存较小的结构体成员后紧跟占用内存较大的结构体成员,则编译器会填充若干无效字节来强制让数据对齐。最终的结构体占用内存大小需要符合以下两点: 条件1、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)。 条件2、结构体大小必须是最大占用内存成员的大小的整数倍。 例如:有以下结构体: struct A { char c; int i; char b; }; 当编译器分配内存的时候,首先分配1个字节给变量c。然后发现下一个成员为int类型,地址对齐数字增大为4.为了符合条件1和条件2,编译器会在变量c的后面填充3个无效的字节,然后再分配变量i的内存。然后发现下一个成员为char类型,此时地址对齐数字已经为4且不会减小,因此会给变量b分配“1字节+3无效字节”。此时我们发现,结构体占用内存大小为12字节但是只有6个字节的有效数据。 对于结构体A来说,若我们适当改变成员顺序,即可减少内存浪费的情况。例如,我们把结构体A改写成: struct B { char c; char b; int i; }; 结构体成员未变,只是出现顺序发生改变。这时我们可以发现该结构体占用内存大小为8个字节。相比结构体A,结构体B更加节约内存。 练习:求以下结构体占内存的大小: ①struct stu1 { char c1; int i; char c2; }; ②struct stu2 { float f; char c; double d; }; ③struct stu3 { short i; char c1; char c2; }; ④struct stu4 { short a; short b; short c; }; 答案: ①12字节②16字节③4字节④6字节 /************************************结构体类型占内存大小end******************************************/ 2、定义结构体类型变量 前面的只是声明了一个结构体类型,即建立了一个模型,它没有定义实际的内容,无具体数据,系统也没有对其分配存储单元。为了能够在程序中使用一个结构体类型的数据,需要定义一个结构体类型变量,并在其中存放数据。定义结构体变量有3种方式。 注意:要区分“声明”与“定义”的区别。声明的结构体只是模板,没有实际数据,没有存储空间,无法直接使用;而定义后的结构体就可以存储数据、有实际存储空间、可以直接使用了。 1)先声明结构体类型,再定义结构体变量 在前面我们已经声明过许多结构体。例如: struct Student { int number; char name[64]; char sex; int age; int score; }; 那么我们就可以直接使用已声明的结构体来定义结构体变量。例如: struct Student student1,student2;//定义2个结构体Student类型变量student1和student2 注意关键字struct不可省略。 这种定义方式与定义普通变量(例如int a,char c等)是相似的。变量student1和student2的变量类型是struct Student类型。 这种定义方法的一般形式是: struct 已声明过的结构体名 结构体类型变量名; 2)在声明类型的同时定义变量 这种定义方法的一般形式是: struct 结构体名 { 成员表列; }变量名表列; 例如: struct Student { int num; char name[64]; char sex; int age; float score; }student1,student2; 作用与第一种方法相同,但是在声明了结构体Student之后同时定义了2个struct Student类型的变量student1和student2。 结构体的声明和定义放在一起进行,能直接看到结构体结构,比较直观,在写中小型程序时使用此方法比较方便。但在写大型程序时,往往把所有的类型声明(变量声明、函数声明等)分别放在不同的地方以使程序结构体清楚、便于维护,因此此方法在写大型程序时不常用。 3)不指定类型名而直接定义结构体类型变量 这种定义方法的一般形式是: struct { 成员表列; }变量名表列; 这种定义方式指定了一个无名的结构体名,它没有名字,显然不能够再使用此结构体去定义其他的结构体类型变量。所以这种方式用的不多。 3、结构体变量的初始化和引用 在定义结构体变量时可以对其初始化。 示例:把一个学生的信息放在一个结构体变量中,然后输出这个学生的信息。 #include int main() { struct Student { int num; char name[64]; char sex; int score; }stu1={10101,"LiLin",'M',95};//定义结构体变量stu1并赋初值 printf("学号:%d\n姓名:%s\n性别:%c\n成绩:%d\n",stu1.num,stu1.name,stu1.sex,stu1.score); return 0; } 分析:程序中声明了一个结构体名为Student的结构体类型,在声明的同时定义了结构体变量stu1,在定义变量的同时进行初始化。在输出时,使用符号“.”来引用结构体变量中的成员。 /*****************结构体成员运算符********************/ 结构体成员运算符: 符号:. 结合性:自左至右 优先级:1级 使用方法:该运算符作用是引用一个结构体变量中的成员变量的值,引用方法为 结构体变量名.成员名 /*****************结构体成员运算符end*****************/ 如程序中的stu1.num,即取结构体变量stu1中的成员num的值。 说明: 1.在定义结构体变量时可以对它的成员进行初始化。初始化列表是用花括号括起来的一些常量,这些常量依次赋值给结构体变量中的各个成员。 2.允许对某一成员进行初始化,例如: struct Student b={.name="Zhang"}; 代表将结构体变量b中的name成员进行初始化。注意在成员变量前有成员运算符. 若进行结构体变量部分成员初始化,则其他未被指定的其他成员则会初始化为0(数值型)或'\0'(字符型)或NULL(指针型)。 3.可以引用结构体变量中成员的值,引用方式为 结构体变量名.成员名 例如: stu1.num=10101; 对于结构体变量中的成员变量可以像普通变量一样进行各种运算。如 stu2.score=stu1.score; sum = stu1.score+stu2.score; stu1.score++; 4.不能企图使用结构体变量名来达到输出结构体所有成员的值的目的。下面用法不正确: printf("%s\n",stu1);//错误,企图使用结构体变量名输出所有成员的值 只能对结构体变量中各个成员分别输出 printf("学号:%d\n姓名:%s\n性别:%c\n成绩:%d\n",stu1.num,stu1.name,stu1.sex,stu1.score);//正确 5.如果结构体成员中又包含另一个结构体,则若使用内层结构体成员变量,则需一层一层找到最低一级成员才行。例如: struct Date { int year; int month; int day; }; struct Student { int num; char name[64]; char sex; int age; struct Date birthday; int score; }student1; 我们会发现birthday也是一个结构体,若给student1变量的成员变量birthday赋值,则需要再次使用成员运算符引用这个结构体内成员。 student.birthday.year=1992; student.birthday.month=10; student.birthday.day=10; 6.同类结构体变量可以互相赋值,例如: student2=student1; 前提是两个变量必须是同一个结构体类型。 练习1:定义两个学生类型的结构体变量,分别包括: 学号(int类型) 姓名(字符串类型) 性别(char类型) 成绩(float类型) 通过键盘输入学生1的信息,之后将学生1的信息赋值给学生2,再输出学生2的信息。 答案: #include #include int main() { struct Student { int num; char name[64]; char sex; float score; }student1,student2; printf("请输入学号:"); scanf("%d",&student1.num); printf("请输入姓名:"); scanf("%s",student1.name);//注意数组名即是数组的首地址,不要再加& getchar(); printf("请输入性别(M/F):"); scanf("%c",&student1.sex); printf("请输入成绩:"); scanf("%f",&student1.score); student2=student1; printf("学号:%d\n姓名:%s\n性别:%c\n成绩:%f\n",student2.num,student2.name,student2.sex,student2.score); return 0; } 练习2:在练习1的基础上,由键盘输入学生2的信息,比较两个学生的成绩高低,如果学生1成绩较高,则输出学生1的信息;如果学生2的成绩较高,则输出学生2的信息;如果两学生成绩相等,则将两个学生的信息都输出。 #include #include int main() { struct Student { int num; char name[64]; char sex; float score; }student1,student2; //输入学生1信息 printf("请输入学号:"); scanf("%d",&student1.num); printf("请输入姓名:"); scanf("%s",student1.name); getchar(); printf("请输入性别(M/F):"); scanf("%c",&student1.sex); printf("请输入成绩:"); scanf("%f",&student1.score); //输入学生2信息 printf("请输入学号:"); scanf("%d",&student2.num); printf("请输入姓名:"); scanf("%s",student2.name); getchar(); printf("请输入性别(M/F):"); scanf("%c",&student2.sex); printf("请输入成绩:"); scanf("%f",&student2.score); printf("成绩更高的学生是:\n"); if(student1.score>student2.score) printf("学号:%d\n姓名:%s\n性别:%c\n成绩:%f\n",student1.num,student1.name,student1.sex,student1.score); else if(student1.score #include struct Person { char name[20];//候选人姓名 int count;//候选人得票数 }leader[3]={"Li",0,"Zhang",0,"Sun",0};//定义结构体数组并初始化 int main() { int i,j; char leader_name[20]; for(i=1;i<=10;i++) { printf("请输入Li/Zhang/Sun三人中一人名字:"); scanf("%s",leader_name); for(j=0;j<3;j++) { if(strcmp(leader_name,leader[j].name)==0) leader[j].count++; } } printf("\n选票结果是:\n"); for(i=0;i<3;i++) printf("%s:%d\n",leader[i].name,leader[i].count); return 0; } 练习:有5个学生的信息如下: 学号 姓名 成绩 10101 Sun 96 10103 Wang 98.5 10110 Li 100 10108 Ling 83.5 10106 Zhang 88 编写程序,分别按照学号顺序和成绩顺序输出学生的信息。 答案: #include #define LEN 5 struct Student { int num; char name[8]; float score; }; void print_stu(struct Student stu[]) { int i; printf("学号\t\t姓名\t\t成绩\n"); for(i=0;istu[j+1].num) { tmp = stu[j]; stu[j]=stu[j+1]; stu[j+1]=tmp; } } } printf("按学号排列如下:\n"); print_stu(stu); //按成绩排列 for(i=0;i #include int main() { struct Student { int num; char name[64]; char sex; float score; }; struct Student stu1; struct Student *pt; pt=&stu1;//指针pt指向结构体变量stu1 stu1.num=10101; strcpy(stu1.name,"Li");//注意给字符串类型赋值不能使用赋值运算符=,而要使用strcpy()函数 stu1.sex='M'; stu1.score=90; printf("%d\t%s\t%c\t%f\n",stu1.num,stu1.name,stu1.sex,stu1.score);//使用直接引用方式访问结构体成员变量 printf("%d\t%s\t%c\t%f\n",(*pt).num,(*pt).name,(*pt).sex,(*pt).score);//使用指针方式访问结构体成员变量 return 0; } 示例程序的两次输出结果是一样的,也就是说指针pt所指向的结构体变量就是stu1。 在第二个printf()函数中,通过指针访问结构体变量的方法是 (*pt).num; 实际上,C语言提供给我们一种更简便的写法: pt->num; 其中->是间接访问结构体成员运算符,->符号左边必须是一个指向结构体变量的指针。 如果一个指针pt已经指向了一个结构体变量stu1,则以下三种写法是等价的: 1.stu.成员名 2.(*pt).成员名 3.pt->成员名 示例程序的第二个printf()函数可以写成: printf("%d\t%s\t%c\t%f\n",pt->num,pt->name,pt->sex,pt->score);//使用指针方式访问结构体成员变量 2)指向结构体数组的指针 同样,我们也可以使用结构体指针来访问结构体数组。 示例: #include struct Student { int num; char name[64]; char sex; float score; }; struct Student stu[3]={{10101,"Li",'M',95},{10103,"Liu",'M',88},{10107,"Zhang",'F',99}};//定义结构体数组包含三个元素 int main() { struct Student *pt;//定义指向stu[3]的结构体指针 for(pt=stu;ptnum,pt->name,pt->sex,pt->score); } return 0; } 在示例程序中,for()循环的第一句 pt=stu; 指定了指针pt指向结构体数组的首地址。每次pt++都将指针移动到下一个成员的首地址。 请注意 (++pt)->num;//先让pt移动到下一个数组元素,然后访问该元素的成员变量num 与 (pt++)->num;//先访问该元素的成员变量num,然后让pt移动到下一个数组元素 的不同。 注意示例程序中,pt的类型是结构体变量类型。所以 pt=stu[1].name;//试图让指针指向stu[1]的成员变量name的地址 是不对的,在编译时编译器会给出warning。 练习1:定义一个日期结构体变量(包括年、月、日),判断该年是不是闰年。要求不使用指针和使用指针两种方法。 练习2:在上一个练习的基础上,定义一个日期结构体变量(包括年、月、日),从键盘输入一个时间(年、月、日),判断该日期是该年的第几天。注意闰年问题。要求不使用指针和使用指针两种方法。 答案: //练习1答案已在练习2答案中,不再重复写明 1)不使用指针 #include struct Date { int year; int month; int day; int isLeap; }; //函数IsLeap,判定一个年份是否是闰年。是闰年返回1,否则返回0 int IsLeap(int input) { if(input%4==0) { if(input%100!=0) { return 1; } else if(input%400==0) { return 1; } else { return 0; } } else { return 0; } } int main() { struct Date date; int count; printf("请输入年份:"); scanf("%d",&date.year); printf("请输入月份:"); scanf("%d",&date.month); printf("请输入日期:"); scanf("%d",&date.day); date.isLeap=IsLeap(date.year); switch(date.month) { case 1:count=0;break; case 2:count=31;break; case 3:count=59;break; case 4:count=90;break; case 5:count=120;break; case 6:count=151;break; case 7:count=181;break; case 8:count=212;break; case 9:count=243;break; case 10:count=273;break; case 11:count=304;break; case 12:count=334;break; default:printf("输入错误!\n");exit(0); } count += date.day; if(IsLeap(date.year)==1 && date.month>=3) count += 1; printf("该天是第%d天\n",count); return 0; } 2)使用指针 #include #include struct Date { int year; int month; int day; int isLeap; }; //函数IsLeap,判定一个年份是否是闰年。是闰年返回1,否则返回0 int IsLeap(int input) { if(input%4==0) { if(input%100!=0) { return 1; } else if(input%400==0) { return 1; } else { return 0; } } else { return 0; } } int main() { struct Date *date; date = (struct Date*)malloc(sizeof(struct Date)); int count; printf("请输入年份:"); scanf("%d",&date->year); printf("请输入月份:"); scanf("%d",&date->month); printf("请输入日期:"); scanf("%d",&date->day); date->isLeap=IsLeap(date->year); switch(date->month) { case 1:count=0;break; case 2:count=31;break; case 3:count=59;break; case 4:count=90;break; case 5:count=120;break; case 6:count=151;break; case 7:count=181;break; case 8:count=212;break; case 9:count=243;break; case 10:count=273;break; case 11:count=304;break; case 12:count=334;break; default:printf("输入错误!\n");exit(0); } count += date->day; if(IsLeap(date->year)==1 && date->month>=3) count += 1; printf("该天是第%d天\n",count); free(date); return 0; } 6、用结构体变量和结构体变量指针作为函数参数 将一个结构体变量的值传递给一个函数,有3种方法: 1.用结构体变量的成员做函数参数。如stu.num,stu.name等做函数的实参。用法和普通变量做实参是一样的,属于“值传递”方式。应当注意实参与形参的类型要一致。 2.用结构变量做实参。这种方式也属于“值传递”的方式,将结构体变量整体传递给函数作为实参。由于这种方式在空间开销上较大,执行效率低,而且传递后无法返回主调函数,因此这种方式基本不用。 3.用指向结构体变量(或结构体数组)的指针做实参,将结构体变量(或数组)以“地址传递”的方式传给形参。 示例:有n个结构体变量,内含学生学号、姓名和3门课的成绩。要求输出平均成绩最高的学生的信息(学号、姓名、3门课成绩及平均成绩) #include #define N 3 struct Student { int num; char name[20]; float score[3]; float avg; }; void input(struct Student stu[])//input函数,输入学生信息 { int i; printf("请输入个学生的信息:学号、姓名、3门课的成绩:\n"); for(i=0;istu[m].avg) m=i; } return stu[m]; } void print(struct Student stud) { printf("\n成绩最高的学生是:\n"); printf("学号:%d\n姓名:%s\n成绩:%f\t%f\t%f\n平均成绩:%f\n",stud.num,stud.name,stud.score[0],stud.score[1],stud.score[2],stud.avg); } int main() { struct Student stu[N],*p=stu; input(p); print(max(p)); return 0; } 在结构体类型struct Student中包括num(学号)、name(姓名)、数组score(三门课成绩)和avg(平均成绩)。其中avg不是由用户输入的而是由程序计算出来的。在主函数中定义了一个指针变量p指向结构体数组首个元素stu[0]地址。在调用input函数时,使用指针p作为函数实参进行地址传递。同样,在主函数中调用print函数,而print函数需要一个struct Student类型的参数。print函数内又调用了max函数,max函数的返回值是struct Student类型,其返回值作为print函数的实参。 练习:有5个成绩的信息需要录入(学号、姓名、性别、成绩)。编写2个函数,input函数用来录入这些信息,output函数用来输出这些信息。最后写一个主函数测试。 答案: #include #define N 5 struct Student { int num; char name[20]; char sex; int score; }; void input(struct Student stud[]) { int i; for(i=0;isun) 等。比较规则是按其在初始化时指定的整数来比较。 示例:口袋中有红、黄、蓝、白、黑5色小球若干个,每次从口袋拿3个球,问得到3种不同颜色的球的可能取法。 #include #include int main() { enum Color{red,yellow,blue,white,black}; enum Color i,j,k,pri; int n,Leap; 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("%d:\n",n); for(Leap=1;Leap<=3;Leap++) { switch(Leap) { case 1:pri=i;break; case 2:pri=j;break; case 3:pri=k;break; default:break; } switch(pri) { case red:printf("%s\n","red");break; case yellow:printf("%s\n","yellow");break; case blue:printf("%s\n","blue");break; case white:printf("%s\n","white");break; case black:printf("%s\n","black");break; default:break; } } } } } } printf("\ntotal:%d\n",n); return 0; } 四、用typedef声明新类型名 除了可以直接使用C提供的标准类型名(例如int,char,float等),我们还可以用typedef来指定新的类型名来替代已有的类型名。 有时,对于结构体等构造数据类型来说,有时会出现难以理解、容易写错等情况。C语言中允许设计者使用一个简单的名字来代替复杂的类型形式。例如 typedef struct Datenum { int year; int month; int day; }Date; 以上声明了一个新类型名Date,代表上面的一个结构体类型。然后可以使用新的类型名Date去定义变量,如: Date birthday; 它与 struct Datenum birthday; 等价。 同样也可用于定义结构体指针 typedef struct Datenum { int year; int month; int day; }*Point; 则 Point p; 等价于 struct Datenum *p; typedef声明数组、指针、结构体、共用体、枚举类型等,使得编程更加方便。 注意:typedef与#define虽然在作用上相似,但二者本质不同。#define是在预处理阶段进行宏代替,而typedef则是在编译阶段处理的。 typedef在数据结构里会大量的使用,请同学们在学习时注意其用法。