
前面二十篇文章我们和数据类型打了很多交道——int、double、char、数组、指针……但它们都有一个共同点一个变量只能存一种类型的数据。可真实世界里一个“东西”往往由很多属性拼成。比如一个学生有姓名字符串、学号整数、成绩浮点数一个坐标点有 x 和 y 两个坐标值。如果各存各的数据散落一地很难管理。C 语言给了我们一个“打包”工具结构体struct。它能把多个不同类型的数据捆在一起变成一个自定义的复合类型。今天我们就来学会怎么定义它、初始化它、访问它的成员以及如何用指针优雅地操作它。一、结构体的定义定义结构体的基本语法struct结构体名{类型 成员1;类型 成员2;// ... 更多成员};// 分号分号分号例如定义一个“学生”结构体structStudent{charname[50];intid;floatscore;};这就创造了一个新的类型叫struct Student。注意struct关键字和Student是一体的不能只用Student除非配合typedef后面会讲。二、声明结构体变量有了struct Student这个类型就可以像用int、double一样声明变量structStudents1;// 未初始化structStudents2{Alice,1001,92.5};// 声明并初始化也可以在定义结构体时顺便声明变量structPoint{intx;inty;}p1,p2;// 同时声明两个全局变量甚至可以不写结构体名匿名结构体但这种只能在那一次声明变量后续无法再使用该类型不常用struct{intday;intmonth;intyear;}birthday;推荐做法老老实实写好结构体定义然后在需要的地方声明变量。三、初始化结构体1. 完全初始化按成员顺序列出所有初始值structStudents{Bob,1002,88.0};2. 部分初始化只写前几个值剩余的成员会被自动初始化为 0或空字符structStudents{Charlie};// id0, score0.0这和数组的部分初始化规则一致。3. C99 指定初始化器Designated Initializer可以点名初始化某些成员不按顺序也可以structStudents{.score95.5,.nameDiana,.id1003};这种写法清晰、不怕记错顺序推荐使用。四、访问结构体成员点运算符.用变量名.成员名读写#includestdio.hstructStudent{charname[50];intid;floatscore;};intmain(void){structStudents{Eve,1004,89.5};printf(姓名: %s\n,s.name);printf(学号: %d\n,s.id);printf(成绩: %.1f\n,s.score);s.score92.0;// 修改成员printf(新成绩: %.1f\n,s.score);return0;}输出姓名: Eve 学号: 1004 成绩: 89.5 新成绩: 92.0五、结构体数组结构体本身是一种类型自然可以拿来组成数组。比如一个班的学生#includestdio.hstructStudent{charname[50];intid;floatscore;};intmain(void){structStudentclass[3]{{Alice,1001,92.5},{Bob,1002,85.0},{Carol,1003,78.5}};for(inti0;i3;i){printf(%s (%d): %.1f\n,class[i].name,class[i].id,class[i].score);}return0;}class[i]是数组的第 i 个元素它是一个struct Student再用.访问其成员。结构体数组结合循环和排序算法就能做出学生成绩管理系统的基础功能。后面的实战篇会不断用到它。六、结构体指针与箭头运算符-每个结构体变量在内存中都有地址可以声明指向它的指针structStudents{Frank,1005,76.0};structStudent*ps;// p 指向 s要通过指针访问成员有两种等价写法方式一(*p).成员—— 先解引用再用点printf(%s\n,(*p).name);括号必须加因为.优先级高于*。*p.name会被解释成*(p.name)那是错的。方式二p-成员—— 箭头运算符推荐printf(%s\n,p-name);// 简洁明了p-name就是(*p).name的语法糖。几乎所有人都用箭头所以请记住它。示例#includestdio.hstructStudent{charname[50];intid;floatscore;};intmain(void){structStudents{Grace,1006,88.0};structStudent*ps;printf(姓名: %s\n,p-name);printf(学号: %d\n,p-id);printf(成绩: %.1f\n,p-score);p-score91.5;// 用指针修改成员printf(新成绩: %.1f\n,s.score);// s.score 也变了return0;}七、结构体作为函数参数按值传递整个结构体被复制voidprint_student(structStudents){printf(%s %d %.1f\n,s.name,s.id,s.score);}调用时print_student(s1)会把整个s1复制一份给形参s。如果结构体很小比如几个int这没什么但若结构体很大比如包含一个大数组复制开销就大了。而且函数内修改s不会影响原始结构体。按指针传递只复制一个地址推荐voidprint_student(conststructStudent*s){printf(%s %d %.1f\n,s-name,s-id,s-score);}调用时传s1只复制 4 或 8 字节的指针。const修饰符表示函数不会修改结构体内容让调用者放心。同理如果要让函数修改结构体的成员传指针也很自然voidraise_score(structStudent*s,floatdelta){s-scoredelta;}结论传递结构体时优先传指针并用const标明只读意图。八、常见错误与陷阱1. 结构体定义忘记末尾分号structPoint{intx;inty;}// 错误缺少分号这个错误会导致后面连续的代码报一堆莫名其妙的错。看到奇怪错误时先检查前一个结构体定义是否少了;。2. 用比较两个结构体structStudenta{Tom,1,80};structStudentb{Tom,1,80};if(ab){...}// 错误不能直接用 比较结构体C 语言不允许对结构体直接使用。需要自己写函数逐个成员比较或使用memcmp但要小心内存对齐产生的填充字节后面会讲。3. 结构体指针未初始化就使用structStudent*p;p-id100;// 危险p 没有指向有效内存指针必须指向已存在的结构体变量或通过malloc分配空间。4. 返回局部结构体的指针structPoint*get_point(void){structPointp{0,0};returnp;// 危险p 在函数返回后消失}和普通变量一样局部结构体在栈上函数返回后失效。如果要返回结构体可以直接返回值结构体类型会复制或返回动态分配的结构体指针。5. 混淆.和-用变量本身访问成员.s.name用指针访问成员-p-name初学时常会写出p.namep 是指针应该用-或s-names 不是指针应该用.。编译器通常会给出清晰的错误提示仔细读。九、小结今天你学会了把各种数据打包成一个“复合类型”——结构体用struct关键字定义最后别忘了分号。声明变量、初始化包括指定初始化器。用.访问成员用-通过指针访问成员。结构体可以组成数组解决了批量管理复杂数据的问题。传参时优先传指针既高效又安全。结构体是 C 语言面向对象思想的雏形。后面你写的链表节点、树节点、哈希表条目全都靠它来定义。下一篇我们会深入结构体在内存中的真实布局——内存对齐到底是什么为什么结构体的大小往往比成员加起来更大以及那个诡异又实用的柔性数组是怎么工作的。这些底层细节会让你对内存的理解再上一个台阶。课后小练习定义一个struct Point包含int x和int y写一个函数double distance(const struct Point *a, const struct Point *b)计算两点之间的欧几里得距离并返回。在main中测试。用前面定义的struct Student创建一个包含 5 个学生的数组从键盘输入他们的信息然后找出成绩最高和最低的学生并打印其信息。用结构体指针和动态内存分配写一个程序先让用户输入学生数量然后用malloc分配一个struct Student的动态数组输入数据、打印、最后free。陷阱修复下面的代码有错误请找出并修正structBook{chartitle[100];intpages;}intmain(void){structBookb;b.titleC Programming;b.pages500;printf(%s\n,b.title);return0;}我们下期见获取本系列示例代码请访问 GitCode 仓库。