文章目录
- 前言
- 一、结构体类型的声明
- 1.1 什么是结构体?
- 1.2 结构体的声明
- 1.3 结构体变量的创建和初始化
- 1.4 使用 typedef 简化结构体名称
- 1.5 结构体的自引用
- 二、结构成员的访问操作符
- 2.1 使用点号 `.` 直接访问
- 2.2 使用箭头 `->` 通过指针访问
- 三、结构体内存对齐
- 3.1 为什么需要内存对齐?
- 3.2 内存对齐的规则
- 3.3 修改默认对齐数
- 四、结构体传参
- 4.1 值传递
- 4.2 指针传递
- 五、结构体实现位段
- 5.1 什么是位段?
- 5.2 位段的声明方法
- 5.3 位段的内存分配
- 5.4 位段的跨平台问题
- 5.5 位段的使用注意事项
- 总结
前言
在 C 语言中,我们常用到一种叫做结构体(struct)的数据类型。
结构体就像一个小盒子,你可以把不同类型的数据(数字、字符串等)放进这个盒子中,这样便于管理和传递数据。
例如,一个学生的信息可以包含学号、姓名和成绩,把这些放到一个结构体中,就很方便了。
本文将一步步介绍:
- 结构体的声明和使用
- 如何创建和初始化结构体变量
- 如何访问结构体中的数据
- 内存对齐的概念和原因
- 结构体传参的方法
- 位段的作用及使用注意事项
即使你没有编程基础,也能看懂每个步骤。
一、结构体类型的声明
1.1 什么是结构体?
结构体就是一个自定义的数据类型,用来将不同类型的数据组合在一起。
举个例子:我们想表示一个学生的信息,可能包括学号、姓名和成绩。
1.2 结构体的声明
在 C 语言中,声明结构体的格式如下:
struct 结构体名 {成员类型 成员名;// 可以有多个成员,每个成员之间用分号隔开
};
示例:定义一个表示学生的结构体
// 定义一个结构体类型,名字叫 Student
struct Student {int id; // 学号,整型数字char name[20]; // 姓名,字符数组(字符串)float score; // 成绩,浮点数(可以有小数)
};
这段代码定义了一个结构体类型 Student
,它有三个成员:
id
:用来存学号name
:用来存姓名(最多20个字符)score
:用来存成绩
1.3 结构体变量的创建和初始化
定义好结构体类型后,我们可以创建这种类型的变量。
创建变量的方法和普通变量类似,例如:
// 创建一个 Student 类型的结构体变量 stu1
struct Student stu1;
如果想在创建的同时给成员赋初值,可以使用初始化列表:
// 按照成员顺序初始化,依次赋值给 id、name、score
struct Student stu2 = { 1001, "Alice", 88.5 };// 或者使用“指定成员”的方式初始化(顺序可以不同)
struct Student stu3 = { .name = "Bob", .score = 92.0, .id = 1002 };
这样,stu2
中的 id
就是 1001,name
是 “Alice”,score
是 88.5。
1.4 使用 typedef 简化结构体名称
为了让结构体的使用更加方便,我们常常使用 typedef
给结构体取个别名。
例如:
// 定义结构体并取别名 Person
typedef struct {char name[20];int age;
} Person;// 现在可以直接用 Person 来声明变量
Person p1 = { "Charlie", 25 };
这样以后声明变量时就不用写 struct Person
了,直接用 Person
即可。
1.5 结构体的自引用
有时我们需要构造“链表”等数据结构,此时需要结构体中包含指向自身类型的指针。
注意:不能直接在结构体中嵌入结构体本身,否则会无限递归,导致大小无法确定。
正确的做法是使用指针,例如:
// 定义链表节点的结构体
typedef struct Node {int data; // 数据struct Node *next; // 指向下一个节点的指针
} Node;
在这个例子中,每个 Node
包含一个整型数据和一个指向下一个 Node
的指针,从而可以构成链表。
二、结构成员的访问操作符
当我们定义好结构体变量后,需要访问其中的成员。有两种常用的方式:
2.1 使用点号 .
直接访问
对于结构体变量,使用点号 .
来访问其中的成员。例如:
// 假设我们有一个 Student 类型的变量 stu1
stu1.id = 1001; // 给学号赋值
printf("Name: %s\n", stu1.name); // 打印姓名
2.2 使用箭头 ->
通过指针访问
如果我们有一个指向结构体的指针,就用箭头 ->
来访问成员。例如:
// 定义一个指向 Student 结构体的指针
struct Student *ptr = &stu1;// 通过指针访问成员
ptr->score = 95.5; // 修改成绩
printf("ID: %d\n", ptr->id); // 打印学号
箭头操作符是 (*ptr).成员
的简写,写起来更方便。
三、结构体内存对齐
当 CPU 访问数据时,为了提高效率,通常要求数据按照特定的边界存储,这就是内存对齐。
简单来说,内存对齐就是让数据存储在“整齐”的地址上。
3.1 为什么需要内存对齐?
- 硬件要求:有的计算机硬件要求数据必须存放在某个地址边界上,否则会产生错误。
- 提高效率:对齐后的数据可以让 CPU 一次性读取完整数据,速度更快。如果数据没有对齐,可能需要多次读取。
3.2 内存对齐的规则
假设在 32 位系统中:
char
类型占 1 个字节int
类型占 4 个字节
下面举个例子说明对齐的概念:
struct S1 {char c1;int i;char c2;
};
可能的内存布局如下:
- c1:占 1 个字节,从地址 0 开始。
- 填充:为了让后面的
i
对齐到 4 的倍数,编译器在c1
后面自动添加 3 个填充字节。 - i:占 4 个字节,从地址 4 开始。
- c2:占 1 个字节,紧接在
i
后面存放,地址 8。 - 尾部填充:为了让整个结构体的大小是最大的对齐数(这里是 4)的整数倍,后面可能填充 3 个字节。
这样整个结构体的大小就是 12 个字节,而不是简单的 1+4+1=6 个字节。
3.3 修改默认对齐数
如果你想让结构体更紧凑,可以使用 #pragma pack(n)
指令来改变默认对齐数。例如,将对齐设置为 1 字节:
#pragma pack(1) // 设置对齐数为 1 字节
struct Test {char a;int b;char c;
};
#pragma pack() // 恢复默认对齐// 现在 Test 结构体的大小为 1 + 4 + 1 = 6 字节(没有填充)
注意:降低对齐数可能会影响程序运行速度,因为数据访问可能不再高效。
四、结构体传参
在函数调用时,我们可以将结构体作为参数传递。主要有两种方式:
4.1 值传递
直接传递结构体变量的值。
这种方式会把整个结构体的内容复制一份传递给函数,如果结构体很大,复制的过程会占用更多时间和内存。
void printStudent(struct Student stu) {printf("ID: %d, Name: %s\n", stu.id, stu.name);
}int main() {struct Student stu = { 1001, "Alice", 88.5 };printStudent(stu); // 将 stu 的一份副本传入函数return 0;
}
4.2 指针传递
传递结构体的地址,只复制一个指针。
这种方式效率更高,并且在函数中修改结构体内容会影响到原变量。
void modifyStudent(struct Student *stu) {// 使用箭头操作符访问成员stu->score = 100;
}int main() {struct Student stu = { 1002, "Bob", 92.0 };modifyStudent(&stu); // 传递 stu 的地址printf("New score: %.1f\n", stu.score); // 输出 100.0return 0;
}
推荐在传递大结构体时使用指针传递。
五、结构体实现位段
5.1 什么是位段?
有时我们只需要一个数据的几个二进制位,而不是整个字节。
位段(bit-field)就是用来指定一个结构体成员占用多少个二进制位。
位段常用于硬件编程、网络协议等对内存要求非常严格的场合。
5.2 位段的声明方法
位段的声明语法与结构体类似,只不过在成员名后面加上冒号和位数:
struct Flags {unsigned int flag1 : 1; // 只占 1 位(0 或 1)unsigned int flag2 : 3; // 占 3 位(可表示 0~7)unsigned int flag3 : 4; // 占 4 位(可表示 0~15)
};
这里,flag1
只需要 1 个比特位,flag2
用 3 个比特位,flag3
用 4 个比特位。
总共占用的位数为 1 + 3 + 4 = 8 位,正好 1 个字节。
5.3 位段的内存分配
- 同一存储单元:相邻的位段成员如果属于相同的类型且剩余位足够,会被存放在同一个存储单元中(通常是 1 个、2 个或 4 个字节)。
- 不足则分新单元:如果剩余空间不足以存放下一个位段,则会从下一个存储单元开始存放。
例如:
struct Bits {char a : 3;char b : 4;char c : 5;char d : 4;
};
在某些编译器下,a
和 b
可能会存放在同一字节中;如果一字节不够存放 c
的 5 位,则 c
可能从下一个字节开始,d
同理。
注意:不同编译器可能在位段存储细节上略有差异。
5.4 位段的跨平台问题
由于位段的存储和对齐规则依赖于编译器和平台,可能会出现以下问题:
- 存储顺序:某些编译器可能将位段从左到右存储,有的则相反;
- 符号问题:位段默认是有符号还是无符号可能不同;
- 填充方式:当位段不足时,编译器如何填充可能不同。
因此,使用位段时要特别注意代码的可移植性。
5.5 位段的使用注意事项
- 不能取地址:由于位段可能并不占满一个完整的字节,不能对位段成员使用取地址符
&
。这意味着你不能获取位段成员的指针。 - 赋值范围:给位段赋值时,要确保数值不超过其位数能表示的范围,否则可能出现错误或数据截断。
- 跨平台谨慎使用:如果你的代码需要在多个平台上运行,请慎重使用位段,因为它们的具体存储方式可能会有所不同。
总结
本文详细介绍了 C 语言中结构体的知识点,内容包括:
- 结构体的声明与使用:如何定义结构体、创建结构体变量、使用
typedef
简化类型以及如何实现结构体的自引用。 - 结构体成员的访问:介绍了使用点号
.
和箭头->
两种方法访问结构体成员。 - 内存对齐:解释了为什么需要内存对齐、内存对齐的规则以及如何使用
#pragma pack
调整对齐方式,让数据在内存中存放得更加“整齐”。 - 结构体传参:讲解了值传递和指针传递两种方式,并指出大结构体使用指针传递可以提高效率。
- 位段:介绍了位段的基本概念、声明方法、内存分配方式及使用中的注意事项。
希望这篇博客能帮助你对 C 语言中的结构体及相关概念有一个全面而清晰的认识。如果有任何问题或疑问,欢迎在评论区留言讨论,一起进步!