尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C语言:自定义类型:结构体

C语言:自定义类型:结构体 文章目录前言1.什么是结构体2.结构体类型的声明2.1结构的声明2.2 结构体变量的创建和初始化2.3 结构的特殊声明2.4 结构体的自引用3.结构体内存对齐3.1 对齐规则3.2 为什么需要内存对齐3.3 修改默认对齐数3.4 结构体传参4.结构体实现位段4.1 位段的定义4.2 位段的内存分配4.3 位段的注意事项前言在 C 语言的世界里我们早已熟悉了int、char、float等内置的基本数据类型。它们能很好地表示单个的数值或字符。然而现实世界中的数据往往是复杂且相互关联的比如一个学生的信息学号、姓名、成绩或者一个坐标点x, y。为了描述这些由多个不同类型数据组合而成的实体C 语言提供了强大的“自定义类型”机制。结构体正是自定义类型中最核心、最常用的一员。它允许我们将多个不同类型的变量“打包”成一个整体从而创建出符合我们业务逻辑的新数据类型。理解并掌握结构体是迈向 C 语言中级编程乃至学习数据结构如链表、树的必经之路。1.什么是结构体结构体是 C 语言中一种用户自定义的复合数据类型。它允许你将多个不同类型的变量称为“成员”组合在一起形成一个逻辑上的整体用来描述一个具有多个属性的实体。2.结构体类型的声明2.1结构的声明声明一个结构体类型就是定义这个“数据包”的模板告诉编译器这个新类型叫什么名字里面包含哪些成员变量。声明本身不分配内存只有用这个类型去声明变量时才会分配。结构体类型的声明使用struct关键字基本格式如下struct结构体标签{数据类型 成员1;数据类型 成员2;// ... 更多成员};structC 语言关键字表示开始定义一个结构体类型。结构体标签给这个结构体类型起的名字用于后续引用。例如Student、Point。{}大括号内是成员列表定义了结构体包含哪些数据。; 结构体声明末尾的分号不能省略。例如声明一个表示学生的结构体类型structStudent{charname[20];// 姓名字符数组成员intage;// 年龄整型成员floatscore;// 成绩浮点型成员};// 注意分号这里我们声明了一个名为struct Student的新类型。它包含三个成员name、age、score。2.2 结构体变量的创建和初始化声明了结构体类型后就可以用它来创建变量称为结构体变量并为其赋值初始化。#includestdio.h// 声明一个学生结构体类型三个成员structStu{charname[20];// 名字intage;// 年龄charsex[5];// 性别};intmain(){// 1. 按照结构体成员的顺序初始化structStus1{张三,20,男};printf(name: %s\n,s1.name);printf(age : %d\n,s1.age);printf(sex : %s\n,s1.sex);// 2. 按照指定的顺序初始化使用“.”成员运算符structStus2{.age18,.name李四,.sex女};printf(\nname: %s\n,s2.name);printf(age : %d\n,s2.age);printf(sex : %s\n,s2.sex);return0;}2.3 结构的特殊声明除了前面介绍的标准声明方式C 语言还允许一种特殊的结构体声明即匿名结构体类型也称为“无标签结构体”。这种声明方式省略了结构体标签通常用于一次性定义变量或者嵌套在其他结构体中。匿名结构体类型的声明格式如下struct{数据类型 成员1;数据类型 成员2;// ... 更多成员}变量名1,变量名2,...;由于没有标签这种类型只能在声明的同时定义变量之后无法再用这个类型去创建新的变量。#includestdio.hintmain(){// 声明一个匿名结构体类型并同时定义两个变量 point1, point2struct{intx;inty;}point1,point2;// 为变量赋值point1.x10;point1.y20;point2.x30;point2.y40;printf(point1: (%d, %d)\n,point1.x,point1.y);printf(point2: (%d, %d)\n,point2.x,point2.y);return0;}匿名结构体常与typedef关键字结合为其创建一个别名从而变相地“命名”这个类型使其可以重复使用。#includestdio.h// 使用 typedef 为匿名结构体类型创建一个别名 Pointtypedefstruct{intx;inty;}Point;intmain(){Point p1{5,10};// 现在可以使用别名 Point 来定义变量Point p2{20,30};printf(p1: (%d, %d)\n,p1.x,p1.y);printf(p2: (%d, %d)\n,p2.x,p2.y);return0;}这种方式结合了匿名结构体的简洁性和typedef带来的可复用性是实践中常见的一种用法。2.4 结构体的自引用结构体的自引用是指一个结构体类型中包含一个指向自身类型实例的指针成员。这是实现链表、树等递归数据结构的基础。示例链表节点structNode{intdata;// 节点数据structNode*next;// 指向下一个节点的指针};这里next是一个指向struct Node类型的指针而不是struct Node本身那会导致无限大小。通过这种方式多个节点可以连接成链。3.结构体内存对齐结构体内存对齐是编译器为了提高内存访问效率而采用的一种内存布局规则。它要求结构体的每个成员在内存中的起始地址必须是其自身类型大小或编译器指定对齐值的整数倍。3.1 对齐规则结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处。从第2个成员变量开始都要对齐到某个对齐数的整数倍的地址处。对齐数 编译器默认的一个对齐数 与 该成员变量大小的较小值。VS 中默认的值为 8Linux 中 gcc 没有默认对齐数对齐数就是成员自身的大小。结构体总大小为最大对齐数结构体中每个成员变量都有一个对齐数所有对齐数中最大的的整数倍。如果嵌套了结构体的情况嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体中成员的对齐数的整数倍。#include stdio.h struct Example1 { char a; // 1 字节偏移 0 int b; // 4 字节偏移必须是 4 的倍数所以从 4 开始 char c; // 1 字节偏移 8 }; // 总大小1 3填充 4 1 9 → 需补齐到 4 的倍数 → 12 字节 struct Example2 { int b; // 4 字节偏移 0 char a; // 1 字节偏移 4 char c; // 1 字节偏移 5 }; // 总大小4 1 1 6 → 需补齐到 4 的倍数 → 8 字节 int main() { printf(sizeof(struct Example1) %zu\n, sizeof(struct Example1)); // 输出 12 printf(sizeof(struct Example2) %zu\n, sizeof(struct Example2)); // 输出 8 return 0; }Example1因int b需要 4 字节对齐char a后填充了 3 字节导致总大小为 12。Example2通过调整成员顺序减少了填充总大小仅为 8。3.2 为什么需要内存对齐性能CPU 通常按对齐的字word读取内存未对齐的访问可能导致多次内存读取或硬件异常降低效率。平台兼容性某些硬件架构如 ARM、x86要求严格对齐否则会引发总线错误。3.3 修改默认对齐数我们可以使用#pragma pack(n)指令来修改默对齐数。#pragmapack(1)// 修改默认对齐数为1structPackedStruct{chara;intb;charc;};// sizeof 1 4 1 6#pragmapack()// 恢复默认对齐3.4 结构体传参在 C 语言中结构体作为函数参数传递时通常建议传递结构体的地址即指针而不是直接传递整个结构体变量。主要原因如下效率直接传递结构体值传递会导致整个结构体的数据被复制一份给形参。如果结构体很大包含多个成员或数组成员这种复制会消耗较多时间和栈空间。传递指针只复制一个地址通常 4 或 8 字节效率更高。修改原数据如果函数需要修改调用者传入的结构体成员必须通过指针传递。值传递只是操作副本对原结构体没有影响。#includestdio.hstructStudent{charname[20];intage;floatscore;};// 值传递效率低且无法修改原结构体voidprint1(structStudentstu){printf(姓名: %s, 年龄: %d, 成绩: %.1f\n,stu.name,stu.age,stu.score);// stu.age 25; // 修改只影响副本不影响 main 中的 s1}// 地址传递推荐高效且可以修改原结构体voidprint2(structStudent*pStu){printf(姓名: %s, 年龄: %d, 成绩: %.1f\n,pStu-name,pStu-age,pStu-score);pStu-age25;// 通过指针修改原结构体的成员}intmain(){structStudents1{张三,20,90.5};printf(值传递调用前: %d\n,s1.age);// 输出 20print1(s1);printf(值传递调用后: %d\n,s1.age);// 仍为 20未改变printf(\n地址传递调用前: %d\n,s1.age);// 输出 20print2(s1);// 传递 s1 的地址printf(地址传递调用后: %d\n,s1.age);// 输出 25已被修改return0;}4.结构体实现位段位段Bit-field是 C 语言中一种特殊的结构体成员它允许我们以位为单位来指定成员所占的内存长度从而更精细地控制内存使用。4.1 位段的定义位段通过在结构体成员声明后加上冒号和位数来定义struct 结构体标签 { 类型 成员名1 : 位数; 类型 成员名2 : 位数; // ... };类型必须是整型int、unsigned int、signed int、char等或枚举类型。位数指定该成员占用的二进制位数不能超过类型本身的位数例如int通常为 32 位则位数不能超过 32。示例定义一个表示 RGB565 颜色的位段#includestdio.hstructRGB565{unsignedintred:5;// 红色分量占 5 位unsignedintgreen:6;// 绿色分量占 6 位unsignedintblue:5;// 蓝色分量占 5 位};intmain(){structRGB565color;color.red31;// 5 位最大值2^5 - 1 31color.green63;// 6 位最大值2^6 - 1 63color.blue31;printf(RGB565 颜色值R%u, G%u, B%u\n,color.red,color.green,color.blue);printf(结构体大小%zu 字节\n,sizeof(structRGB565));// 通常为 2 字节16 位return0;}4.2 位段的内存分配位段的内存分配遵循以下规则位段的成员可以是int、unsigned int、signed int或者是char等整型类型。位段的空间上是按照需要以 4 个字节int或者 1 个字节char的方式来开辟的。编译器会为位段分配一个或多个“存储单元”当一个存储单元不足以容纳下一个位段时会开辟新的存储单元。位段涉及很多不确定因素例如位段成员在存储单元内的存放方向、是否允许跨存储单元等位段是不跨平台的注重可移植的程序应该避免使用位段。4.3 位段的注意事项位段的跨平台问题位段的可移植性差主要体现在以下几个方面int位段被当成有符号数还是无符号数是不确定的**。不同编译器可能对未显式声明signed或unsigned的int位段做不同解释。位段中最大位的数目不能确定。例如16 位机器上int位段最大为 16 位32 位机器上最大为 32 位。若将位段位数写成 27在 16 位机器上会出问题。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。不同编译器、不同平台可能采用不同的方向。当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。编译器可能选择将第二个位段放入新的存储单元也可能利用当前单元剩余位如果空间足够但跨边界规则允许。取地址操作不能对位段成员使用取地址运算符因为位段的几个成员共有同一个字节这样有些成员的起始位置并不是某个字节的起始位置那么这些位置处是没有地址的。那么就不能使⽤scanf直接给位段的成员输⼊值只能是先输⼊放在一个变量中然后赋值给位段的成员。struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { struct A sa { 0 }; scanf(%d, sa._b);//这是错误的 //正确的⽰范 int b 0; scanf(%d, b); sa._b b; return 0; }未命名位段可以定义没有名字的位段用于占位或填充以达到特定的内存对齐效果。structPadding{unsignedinta:4;unsignedint:4;// 未命名位段占 4 位用于填充unsignedintb:8;};零长度位段位段长度可以为 0表示强制下一个位段从新的存储单元开始。structForceAlign{unsignedinta:10;unsignedint:0;// 零长度位段强制 b 从下一个存储单元开始unsignedintb:10;};
返回列表