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;// ... 更多成员};struct:C 语言关键字,表示开始定义一个结构体类型。- 结构体标签:给这个结构体类型起的名字,用于后续引用。例如
Student、Point。 {}:大括号内是成员列表,定义了结构体包含哪些数据。;: 结构体声明末尾的分号不能省略。
例如,声明一个表示学生的结构体类型
structStudent{charname[20];// 姓名,字符数组成员intage;// 年龄,整型成员floatscore;// 成绩,浮点型成员};// 注意分号这里我们声明了一个名为struct Student的新类型。它包含三个成员:name、age、score。
2.2 结构体变量的创建和初始化
声明了结构体类型后,就可以用它来创建变量(称为结构体变量)并为其赋值(初始化)。
#include<stdio.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={.age=18,.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,...;由于没有标签,这种类型只能在声明的同时定义变量,之后无法再用这个类型去创建新的变量。
#include<stdio.h>intmain(){// 声明一个匿名结构体类型,并同时定义两个变量 point1, point2struct{intx;inty;}point1,point2;// 为变量赋值point1.x=10;point1.y=20;point2.x=30;point2.y=40;printf("point1: (%d, %d)\n",point1.x,point1.y);printf("point2: (%d, %d)\n",point2.x,point2.y);return0;}匿名结构体常与typedef关键字结合,为其创建一个别名,从而变相地“命名”这个类型,使其可以重复使用。
#include<stdio.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 中默认的值为 8,Linux 中 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 字节),效率更高。
- 修改原数据:如果函数需要修改调用者传入的结构体成员,必须通过指针传递。值传递只是操作副本,对原结构体没有影响。
#include<stdio.h>structStudent{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->age=25;// 通过指针修改原结构体的成员}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 颜色的位段
#include<stdio.h>structRGB565{unsignedintred:5;// 红色分量,占 5 位unsignedintgreen:6;// 绿色分量,占 6 位unsignedintblue:5;// 蓝色分量,占 5 位};intmain(){structRGB565color;color.red=31;// 5 位最大值:2^5 - 1 = 31color.green=63;// 6 位最大值:2^6 - 1 = 63color.blue=31;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;};