第9章用户自己建立数据类型
书上典型例题总结:
如何去定义一个结构体数组: #include<stdio.h> #include<string.h> 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++) { scanf("%s",leader_name); for(j=0;i<3;i++) if(strcmp(leader_name,leader[j].name)==0)leader[j].count++; } printf("\nResult:\n"); for(i=0;i<3;i++) printf("%5s:%d\n",leader[i].name,leader[i].count); return 0; } 指向结构体变量的指针: #include<stdio.h> #include<string.h> int main() { struct Student { long num; char name[20]; char sex; float score; }; struct Student_stu_1; struct Student*p; p=stu&_1; //p指向stu_1 stu_1.num=10101; strcpy(stu_1.name,"Li Lin"); stu_1.sex='M'; stu_1.score=89.5; printf("No.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n"); stu_1.num,stu_1.name,stu_1.sex,stu_1.score); printf("No.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n"); (*p).num,(*p).name,(*p).sex,(*p).score); //(*p)表示p指向的结构体变量,(*p).num表示p所指向的结构体变量中的成员num。注意*两侧的括号不可省,因为成员运算符“.”优先于“*”运算符 return 0; } 如果p指向一个结构体变量stu,以下三种用法等价 (1)stu.成员名(如stu.num); (2)(*p).成员名(如(*p).num); (3)p->成员名(如p->num)。 306页例9.7: 学生成绩管理系统 #include<stdio.h> #include<stdlib.h> #include<stdlib.h> #include<string.h> struct Student{ //每个学生对应一个结构体 char ID[20];//学号 char Name[10];//姓名 float Mark1;//语文成绩 float Mark2;//数学成绩 //四个变量 float Mark3;//英语成绩 float Mark4;//计算机成绩 float All; //总分 float Average;//平均成绩 }students[1000]; int num=0; //计数器 void Copy(struct Student *arr,int i,int j) { strcpy(arr[i].ID,arr[j].ID); //strcpy()函数的简介: 是将一个字符串复制到另一块空间地址中 的函数,‘\0’是停止拷贝的终止条件, 也复制到目标空间。下面是库中的strcpy()函数声明: strcpy(arr[i].Name,arr[j].Name); arr[i].Mark1 = arr[j].Mark1; arr[i].Mark2 = arr[j].Mark2; arr[i].Mark3 = arr[j].Mark3; arr[i].Mark4 = arr[j].Mark4; arr[i].All = arr[j].All; arr[i].Average = arr[j].Average; } int Student_SearchByName(char name[])//通过姓名来检索学生 { int i; for (i=0;i<num;i++) { if (strcmp(students[i].Name,name)==0) //通过strcmp函数来对比学生姓名,找到返回在数组的位置 { return i; } } return -1; //未找到返回 -1 } int Student_SearchByIndex(char id[])//通过学号来检索学生信息 { int i; for (i=0;i<num;i++) { if (strcmp(students[i].ID,id)==0) //通过strcmp函数来对比学生id,找到返回位置 { return i; } } return -1; //未找到返回 -1 } void Student_DisplaySingle(int index)//输出表头 { printf("%10s%10s%8s%8s%8s%10s\n","学号","姓名","语文","数学","英语","计算机","总成绩","平均成绩"); printf("-------------------------------------------------------------\n"); printf("%10s%10s%8.2f%8.2f%8.2f%8.2f%10.2f%10.2f\n",students[index].ID,students[index].Name, students[index].Mark1,students[index].Mark2,students[index].Mark3,students[index].Mark4,students[index].All,students[index].Average); } void inputt()//利用循环录入学生信息 { while(1) { printf("请输入学号:"); scanf("%s",&students[num].ID); getchar(); printf("请输入姓名:"); scanf("%s",&students[num].Name); getchar(); printf("请输入成绩:"); scanf("%f",&students[num].Mark1); getchar(); printf("请输入成绩:"); scanf("%f",&students[num].Mark2); getchar(); printf("请输入成绩:"); scanf("%f",&students[num].Mark3); getchar(); printf("请输入成绩:"); scanf("%f",&students[num].Mark4); //依次输入各项数据 getchar(); students[num].All=students[num].Mark1+students[num].Mark2+students[num].Mark3+students[num].Mark4; //输完数据后自动计算总成绩与平均成绩 students[num].Average=(students[num].Mark1+students[num].Mark2+students[num].Mark3+students[num].Mark4)/4; if(Student_SearchByIndex(students[num].ID) == -1) { num++; //移向下一个位置 } else { printf("学号重复,输入数据无效 !!!\n"); } printf("是否继续?(y/n)"); if (getchar()=='n') { break; } } } void modify()//修改成绩 { while(1) { char id[20]; int index; printf("请输入要修改的学生的学号:"); scanf("%s",&id); getchar(); index=Student_SearchByIndex(id); //调用搜查id函数,根据其返回值确定位置 if (index==-1) { printf("学生不存在!\n"); } else { printf("你要修改的学生信息为:\n"); Student_DisplaySingle(index); printf("-- 请输入新值--\n"); printf("请输入学号:"); scanf("%s",&students[index].ID); getchar(); printf("请输入姓名:"); scanf("%s",&students[index].Name); getchar(); printf("请输入语文成绩:"); scanf("%f",&students[index].Mark1); getchar(); printf("请输入数学成绩:"); scanf("%f",&students[index].Mark2); getchar(); printf("请输入英语成绩:"); scanf("%f",&students[index].Mark3); getchar(); printf("请输入计算机成绩:"); scanf("%f",&students[index].Mark4); //重新录入一套新的数据替代 getchar(); students[index].All=students[index].Mark1+students[index].Mark2+students[index].Mark3+students[index].Mark4; students[index].Average=(students[index].Mark1+students[index].Mark2+students[index].Mark3+students[index].Mark4)/4; } printf("是否继续?(y/n)"); if (getchar()=='n') { break; } } } void deletee()//删除学生信息 { int i; while(1) { char id[20]; int index; printf("请输入要删除的学生的学号:"); scanf("%s",&id); getchar(); index=Student_SearchByIndex(id); //调用搜查id函数,根据其返回值确定位置 if (index==-1) { printf("学生不存在!\n"); } else { printf("你要删除的学生信息为:\n"); Student_DisplaySingle(index); printf("是否真的要删除?(y/n)"); if (getchar()=='y') { for (i=index;i<num-1;i++) { Copy(students,i,i+1); //students[i]=students[i+1]; //把后边的对象都向前移动 } num--; } getchar(); } printf("是否继续?(y/n)"); if (getchar()=='n') { break; } } } void display()//打印已录入的学生信息 { int a; printf("%10s%10s%8s%8s%8s%8s%10s%10s\n","学号","姓名","语文","数学","英语","计算机","总成绩","平均成绩"); printf("-------------------------------------------------------------\n"); for (a=0;a<num;a++) { printf("%10s%10s%8.2f%8.2f%8.2f%8.2f%10.2f%10.2f\n",students[a].ID,students[a].Name, students[a].Mark1,students[a].Mark2,students[a].Mark3,students[a].Mark4,students[a].All,students[a].Average); } } void insert()//指定位置插入学生信息 { int a,b,c; printf("请输入你要插入的位置"); scanf("%d",&a); if(a>num) { printf("输入的位置有误,请重新输入,当前共%d条数据\n",num); scanf("%d",&a);} b=num-1; for(;b>=a-1;b--) { //strcpy(students[b+1].ID,students[b].ID); //strcpy(students[b+1].Name,students[b].Name); //students[b+1].Mark1=students[b].Mark1; //students[b+1].Mark2=students[b].Mark2; //students[b+1].Mark3=students[b].Mark3; //students[b+1].Mark4=students[b].Mark4; //students[b+1].All=students[b].All; //students[b+1].Average=students[b].Average; Copy(students,b+1,b); //根据其输入的位置,将其及以后的数据向后移动一个位置 } num++; printf("请输入学号:"); scanf("%s",&students[a-1].ID); getchar(); printf("请输入姓名:"); scanf("%s",&students[a-1].Name); getchar(); printf("请输入语文成绩:"); scanf("%f",&students[a-1].Mark1); getchar(); printf("请输入数学成绩:"); scanf("%f",&students[a-1].Mark2); getchar(); printf("请输入英语成绩:"); scanf("%f",&students[a-1].Mark3); getchar(); printf("请输入计算机成绩:"); scanf("%f",&students[a-1].Mark4); //输入新数据 getchar(); students[a-1].All=students[a-1].Mark1+students[a-1].Mark2+students[a-1].Mark3+students[a-1].Mark4; students[a-1].Average=(students[a-1].Mark1+students[a-1].Mark2+students[a-1].Mark3+students[a-1].Mark4)/4; } void search()//查询学生信息 { while(1) { char name[20]; int index; printf("请输入要查询的学生的姓名:"); scanf("%s",&name); getchar(); index=Student_SearchByName(name); //调用搜查name函数,根据其返回值确定位置 if (index==-1) { printf("学生不存在!\n"); } else { printf("你要查询的学生信息为:\n"); Student_DisplaySingle(index); } printf("是否继续?(y/n)"); if (getchar()=='n') { break; } } } voidsort()//根据平均分排序 (此时注意按照题目要求应该排序两个) { int i,j; //struct students tmp; for (i=0;i<num;i++) { students[i].Average=(students[i].Mark1+students[i].Mark2+students[i].Mark3+students[i].Mark4)/4; } for (i=0;i<num;i++) { for (j=1;j<num-i;j++) { if (students[j-1].Average<students[j].Average) { Copy(students,num,j-1); Copy(students,j-1,j); Copy(students,j,num); //tmp=students[j-1]; //students[j-1]=students[j]; //students[j]=tmp; //冒泡排序 } } } int a; printf("%10s%10s%8s%8s%8s%10s\n","学号","姓名","语文","数学","英语","计算机","总成绩","平均成绩"); printf("-------------------------------------------------------------\n"); for (a=0;a<num;a++) { printf("%10s%10s%8.2f%8.2f%8.2f%8.2f%10.2f%10.2f\n",students[a].ID,students[a].Name, students[a].Mark1,students[a].Mark2,students[a].Mark3,students[a].Mark4,students[a].All,students[a].Average); } } void SearchLow()//搜索不及格的并输出 { int a; printf(" 语文不及格的有%10s%10s%8s\n","学号","姓名","语文"); for(a=0;a<num;a++) { if(students[a].Mark1<60) printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark1); //从头搜索到尾,若小于60就输出 } printf(" 数学不及格的有%10s%10s%8s\n","学号","姓名","数学"); for(a=0;a<num;a++) { if(students[a].Mark2<60) printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark2); } printf(" 英语不及格的有%10s%10s%8s\n","学号","姓名","英语"); for(a=0;a<num;a++) { if(students[a].Mark3<60) printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark3); } printf(" 计算机不及格的有%10s%10s%8s\n","学号","姓名","计算机"); for(a=0;a<num;a++) { if(students[a].Mark4<60) printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark4); } system("pause"); //这个好像没作用 } void SearchHigh()//搜索成绩最高者输出 { int a; int max ; printf(" 语文最高分为%10s%10s%8s\n","学号","姓名","语文"); max=students[0].Mark1; for(a=1;a<num;a++) { if(students[a].Mark1>max) max=students[a].Mark1; } for(a=0;a<num;a++) { if(max==students[a].Mark1) printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark1); } printf(" 数学最高分为%10s%10s%8s\n","学号","姓名","数学"); max=students[0].Mark2; for(a=1;a<num;a++) { if(students[a].Mark2>max) max=students[a].Mark2; } for(a=0;a<num;a++) { if(max==students[a].Mark2) printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark2); } printf(" 英语最高分为%10s%10s%8s\n","学号","姓名","英语"); max=students[0].Mark3; for(a=1;a<num;a++) { if(students[a].Mark3>max) max=students[a].Mark3; } for(a=0;a<num;a++) { if(max==students[a].Mark3) printf(" %10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark3); } printf(" 计算机最高分为%10s%10s%8s\n","学号","姓名","计算机"); max=students[0].Mark4; for(a=1;a<num;a++) { if(students[a].Mark4>max) max=students[a].Mark4; } for(a=0;a<num;a++) { if(max==students[a].Mark4) printf(" %10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark4); } system("pause"); } void Save() { FILE*fp = fopen("temp.txt","w+"); fprintf(fp,"%d\n",num); for(int i = 0 ; i< num ;i++) { fprintf(fp,"%s %s %f %f %f %f %f %f\n",students[i].ID,students[i].Name,students[i].Mark1,students[i].Mark2,students[i].Mark3,students[i].Mark4,students[i].All,students[i].Average); } fclose(fp); } void Load() { FILE*fp = fopen("temp.txt","r"); fscanf(fp,"%d",&num); for(int i = 0 ; i< num ;i++) { fscanf(fp,"%s %s %f %f %f %f %f %f\n",students[i].ID,students[i].Name,&students[i].Mark1,&students[i].Mark2,&students[i].Mark3,&students[i].Mark4,&students[i].All,&students[i].Average); } fclose(fp); } /*主程序*/ int main(){ int i; while(1){ Load(); printf("\t\t\t\t\t-------- 学生成绩管理系统-------\n\n\n\n"); //菜单 printf("\t\t\t\t\t1. 增加学生记录\n\n"); printf("\t\t\t\t\t2. 修改学生记录\n\n"); printf("\t\t\t\t\t3. 删除学生记录\n\n"); printf("\t\t\t\t\t4. 插入学生记录\n\n"); printf("\t\t\t\t\t5. 显示所有记录\n\n"); printf("\t\t\t\t\t6. 查询学生记录\n\n"); printf("\t\t\t\t\t7. 按平均成绩排序\n\n"); printf("\t\t\t\t\t8. 输出各科目不及格学生\n\n"); printf("\t\t\t\t\t9. 输出各科目最高分\n\n"); printf("\t\t\t\t\t0. 退出\n\n\n"); printf("请选择(0-9):"); scanf("%d",&i); switch(i){ case 1:inputt();break; case 2:modify();break; case 3:deletee();break; case 4:insert();break; case 5:display();break; case 6:search();break; case 7:sort();break; case 8:SearchLow();break; case 9:SearchHigh();break; case 0:exit(0); default: ; } Save(); } return 0; } void Save() { FILE *fp = fopen("temp.txt", "w+"); fprintf(fp, "%d\n", num); for (int i = 0 ; i < num ; i++) { fprintf(fp, "%s %s %f %f %f %f %f %f\n", students[i].ID, students[i].Name, students[i].Mark1, students[i].Mark2, students[i].Mark3, students[i].Average1); } fclose(fp); } void Load() { FILE *fp = fopen("temp.txt", "r"); fscanf(fp, "%d", &num); for (int i = 0 ; i < num ; i++) { fscanf(fp, "%s %s %f %f %f %f %f %f\n", students[i].ID, students[i].Name, &students[i].Mark1, &students[i].Mark2, &students[i].Mark3, &students[i].Average1); } fclose(fp);
示例问题:创建图书目录
Gwen Glenn要打印一份图书目录。她想打印每本书的各种信息:书名、 作者、出版社、版权日期、页数、册数和价格。其中的一些项目(如,书 名)可以储存在字符数组中,其他项目需要一个int数组或float数组。用 7 个 不同的数组分别记录每一项比较繁琐,尤其是 Gwen 还想创建多份列表:一 份按书名排序、一份按作者排序、一份按价格排序等。如果能把图书目录的 信息都包含在一个数组里更好,其中每个元素包含一本书的相关信息。 因此,Gwen需要一种即能包含字符串又能包含数字的数据形式,而且 还要保持各信息的独立。C结构就满足这种情况下的需求。我们通过一个示 例演示如何创建和使用数组。但是,示例进行了一些限制。第一,该程序示 例演示的书目只包含书名、作者和价格。第二,只有一本书的数目。当然, 别忘了这只是进行了限制,我们在后面将扩展该程序。请看程序清单14.1及 其输出,然后阅读后面的一些要点。 程序清单14.1 book.c程序 //* book.c -- 一本书的图书目录 */ #include <stdio.h> #include <string.h> char * s_gets(char * st, int n); #define MAXTITL41 /* 书名的最大长度 + 1 */ #define MAXAUTL31 /* 作者姓名的最大长度 + 1*/ struct book { /* 结构模版:标记是 book */ char title[MAXTITL]; 1007char author[MAXAUTL]; float value; }; /* 结构模版结束 */ int main(void) { struct book library; /* 把 library 声明为一个 book 类型的变量 */ printf("Please enter the book title.\n"); s_gets(library.title, MAXTITL); /* 访问title部分*/ printf("Now enter the author.\n"); s_gets(library.author, MAXAUTL); printf("Now enter the value.\n"); scanf("%f", &library.value); printf("%s by %s: $%.2f\n", library.title, library.author, library.value); printf("%s: \"%s\" ($%.2f)\n", library.author, library.title, library.value); printf("Done.\n"); return 0; } 1008char * s_gets(char * st, int n) { char * ret_val; char * find; ret_val = fgets(st, n, stdin); if (ret_val) { find = strchr(st, '\n'); // 查找换行符 if (find) // 如果地址不是 NULL, *find = '\0'; // 在此处放置一个空字符 else while (getchar() != '\n') continue; //处理输入行中剩余的字符 } return ret_val; } 我们使用前面章节中介绍的s_gets()函数去掉fgets()储存在字符串中的换 行符。下面是该例的一个运行示例: Please enter the book title. 1009Chicken of the Andes Now enter the author. Disma Lapoult Now enter the value. 29.99 Chicken of the Andes by Disma Lapoult: $29.99 Disma Lapoult: "Chicken of the Andes" ($29.99) Done. 程序清单14.1中创建的结构有3部分,每个部分都称为成员(member) 或字段( field)。这3部分中,一部分储存书名,一部分储存作者名,一部 分储存价格。下面是必须掌握的3个技巧: 为结构建立一个格式或样式; 声明一个适合该样式的变量; 访问结构变量的各个部分 建立结构声明 结构声明( structure declaration)描述了一个结构的组织布局。声明类 似下面这样: struct book { char title[MAXTITL]; char author[MAXAUTL]; float value; }; 该声明描述了一个由两个字符数组和一个float类型变量组成的结构。该 声明并未创建实际的数据对象,只描述了该对象由什么组成。〔有时,我们 把结构声明称为模板,因为它勾勒出结构是如何储存数据的。如果读者知道 C++的模板,此模板非彼模板,C++中的模板更为强大。〕我们来分析一些 细节。首先是关键字 struct,它表明跟在其后的是一个结构,后面是一个可 选的标记(该例中是 book),稍后程序中可以使用该标记引用该结构。所 以,我们在后面的程序中可以这样声明: struct book library; 这把library声明为一个使用book结构布局的结构变量。 在结构声明中,用一对花括号括起来的是结构成员列表。每个成员都用 自己的声明来描述。例如,title部分是一个内含MAXTITL个元素的char类型 数组。成员可以是任意一种C的数据类型,甚至可以是其他结构!右花括号 后面的分号是声明所必需的,表示结构布局定义结束。可以把这个声明放在 所有函数的外部(如本例所示),也可以放在一个函数定义的内部。如果把 结构声明置于一个函数的内部,它的标记就只限于该函数内部使用。如果把 结构声明置于函数的外部,那么该声明之后的所有函数都能使用它的标记。 1011例如,在程序的另一个函数中,可以这样声明: struct book dickens; 这样,该函数便创建了一个结构变量dickens,该变量的结构布局是 book。 结构的标记名是可选的。但是以程序示例中的方式建立结构时(在一处 定义结构布局,在另一处定义实际的结构变量),必须使用标记。我们学完 如何定义结构变量后,再来看这一点。 访问结构成员 结构类似于一个“超级数组”,这个超级数组中,可以是一个元素为char 类型,下一个元素为forat类型,下一个元素为int数组。可以通过数组下标单 独访问数组中的各元素,那么,如何访问结构中的成员?使用结构成员运算 符——点( .)访问结构中的成员。例如,library.value即访问library的value 部分。可以像使用任何float类型变量那样使用library.value。与此类似,可以 像使用字符数组那样使用 library.title。因此,程序清单 14.1 中的程序中有 s_gets(library.title, MAXTITL);和scanf("%f", &library.value);这样的代码。 本质上,.title、.author和.value的作用相当于book结构的下标。 注意,虽然library是一个结构,但是library.value是一个float类型的变 量,可以像使用其他 float 类型变量那样使用它。例如,scanf("%f",...)需要一 个 float 类型变量的地址,而&library.float正好符合要求。.比&的优先级高, 因此这个表达式和&(library.float)一样。
指向结构的指针
喜欢使用指针的人一定很高兴能使用指向结构的指针。至少有 4 个理由
可以解释为何要使用指向结构的指针。第一,就像指向数组的指针比数组本
身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容
易操控。第二,在一些早期的C实现中,结构不能作为参数传递给函数,但
是可以传递指向结构的指针。第三,即使能传递一个结构,传递指针通常更
有效率。第四,一些用于表示数据的结构中包含指向其他结构的指针。
下面的程序(程序清单14.4)演示了如何定义指向结构的指针和如何用
这样的指针访问结构的成员。
程序清单14.4 friends.c程序 /* friends.c -- 使用指向结构的指针 */ #include <stdio.h> #define LEN 20 struct names { char first[LEN]; char last[LEN]; }; struct guy { struct names handle; char favfood[LEN]; char job[LEN]; 1031float income; }; int main(void) { struct guy fellow[2] = { { { "Ewen", "Villard" }, "grilled salmon", "personality coach", 68112.00 }, { { "Rodney", "Swillbelly" }, "tripe", "tabloid editor", 432400.00 } }; struct guy * him; /* 这是一个指向结构的指针 */ printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]); him = &fellow[0]; /* 告诉编译器该指针指向何处 */ 1032printf("pointer #1: %p #2: %p\n", him, him + 1); printf("him->income is $%.2f: (*him).income is $%.2f\n", him->income, (*him).income); him++; /* 指向下一个结构 */ printf("him->favfood is %s: him->handle.last is %s\n", him->favfood, him->handle.last); return 0; } 该程序的输出如下: address #1: 0x7fff5fbff820 #2: 0x7fff5fbff874 pointer #1: 0x7fff5fbff820 #2: 0x7fff5fbff874 him->income is $68112.00: (*him).income is $68112.00 him->favfood is tripe: him->handle.last is Swillbelly
关键概念
我们在编程中要表示的信息通常不只是一个数字或一些列数字。程序可
能要处理具有多种属性的实体。例如,通过姓名、地址、电话号码和其他信
息表示一名客户;或者,通过电影名、发行人、播放时长、售价等表示一部
电影DVD。C结构可以把这些信息都放在一个单元内。在组织程序时这很重
要,因为这样可以把相关的信息都储存在一处,而不是分散储存在多个变量
中。
设计结构时,开发一个与之配套的函数包通常很有用。例如,写一个以
结构(或结构的地址)为参数的函数打印结构内容,比用一堆printf()语句强
得多。因为只需要一个参数就能打印结构中的所有信息。如果把信息放到零
散的变量中,每个部分都需要一个参数。另外,如果要在结构中增加一个成
员,只需重写函数,不必改写函数调用。这在修改结构时很方便。
联合声明与结构声明类似。但是,联合的成员共享相同的存储空间,而
且在联合中同一时间内只能有一个成员。实质上,可以在联合变量中储存一
个类型不唯一的值。
enum 工具提供一种定义符号常量的方法,typedef 工具提供一种为基本
或派生类型创建新标识符的方法。
指向函数的指针提供一种告诉函数应使用哪一个函数的方法。
本章小结
C 结构提供在相同的数据对象中储存多个不同类型数据项的方法。可以 使用标记来标识一个具体的结构模板,并声明该类型的变量。通过成员点运 算符(.)可以使用结构模版中的标签来访问结构的各个成员。 如果有一个指向结构的指针,可以用该指针和间接成员运算符(->)代 替结构名和点运算符来访问结构的各成员。和数组不同,结构名不是结构的 地址,要在结构名前使用&运算符才能获得结构的地址。 一贯以来,与结构相关的函数都使用指向结构的指针作为参数。现在的 C允许把结构作为参数传递,作为返回值和同类型结构之间赋值。然而,传 递结构的地址通常更有效。 联合使用与结构相同的语法。然而,联合的成员共享一个共同的存储空 间。联合同一时间内只能储存一个单独的数据项,不像结构那样同时储存多 种数据类型。也就是说,结构可以同时储存一个int类型数据、一个double类 型数据和一个char类型数据,而相应的联合只能保存一个int类型数据,或者 一个double类型数据,或者一个char类型数据。 通过枚举可以创建一系列代表整型常量(枚举常量)的符号和定义相关 联的枚举类型。 typedef工具可用于建立C标准类型的别名或缩写。 函数名代表函数的地址,可以把函数的地址作为参数传递给其他函数, 然后这些函数就可以使用被指向的函数。如果把特定函数的地址赋给一个名 为pf的函数指针,可以通过以下两种方式调用该函数: #include <math.h> /* 提供sin()函数的原型:double sin(double) */ ... double (*pdf)(double); 1121double x; pdf = sin; x = (*pdf)(1.2); // 调用sin(1.2) x = pdf(1.2); // 同样调用 sin(1.2)
在 C 语言中*号有三个用途,
分别是:
- 乘号,用做乘法运算,例如 5*6。
- 申明一个指针,在定义指针变量时使用,例如 int *p;。
- 间接运算符,取得指针所指向的内存中的值,例如 printf(“%d”,*p)
第10章对文件的输入输出
C 文件的有关基本知识
1.1 什么是文件
文件有不同的类型,在程序设计时,主要用到两种文件: (1)程序文件。包括源程序文件(后缀为.c)、目标文件(后缀.obj)、可执 行文件(后缀.exe)等。这种文件的内容是程序代码。 (2)数据文件。文件的内容不是程序,而是供程序运行时读写的数据。 1.2 文件名 一个文件要有一个唯一的文件标识,以便用户识别和引用。 文件标识包括 3 部分:(1)文件路径 (2)文件名主干 (3)文件后缀 文件路径表示文件在外部存储设备中的位置。 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 137 扫码领答案 精讲系统课 1.3 文件的分类 根据数据的组织形式,数据文件可分为 ASCII 文件和二进制文件。 二进制文件:数据在内存中是以二进制形式存储的,如果不加转换地输出到 外存,就是二进制文件。 ASCII 文件:如果要求在外存上以 ASCII 代码形式存储,则需要在存储前 进行转换,就是 ASCII 文件,ASCII 文件又称文本文件。 1.4 文件类型指针 每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文 件的有关信息(如文件的名字、文件状态及文件当前位置等)。这些信息是存 放在一个结构体变量中的。该结构体类型是由系统声明的,取名为 FILE。 一般不对 FILE 类型变量命名,也就是不通过变量的名字来引用这些变 量,而是设置一个指向 FILE 类型变量的指针变量,然后通过它来引用这些 FILE 类型变量。 FILE *fp; 题 1.C 语言中系统默认的文件类型有文本文件和二进制文件。(判断题) 答案:正确 2.打开与关闭文件 2.1 用 fopen 函数打开数据文件 fopen 函数的调用方式为 fopen(文件名,使用文件方式); 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 138 扫码领答案 精讲系统课 表 1 使用文件方式 文件使用方式 含义 文件不存在 “r”(只读) 为了输入数据,打开一个已存在的文本文件 出错 “w”(只读) 为了输出数据,打开一个文本文件 建立新文件 “a”(追加) 向文本文件尾添加数据 出错 “rb”(只读) 为了输入数据,打开一个二进制文件 出错 “wb”(只写) 为了输出数据,打开一个二进制文件 建立新文件 “ab”(追加) 向二进制文件尾添加数据 出错 “r+”(读写) 为了读和写,打开一个文本文件 出错 “w+”(读写) 为了读和写,建立一个新的文本文件 建立新文件 “a+”(读写) 为了读和写,打开一个文本文件 出错 “rb+”(读写) 为了读和写,打开一个二进制文件 出错 “wb+”(读写) 为了读和写,建立一新的二进制文件 建立新文件 “ab+”(读写) 为读写打开一个二进制文件 出错 如果不能实现“打开”的任务,fopen 函数将会带回一个出错信息。此时 fopen 函数将带回一个空指针值 NULL。 常用下面的方法打开一个文件: if((fp=fopen(“file1”,“r”))==NULL) { printf(“cannot open this file\n”); exit(0); } 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 139 扫码领答案 精讲系统课 2.2 用 fclose 函数关闭数据文件 “关闭”就是撤销文件信息区和文件缓冲区,使文件指针变量不再指向该 文件,也就是文件指针变量与文件“脱钩”,此后不能再通过该指针对原来与 其相联系的文件进行读写操作。 关闭文件用 fclose 函数。fclose 函数调用的一般形式为 fclose(文件指针); fclose 函数也带回一个值,当成功地执行了关闭操作,则返回值为 0;否 则返回 EOF(-1)。 题 1.定义 FILE *fp;以写方式打开文件 C:\aa.dat 的正确语句是( )。 A.fp=fopen(“C:\aa.dat”,“w”); B.fp=fopen(“C:\aa.dat”,“r”); C.fp=fopen(“C:\\aa.dat”,“w”); D.fp=fopen(“C:\\aa.dat”,“r”); 答案:C 题 2.若 fp 已正确定义为一个文件指针,d.txt 为 C 盘根目录下的文本文件。 请填空,以便为“读”而打开此文件: fp=fopen(“ ”,“ ”); 答案:C:\\d.txt,r 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 140 扫码领答案 精讲系统课 3.顺序读写数据文件 3.1 向文件读写一个字符 表 2 读写一个字符的函数 函数名 调用形式 功能 返回值 fgetc fgetc(fp) 从 fp 指向的文件读 入一个字符 读成功,带回所读的字符,失 败则返回文件结束标志EOF(即 -1) fputc fputc(ch,fp) 把字符 ch 写到文件 指针变量 fp 所指向 的文件中 输出成功,返回值就是输出的 字符;输出失败,则返回EOF(即 -1) 3.2 向文件读写一个字符串 表 3 读写一个字符串的函数 函数名 调用形式 功能 返回值 fgets fgets(str,n,fp) 从 fp 指向的文件读入一个 长度为(n-1)的字符串,存 放到字符数组 str 中。 读成功,返回地址 str,失败则返回 NULL fputs fputs(str,fp) 把 str 所指向的字符串写到 文件指针变量 fp 所指向的 文件中 输出成功,返回 0; 输出失败,否则返 回非 0 值 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 141 扫码领答案 精讲系统课 3.3 用格式化的方式读写文件 一般的调用方式为: fprintf(文件指针,格式字符串,输出列表); fscanf(文件指针,格式字符串,输入列表); 3.4 用二进制方式向文件读写一组数据 C 语言允许用 fread 函数从文件中读一个数据块,用 fwrite 函数向文件写 一个数据块。在读写时是以二进制形式进行的。再想磁盘写数据时,直接将内 存中一组数据原封不动、不加转换地复制到磁盘文件上,在读入也是将磁盘文 件中若干字节的内容一批读入内存。 它们的一般调用形式为 fread(buffer,size,count,fp); fwrite(buffer,size,count,fp); 题 1.C 语言中,调用 函数来检测文件位置指针有没有到文件尾。 答案:feof 题 2.以下叙述中错误的是( )。 A.gets 函数用于从键盘输入字符串。 B.getchar 函数用于从文件读入字符。 C.fputs 函数用于把字符串输出到文件。 D.fwrite 函数用于以二进制形式输出数据到文件。 答案:B 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 142 扫码领答案 精讲系统课 题 3.将整型数组 a 中各元素存入文件 data.txt 中。(改错题) #include<stdio.h> int main() { int a[]={1,2,3,4,5,6,7,8}; FILE *fp; fp=fopen(“E:\data.txt”,w); if(fp!=NULL) { for(i=0;i<8;i++) fprintf(“%d”,a[i]); } fclose(fp); } 答案:fp=fopen(“E:\\data.txt”,”w”); fwrite(&a[i],sizeof(int),1,fp); 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 143 扫码领答案 精讲系统课 课时十二 练习题 1.C 语言中的文件类型有( )。 A.索引文件和文本文件两种 B.二进制文件一种 C.文本文件一种 D.ASCII 文件和二进制文件两种 2.若要打开名为 abc.txt 的文本文件进行读、写操作,下面符合此要求的函数 调用的是( )。 A.fopen(“abc.txt”,“r”); B.fopen(“abc.txt”,“r+”); C.fopen(“abc.txt”,“rb”); D.fopen(“abc.txt”,“w”); 3.若执行 fopen()函数时发生错误,则函数的返回值是( )。 A.地址值 B.NULL C.1 D.EOF 4.fscanf 函数的正确调用形式是( )。 A.fscanf(fp,格式控制符,输出列表); B.fscanf(格式控制符,输出列表,fp); C.fscanf(格式控制符,文件指针,输出列表); D.fscanf(文件指针,格式控制符,输入列表); 5.在向文件写时,下列哪个函数名是写单字符函数( )。 A.fread B.fgetc C.fputc D.fwrite 蜂考系统课 官方公众号:蜂考官微 学习交流加 QQ 群:975047395 144 扫码领答案 精讲系统课 6.在 C 语言中,对文件操作的一般步骤是( )。 A.打开文件,读文件,写文件,关闭文件 B.定义文件指针,读文件,写文件,关闭文件 C.定义文件指针,打开文件,读写文件,关闭文件 D.操作文件,定义文件指针,修改文件,关闭文件 7.以下程序的功能是,从键盘输入一组字符,将字符本身及字符 ASCII 码写入 D 盘中 test.txt 文本文件保存,输入字符以#结束。 #include<stdio.h> #include<stdlib.h> int main() { ; char ch; fp=fopen(“D:\\test.txt”,“ ”); while((ch=getchar())!=‘#’) fprintf(fp,“%c %d”, ); fclose(fp); return 0; }
与文件进行通信 有时,需要程序从文件中读取信息或把信息写入文件。这种程序与文件 交互的形式就是文件重定向。这种方法很简单,但是有一定限制。例如,假设要编写一个交互程序,询问用户书名并把完整的书名列表保存在文件中。如果使用重定向,应该类似于: books > bklist 用户的输入被重定向到 bklist 中。这样做不仅会把不符合要求的文本写 入 bklist,而且用户也看不到要回答什么问题。 C提供了更强大的文件通信方法,可以在程序中打开文件,然后使用特 殊的I/O函数读取文件中的信息或把信息写入文件。在研究这些方法之前, 先简要介绍一下文件的性质。 文件是什么 文件(file)通常是在磁盘或固态硬盘上的一段已命名的存储区。对我 们而言,stdio.h就是一个文件的名称,该文件中包含一些有用的信息。然 而,对操作系统而言,文件更复杂一些。例如,大型文件会被分开储存,或 者包含一些额外的数据,方便操作系统确定文件的种类。然而,这都是操作 系统所关心的,程序员关心的是C程序如何处理文件(除非你正在编写操作 系统)。 C把文件看作是一系列连续的字节,每个字节都能被单独读取。这与UNIX环境中(C的发源地)的文件结构相对应。由于其他环境中可能无法完 全对应这个模型,C提供两种文件模式:文本模式和二进制模式。 文本模式和二进制模式 首先,要区分文本内容和二进制内容、文本文件格式和二进制文件格 式,以及文件的文本模式和二进制模式。 948所有文件的内容都以二进制形式(0或1)储存。但是,如果文件最初使 用二进制编码的字符(例如, ASCII或Unicode)表示文本(就像C字符串那 样),该文件就是文本文件,其中包含文本内容。如果文件中的二进制值代 表机器语言代码或数值数据(使用相同的内部表示,假设,用于long或 double类型的值)或图片或音乐编码,该文件就是二进制文件,其中包含二 进制内容。 UNIX用同一种文件格式处理文本文件和二进制文件的内容。不奇怪, 鉴于C是作为开发UNIX的工具而创建的,C和UNIX在文本中都使用\n(换行 符)表示换行。UNIX目录中有一个统计文件大小的计数,程序可使用该计 数确定是否读到文件结尾。然而,其他系统在此之前已经有其他方法处理文 件,专门用于保存文本。也就是说,其他系统已经有一种与UNIX模型不同 的格式处理文本文件。例如,以前的OS X Macintosh文件用\r (回车符)表 示新的一行。早期的MS-DOS文件用\r\n组合表示新的一行,用嵌入的Ctrl+Z 字符表示文件结尾,即使实际文件用添加空字符的方法使其总大小是256的 倍数(在Windows中,Notepad仍然生成MS-DOS格式的文本文件,但是新的 编辑器可能使用类UNIX格式居多)。其他系统可能保持文本文件中的每一 行长度相同,如有必要,用空字符填充每一行,使其长度保持一致。或者, 系统可能在每行的开始标出每行的长度。 为了规范文本文件的处理,C 提供两种访问文件的途径:二进制模式和 文本模式。在二进制模式中,程序可以访问文件的每个字节。而在文本模式 中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时, 把本地环境表示的行末尾或文件结尾映射为C模式。例如,C程序在旧式 Macintosh中以文本模式读取文件时,把文件中的\r转换成\n;以文本模式写 入文件时,把\n转换成\r。或者,C文本模式程序在MS-DOS平台读取文件 时,把\r\n转换成\n;写入文件时,把\n转换成\r\n。在其他环境中编写的文本 模式程序也会做类似的转换。 除了以文本模式读写文本文件,还能以二进制模式读写文本文件。如果 读写一个旧式MS-DOS文本文件,程序会看到文件中的\r 和\n 字符,不会发 生映射(图 13.1 演示了一些文本)。如果要编写旧式 Mac格式、MS-DOS格 949式或UNIX/Linux格式的文件模式程序,应该使用二进制模式,这样程序才能 确定实际的文件内容并执行相应的动作。 虽然C提供了二进制模式和文本模式,但是这两种模式的实现可以相 同。前面提到过,因为UNIX使用一种文件格式,这两种模式对于UNIX实现 而言完全相同。Linux也是如此 关键概念 C程序把输入看作是字节流,输入流来源于文件、输入设备(如键 盘),或者甚至是另一个程序的输出。类似地,C程序把输出也看作是字节 流,输出流的目的地可以是文件、视频显示等。 C 如何解释输入流或输出流取决于所使用的输入/输出函数。程序可以 不做任何改动地读取和存储字节,或者把字节依次解释成字符,随后可以把 这些字符解释成普通文本以用文本表示数字。类似地,对于输出,所使用的 函数决定了二进制值是被原样转移,还是被转换成文本或以文本表示数字。 如果要在不损失精度的前提下保存或恢复数值数据,请使用二进制模式以及 fread()和fwrite()函数。如果打算保存文本信息并创建能在普通文本编辑器查 看的文本,请使用文本模式和函数(如getc()和fprintf())。 要访问文件,必须创建文件指针(类型是FILE *)并把指针与特定文件 名相关联。随后的代码就可以使用这个指针(而不是文件名)来处理该文 件。 要重点理解C如何处理文件结尾。通常,用于读取文件的程序使用一个 循环读取输入,直至到达文件结尾。C 输入函数在读过文件结尾后才会检测 到文件结尾,这意味着应该在尝试读取之后立即判断是否是文件结尾。可以 使用13.2.4节中“设计范例”中的双文件输入模式。 本章小结 对于大多数C程序而言,写入文件和读取文件必不可少。为此,绝大对 数C实现都提供底层I/O和标准高级I/O。因为ANSI C库考虑到可移植性,包 含了标准I/O包,但是未提供底层I/O。 标准 I/O 包自动创建输入和输出缓冲区以加快数据传输。fopen()函数为 标准 I/O 打开一个文件,并创建一个用于存储文件和缓冲区信息的结构。 fopen()函数返回指向该结构的指针,其他函数可以使用该指针指定待处理的 文件。feof()和ferror()函数报告I/O操作失败的原因。 C把输入视为字节流。如果使用fread()函数,C把输入看作是二进制值 并将其储存在指定存储位置。如果使用fscanf()、getc()、fgets()或其他相关函 数,C则将每个字节看作是字符码。然后fscanf()和scanf()函数尝试把字符码 翻译成转换说明指定的其他类型。例如,输入一个值23,%f转换说明会把 23翻译成一个浮点值,%d转换说明会把23翻译成一个整数值,%s转换说明 则会把23储存为字符串。getc()和 fgetc()系列函数把输入作为字符码储存, 将其作为单独的字符保存在字符变量中或作为字符串储存在字符数组中。类 似地,fwrite()将二进制数据直接放入输出流,而其他输出函数把非字符数 据转换成用字符表示后才将其放入输出流。 ANSI C提供两种文件打开模式:二进制和文本。以二进制模式打开文 件时,可以逐字节读取文件;以文本模式打开文件时,会把文件内容从文本 的系统表示法映射为C表示法。对于UNIX和Linux系统,这两种模式完全相 同。 通常,输入函数getc()、fgets()、fscanf()和fread()都从文件开始处按顺序 读取文件。然而, fseek()和ftell()函数让程序可以随机访问文件中的任意位 置。fgetpos()和fsetpos()把类似的功能扩展至更大的文件。与文本模式相 比,二进制模式更容易进行随机访问。
(拓展)一些比较有意思的东西
不使用临时变量交换两个整数的值
(其实交换两个指针变量的值也可以) #include <stdio.h> int main() { int a, b; a = 11; b = 99; printf("交换之前 - \n a = %d, b = %d \n\n", a, b); a = a + b; // ( 11 + 99 = 110) 此时 a 的变量为两数之和,b 未改变 b = a - b; // ( 110 - 99 = 11) a = a - b; // ( 110 - 11 = 99) printf("交换后 - \n a = %d, b = %d \n", a, b); }
奇偶数判断(利用二进制)
奇偶数判断其实有个更简单高效的办法,我们的整数,在计算机中存储的都是二进制奇数的最后一位必是1,所以我们可以这样写: #include <stdio.h> int main() { int number; printf("请输入一个整数: "); scanf("%d", &number); // 判断这个数最后一位是1这为奇数 if(number&1) printf("%d 是奇数。", number); else printf("%d 是偶数。", number); return 0; }
接条件判断闰年
(四年一闰,百年不闰) || 四百年在闰年 #include <stdio.h> int main() { int year; printf("输入年份: "); scanf("%d",&year); // year = 400; // (四年一闰,百年不闰) || 四百年在闰年 if((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { printf("y\n"); } else { printf("n\n"); } return 0; }
判断素数
质数(prime number)又称素数,有无限个。质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数,这样的数称为质数 (1) #include <stdio.h> int main() { int n, i, flag = 0; printf("输入一个正整数: "); scanf("%d",&n); for(i=2; i<=n/2; ++i) { // 符合该条件不是素数 if(n%i==0) { flag=1; break; } } if (flag==0) printf("%d 是素数",n); else printf("%d 不是素数",n); return 0; } 输出结果: 输入一个正整数: 29 29 是素数 (2)判断两个数之间的素数 #include <stdio.h> int main() { int low, high, i, flag; printf("输入两个整数: "); scanf("%d %d", &low, &high); printf("%d 与 %d 之间的素数为: ", low, high); while (low < high) { flag = 0; for(i = 2; i <= low/2; ++i) { if(low % i == 0) { flag = 1; break; } } if (flag == 0) printf("%d ", low); ++low; } return 0; } 输出结果: 输入两个整数: 100 200 100 与 200 之间的素数为: 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199