【C语言】自定义数据类型——结构体

张开发
2026/4/8 23:57:14 15 分钟阅读

分享文章

【C语言】自定义数据类型——结构体
目录一、引入结构体二、结构体的定义、创建与初始化1.定义2.创建①定义后直接创建②先定义再创建③使用匿名结构体不推荐因为无法再次使用该类型④创建结构体数组3.初始化①按成员顺序初始化②按照任意顺序初始化③先创建再逐个赋值三、结构体成员的访问1.使用.访问2.使用-访问3.结构体成员嵌套的访问四、结构体作为参数传递1.传值2.传地址五、typedef简化结构体六、结构体的自引用七、结构体内存对齐问题节省时间1.什么是内存对齐2.需要使用内存对齐的原因3.内存对齐的规则4.举例5.小结6.控制对齐方式八、结构体实现位段操作节省空间1.位域简介与使用原因2.位域的定义与使用时的注意事项3.举例4.工程应用一、引入结构体C语言已经给我们提供了不少内置数据类型例如 int、char、float 等但是如果我们需要描述由姓名、年龄、性别、身高、体重等等等等多种数据类型复合而成的“人”这样一个对象时在C语言中找不到现成的这样的数据类型。但是C语言给我们提供了创造数据类型的东西——结构体结构体struct是C语言中一种非常重要的用户自定义数据类型它允许我们将不同类型的数据组合在一起形成一个逻辑上的整体。这对于表示复杂对象如学生、汽车、坐标点等非常方便。二、结构体的定义、创建与初始化1.定义使用struct关键字来定义结构体。定义结构体时需要列出其包含的成员member每个成员可以是任意基本数据类型、数组、指针甚至其他结构体。/******************************* 语法 *******************************/ struct 结构体标签 { 类型 成员1; 类型 成员2; ... }; // 分号不能省略 /******************************* 示例 *******************************/ struct Student { int id; // 学号 char name[50]; // 姓名 float score; // 成绩 }; //这里 Student 是结构体标签tag它本身不是变量名而是一种新的数据类型。2.创建定义好结构体后就可以用它来创建变量了。有三种方式①定义后直接创建struct Student { int id; char name[50]; float score; } stu1, stu2; // 在定义类型后直接创建变量②先定义再创建struct Student { int id; // 学号 char name[50]; // 姓名 float score; // 成绩 }; int main() { struct Student stu1, stu2; // 创建了两个Student类型的变量 return 0; }③使用匿名结构体不推荐因为无法再次使用该类型struct { int id; char name[50]; float score; } stu1, stu2; // struct后面没有标签后续的程序只能使用stu1和stu2这两个数据④创建结构体数组结构体数组用于存储多个相同类型的结构体对象struct Student { int id; // 学号 char name[50]; // 姓名 float score; // 成绩 }; int main() { //创建结构体数组数组的变量类型是结构体类型有三个成员变量每个变量都是一个结构体类型 struct Student class[3] { {1001, 张三, 88.5}, {1002, 李四, 92.0}, {1003, 王五, 76.5} }; //数组元素的访问 for (int i 0; i 3; i) { printf(%d\t%s\t%.1f\n, class[i].id, class[i].name, class[i].score); } return 0; }3.初始化未显式初始化的成员会被自动赋值为 0对于数值类型或空字符串对于字符数组初始化可以在创建变量时进行有多种初始化方法①按成员顺序初始化注意必须按照结构体定义中成员的顺序依次给出初始值且值的类型要匹配。struct Student stu1 {1001, 张三, 88.5};②按照任意顺序初始化可以按照任意顺序初始化使用.成员名来指定。struct Student stu2 {.name 李四, .id 1002, .score 92.0};③先创建再逐个赋值struct Student stu3; stu3.id 1003; strcpy(stu3.name, 王五); // 注意不能直接用 给数组赋值 stu3.score 76.5;三、结构体成员的访问1.使用.访问printf(学号%d\n, stu1.id); printf(姓名%s\n, stu1.name); printf(成绩%.1f\n, stu1.score);2.使用-访问如果使用指向结构体的指针则需要用箭头运算符-来访问成员或者先解引用再用点号。struct Student *p stu1; printf(学号%d\n, p-id); // 等价于 (*p).id3.结构体成员嵌套的访问struct Date { int year; int month; int day; }; struct Student { int id; char name[50]; float score; struct Date birthday; // 嵌套结构体 }; int main() { struct Student stu {1001, 张三, 88.5, {2000, 5, 20}}; printf(生日%d年%d月%d日\n, stu.birthday.year, stu.birthday.month, stu.birthday.day); return 0; }四、结构体作为参数传递1.传值将整个结构体作为参数传递函数会得到一份拷贝修改拷贝不影响原结构体。使用 . 进行成员访问缺点如果结构体很大拷贝开销大。void printStudent(struct Student s) { printf(%d %s %.1f\n, s.id, s.name, s.score); } // 调用printStudent(stu);2.传地址传递结构体指针函数内通过指针访问原始数据避免拷贝且能修改原结构体。使用 - 进行成员访问推荐使用指针尤其是结构体较大时。函数传参的时候参数是需要压栈会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候结构体过大参数压栈的系统开销比较大所以会导致性能的下降。void modifyScore(struct Student *s, float newScore) { printf(%d %s %.1f\n, s-id, s-name, s-score); s-score newScore; // 直接修改原变量 } // 调用modifyScore(stu, 99.0);五、typedef简化结构体使用typedef可以为结构体定义一个别名这样在声明变量时就不必每次都写struct关键字。/***************************************** 第一种 *****************************************/ typedef struct Student { int id; char name[50]; float score; } Stu; // Stu 现在是 struct Student 的别名 /***************************************** 第二种 *****************************************/ /* 或者使用匿名结构体在定义时直接使用typedef */ typedef struct { int id; char name[50]; float score; } Stu; // 匿名结构体直接定义别名Student int main() { Stu stu1, stu2; return 0; }六、结构体的自引用/*************************** 错误的自引用 ***************************/ struct Node { int data; struct Node next; }; //⼀个结构体中再包含⼀个同类型的结构体变量这样结构体变量的大小将是无穷大 /*************************** 正确的自引用 ***************************/ struct Node { int data; struct Node* next; };/*************************** 错误的自引用 ***************************/ typedef struct { int data; Node* next; //虽然使用typedef重命名但是内部成员变量还没有接收到 }Node; /*************************** 正确的自引用 ***************************/ typedef struct Node { int data; struct Node* next; }Node;七、结构体内存对齐问题节省时间1.什么是内存对齐内存对齐Memory Alignment是指编译器将结构体的每个成员放置在内存中特定位置的过程以提高CPU访问内存的效率。它会在成员之间插入“填充字节”padding使得每个成员的起始地址满足一定的对齐要求。2.需要使用内存对齐的原因硬件要求许多CPU架构如RISC、x86要求特定类型的数据必须存放在特定地址上否则会引发硬件异常如总线错误或性能急剧下降。例如32位CPU通常要求int类型的数据地址是4的倍数。访问效率即使某些CPU允许非对齐访问如x86但访问对齐的数据通常更快可能一次内存访问即可完成而非对齐可能需要两次内存访问并拼接数据。原子性保证对齐的数据可以保证原子操作在某些平台上非对齐可能导致操作被拆分。3.内存对齐的规则确定起点结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处确定其他成员对齐位置其他成员变量要对齐到对齐数的整数倍的地址处对齐数编译器默认的⼀个对齐数与该成员变量大小的较小值。VS 编辑器中默认的对齐数为 8Linux中gcc没有默认对齐数对齐数就是成员自身的大小确定总大小结构体总大小为最大对齐数结构体中每个成员变量都有一个对齐数所有对齐数中最大的的整数倍嵌套问题如果嵌套了结构体的情况嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体中成员的对齐数的整数倍。4.举例5.小结在设计结构体的时候我们既要满足对齐又要节省空间那么就要让占用空间小的成员尽量集中在⼀起6.控制对齐方式在某些场景如网络协议、硬件寄存器、文件格式我们希望结构体紧密排列不插入填充字节。这时可以使用编译器扩展来“取消”对齐#pragma pack(x) struct PackedExample { char c; int i; short s; }; #pragma pack()八、结构体实现位段操作节省空间1.位域简介与使用原因位域允许我们将结构体成员按位bit分配空间适用于需要精确控制位数、节省内存的场景如硬件寄存器、网络协议头等2.位域的定义与使用时的注意事项定义在成员后加冒号和位数struct Flags { unsigned int a : 1; // a 占1位 unsigned int b : 2; // b 占2位 unsigned int c : 3; // c 占3位 };注意事项位域成员必须是无符号或有符号整数类型int、unsigned int等。不能对位域成员取地址。struct A { 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; }位域的总位数不能超过底层类型的位数通常为unsigned int的位数如32位。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用标准尚未定义。3.举例4.工程应用网络协议中IP数据报的格式我们可以看到其中很多的属性只需要几个bit位就能描述这里使用位段能够实现想要的效果也节省了空间这样网络传输的数据报大小也会较小一些对网络的畅通是有帮助的。

更多文章