《C程序设计(第五版)》第9章 用户自己建立数据类型:知识要点、案例分析与课后习题解析
1. 本章在学什么在第 9 章之前我们使用的数据大多来自 C 语言内置的基本类型int、float、char、数组、指针等。但从实际问题来看很多数据天然是**“组合”在一起**的例如一个学生的学号、姓名、成绩一天的日期年、月、日一本书的书名、作者、出版社、价格。这些数据如果分别用多个独立的数组或变量存放既容易出错也无法表达“这些字段属于同一个对象”的语义。本章的核心就是解决这个问题由用户自己定义新的数据类型把不同类型的数据组织成一个整体。主要内容包括结构体struct结构体数组结构体指针用指针处理链表共用体union枚举类型enum用typedef为已有类型取新名字其中结构体 指针 链表是本章的重中之重也是后续学习数据结构如链表、树、图的直接基础。2. 为什么要建立结构体类型2.1 问题引入假设要存放 3 名学生的信息学号、姓名和成绩。如果用基本类型分开存放代码会变成intnum[3];charname[3][20];floatscore[3];这里num[0]、name[0]、score[0]分别属于同一个学生但它们在代码层面是被割裂的。要给某个学生赋值或输出必须同时记住三个下标非常容易错位。2.2 引入结构体的思路结构体允许我们把“描述同一个事物”的多个字段打包成一个整体structStudent{intnum;charname[20];floatscore;};这样num、name、score就成为了Student类型的一部分数据组织更清晰。3. 定义和使用结构体变量3.1 声明结构体类型结构体类型的一般形式为struct结构体名{成员列表;};例如定义一个“日期”类型structDate{intyear;intmonth;intday;};注意这里struct Date是一个类型名year、month、day是它的成员。3.2 定义结构体变量常用的有 3 种方式方式一先声明类型再定义变量structDated1,d2;方式二声明类型的同时定义变量structDate{intyear;intmonth;intday;}d1,d2;方式三不指定类型名直接定义变量匿名结构体struct{intyear;intmonth;intday;}d1,d2;方式三的缺点是以后无法再用这个“无名类型”定义新的变量因此实际开发中推荐方式一或方式二。3.3 初始化结构体变量在定义变量的同时可以初始化structDated1{2024,7,15};也可以逐个成员赋值structDated2;d2.year2025;d2.month1;d2.day1;C 语言不允许对结构体变量整体进行scanf输入只能通过成员逐个输入。3.4 引用结构体成员通过成员运算符.访问成员其优先级最高d1.year2024;printf(%d-%d-%d\n,d1.year,d1.month,d1.day);若成员本身是数组或字符串仍按对应的数组/字符串方式操作structStudentstu;stu.num1001;strcpy(stu.name,Zhang San);// 字符数组不能直接整体赋值stu.score89.5;4. 结构体数组当一个“组合数据”需要存储多条时可以使用结构体数组。4.1 定义与初始化structStudent{intnum;charname[20];floatscore;};structStudentclass[3]{{1001,Zhang San,89.5},{1002,Li Si,92.0},{1003,Wang Wu,78.5}};4.2 访问结构体数组元素结构体数组元素class[i]本身是一个结构体变量所以先通过下标取到元素再用.访问成员for(inti0;i3;i){printf(%d %s %.1f\n,class[i].num,class[i].name,class[i].score);}4.3 应用示例按成绩排序#includestdio.h#includestring.hstructStudent{intnum;charname[20];floatscore;};intmain(void){structStudents[3]{{1001,Zhang,89.5},{1002,Li,92.0},{1003,Wang,78.5}};structStudenttmp;// 按成绩从高到低进行简单的冒泡排序for(inti0;i2;i){for(intj0;j2-i;j){if(s[j].scores[j1].score){tmps[j];s[j]s[j1];s[j1]tmp;}}}for(inti0;i3;i){printf(%d %s %.1f\n,s[i].num,s[i].name,s[i].score);}return0;}tmp s[j];说明同类型的结构体变量可以整体赋值这是结构体与数组的重要区别之一。5. 结构体指针5.1 定义结构体指针structStudentstu{1001,Zhang,89.5};structStudent*pstu;5.2 通过指针访问成员有两种等价写法(*p).nump-num推荐-称为指向运算符表示“先解引用再取成员”。printf(%d\n,(*p).num);printf(%d\n,p-num);两种写法完全等价实际代码中常用p-num。5.3 结构体指针作为函数参数为了避免复制整个结构体尤其成员较多时开销较大常把结构体指针作为函数参数voidprintStudent(conststructStudent*p){printf(%d %s %.1f\n,p-num,p-name,p-score);}这里使用const表示函数不会修改结构体内容是一种良好的编程习惯。6. 用指针处理链表链表是本章的核心难点。数组要求连续存放、长度固定链表则通过“结点 指针”把零散的数据连接起来便于动态插入和删除。6.1 结点结构structNode{intdata;// 数据域structNode*next;// 指针域指向下一个结点};next是指向同类结构体的指针这是构造链表的关键。这种“包含自身类型指针”的定义是合法的。6.2 建立简单的静态链表#includestdio.hstructNode{intdata;structNode*next;};intmain(void){structNodea{1,NULL};structNodeb{2,NULL};structNodec{3,NULL};a.nextb;b.nextc;// 遍历链表structNode*pa;while(p!NULL){printf(%d - ,p-data);pp-next;}printf(NULL\n);return0;}输出结果为1 - 2 - 3 - NULL6.3 动态建立链表使用malloc逐个创建结点#includestdio.h#includestdlib.hstructNode{intdata;structNode*next;};// 在链表头部插入新结点返回新的头指针structNode*insertHead(structNode*head,intvalue){structNode*newNode(structNode*)malloc(sizeof(structNode));if(newNodeNULL){printf(内存分配失败\n);exit(1);}newNode-datavalue;newNode-nexthead;returnnewNode;}intmain(void){structNode*headNULL;// 依次插入 3、2、1最终链表为 1 - 2 - 3headinsertHead(head,3);headinsertHead(head,2);headinsertHead(head,1);structNode*phead;while(p!NULL){printf(%d ,p-data);pp-next;}printf(\n);return0;}易错点提示使用malloc后一定要检查返回值是否为NULL插入/删除结点时要注意指针的赋值顺序否则容易“断链”动态链表使用完毕应及时free避免内存泄漏。7. 共用体、枚举与 typedef7.1 共用体 union共用体与结构体的定义形式类似用关键字unionunionData{inti;charch;floatf;};区别在于结构体各成员占有不同的内存总长度约等于各成员之和而共用体的所有成员共用同一段内存总长度等于最长成员的长度。unionData d;d.i97;printf(%c\n,d.ch);// 输出 a因为整型 97 与字符 a 共用同一内存任一时刻共用体只能保存一个成员的值后赋值的成员会覆盖前面成员的值。7.2 枚举类型 enum枚举用于把一组有含义的常量组织在一起enumColor{red,yellow,blue};默认情况下red的值为 0yellow为 1blue为 2。也可以手动指定enumWeekday{Monday1,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday};这里Tuesday会自动取 2依次类推。枚举变量的定义与使用enumColorcblue;printf(%d\n,c);// 输出 27.3 typedef 声明新类型名typedef并不创建新类型而是给已有类型起一个别名typedefstructStudent{intnum;charname[20];floatscore;}Student;以后就可以直接写Student stu;// 等价于 struct Student stu;常见用法还包括typedefintElemType;// 给 int 取别名typedefstructNode*LinkList;// 给指针类型取别名8. 案例分析学生成绩管理系统下面综合运用本章知识实现一个简化版的学生成绩管理系统。8.1 功能设计使用结构体保存学生信息使用结构体数组保存多条数据使用结构体指针遍历和处理数据提供输入、输出、求平均值、查找最高分等功能。8.2 完整代码#includestdio.h#defineN5structStudent{intnum;charname[20];floatscore;};// 输入学生信息voidinput(structStudent*s,intn){for(inti0;in;i){printf(请输入第 %d 个学生的学号、姓名、成绩\n,i1);scanf(%d %s %f,s[i].num,s[i].name,s[i].score);}}// 输出学生信息voidoutput(conststructStudent*s,intn){printf(\n学号\t姓名\t成绩\n);for(inti0;in;i){printf(%d\t%s\t%.1f\n,s[i].num,s[i].name,s[i].score);}}// 求平均成绩floataverage(conststructStudent*s,intn){floatsum0;for(inti0;in;i){sums[i].score;}returnsum/n;}// 查找最高分学生的下标intmaxIndex(conststructStudent*s,intn){intmax0;for(inti1;in;i){if(s[i].scores[max].score){maxi;}}returnmax;}intmain(void){structStudents[N];input(s,N);output(s,N);printf(平均成绩%.2f\n,average(s,N));intmmaxIndex(s,N);printf(最高分学生%d %s %.1f\n,s[m].num,s[m].name,s[m].score);return0;}8.3 设计要点结构体数组名s在传给函数时本质上会退化为指向第一个元素的指针因此形参常写作struct Student *s函数接收const struct Student *表明函数只读数据、不修改数据语义更清晰把“输入、输出、统计、查找”拆成独立函数是结构化程序设计思想在结构体上的典型应用。9. 课后习题解析以下选取的是本章常见的典型题型涵盖结构体定义、成员引用、结构体数组、结构体指针与链表等核心考点。建议读者先独立作答再对照解析。习题 1结构体定义与成员赋值题目定义一个结构体变量成员包括“年、月、日”并从键盘输入日期后输出。解析与答案#includestdio.hstructDate{intyear;intmonth;intday;};intmain(void){structDated;printf(请输入年、月、日\n);scanf(%d %d %d,d.year,d.month,d.day);printf(%d年%d月%d日\n,d.year,d.month,d.day);return0;}考查点结构体变量只能通过成员运算符.逐个输入不能整体scanf。习题 2结构体数组的遍历与统计题目有 5 名学生使用结构体数组存储其学号和成绩统计不及格人数并输出。解析与答案#includestdio.h#defineN5structStudent{intnum;floatscore;};intmain(void){structStudents[N]{{1,56},{2,78},{3,90},{4,43},{5,61}};intcount0;for(inti0;iN;i){if(s[i].score60){count;}}printf(不及格人数%d\n,count);return0;}考查点结构体数组元素s[i]是结构体变量通过s[i].score访问成员。习题 3结构体指针访问成员题目定义结构体变量并用指针指向它分别用(*p).member和p-member输出成员。解析与答案#includestdio.hstructStudent{intnum;floatscore;};intmain(void){structStudentstu{1001,88.5};structStudent*pstu;printf(%d %.1f\n,(*p).num,(*p).score);printf(%d %.1f\n,p-num,p-score);return0;}考查点(*p).num与p-num等价-运算符使用更简洁。习题 4链表结点的建立与遍历题目用结构体建立包含 3 个结点的动态链表并遍历输出各结点的数据。解析与答案#includestdio.h#includestdlib.hstructNode{intdata;structNode*next;};intmain(void){structNode*headNULL;structNode*pNULL;// 尾插法建立链表依次插入 10、20、30for(inti0;i3;i){structNode*newNode(structNode*)malloc(sizeof(structNode));if(newNodeNULL){printf(内存分配失败\n);return1;}newNode-data(i1)*10;newNode-nextNULL;if(headNULL){headnewNode;}else{p-nextnewNode;}pnewNode;}// 遍历输出phead;while(p!NULL){printf(%d ,p-data);pp-next;}printf(\n);return0;}考查点动态内存分配malloc、链表尾插法、用p-next连接结点。10. 常见易错点与小结10.1 常见易错点直接对结构体变量使用scanf错误写法scanf(%d%d%d, d);正确做法逐个成员输入scanf(%d%d%d, d.year, d.month, d.day);字符数组成员直接整体赋值错误写法stu.name Zhang;正确做法使用strcpy(stu.name, Zhang);混淆.和-的使用场景结构体变量用.stu.num结构体指针用-p-num链表操作丢失头指针或断链插入、删除结点时要先保存必要的指针再修改next避免链表断裂。忽略malloc的返回值检查内存分配可能失败必须判断返回指针是否为NULL。忘记释放动态链表的结点动态分配的内存在不再使用时要用free释放。10.2 本章小结本章从“为什么要自定义数据类型”出发系统介绍了 C 语言中用户建立数据类型的主要手段结构体把相关的不同数据类型组织成一个整体是最常用的自定义类型结构体数组解决“多个结构体对象”的存储问题结构体指针提高函数传参效率也是链表的基础链表用结点指针把零散数据串起来支持灵活插入和删除共用体、枚举、typedef丰富了数据表达方式提高了程序可读性。掌握本章后读者就具备了从“变量”走向“对象化数据组织”的关键能力也为后续学习数据结构和 C 中的类打下了扎实基础。