之前的学习中,我们已经接触了 C 语言中最基础的自定义类型 ------ 结构体。这篇文章将带你深入理解 C 语言中其他自定义类型,并且深入剖析它们在内存中的存储原理。
一、结构体
1.1 什么是结构体
结构体(Structure)是一组不同类型数据的集合 ,这些数据被称为结构体的成员变量(Member Variables)。
与数组不同,结构体可以把不同类型的数据封装到一起,这样就可以用结构体描述一个完整的对象。
举个例子来看:当我们要描述一个学生的时候,他的信息包含姓名(字符串)、身高(浮点数或整数)、年龄(整型)、成绩(浮点数),这些数据类型各不相同,我们就可以用结构体进行封装。
1.2 结构体类型声明
结构体类型的声明如下:
cs
struct 结构体名
{
成员类型1 成员名1;
成员类型2 成员名2;
// ... 更多成员
}变量名1, 变量名2, 变量名3;
注意:结构体声明后必须加分号,表示结构体声明结束!
举个例子看一下:

1.3 结构体变量的定义与初始化
1.3.1 结构体变量的定义
(1)先声明类型 再定义变量

(2)声明类型 同时定义变量

注意:s1,stu10定义在结构体声明的末尾是全局变量
(3)匿名结构体定义
见名知意,匿名结构体就是没有名字的结构体。
语法格式:
cs
struct
{
成员类型1 成员名1;
成员类型2 成员名2;
// ...
} 变量名1, 变量名2;
注意:匿名结构体没有"名字",编译器没办法通过名字识别它,所以它只能在声明的同时定义变量。这也就说明,此结构体类型只能使用一次。(使用时一定注意!)

到这里,我想问屏幕前的你们一个问题:匿名结构体不能通过名字被找到,那我们是不是可以将它的地址传给一个指针变量,通过这个指针变量来访问它?
答案:匿名结构体可以在声明的同时定义指针变量,通过地址访问成员,但无法在声明结束后,再定义新的指针或变量来引用它。
举个例子说明:


1.3.2 结构体变量的初始化
(1)成员顺序初始化
按照结构体成员的顺序赋值

(2)指定成员初始化(乱序初始化)

1.4 结构体成员访问
访问结构体成员必须使用.操作符
如果是指针 则用->操作符
1.5 结构体的自引用
1.5.1 什么是结构体的自引用
结构体里包含一个指向自身类型的指针,它是链表、树等数据结构的核心基础。
这里引入一个知识点给大家讲解:
数据结构:数据结构是指数据在内存中的存储结构,例如:线性表(顺序表、链表)
(1)如果我们想存储12345这五个数字,首先我们想到要建立一个数组来存放这五个数据,这五个数据在内存中是连续存放的,这就称为顺序表。
(2)如果这五个数据在内存中随机存放我们要如何找到它们?
C语言中要如何描述这个节点?这就用到了结构体的自引用
语法格式:
cs
struct Node
{
int data; //数据域
struct Node* next; //指针域
};
注意:不能直接包含结构体自身变量(struct Node next;),这样程序并不知道到底有多少个变量,程序将会陷入死循环。
1.5.2 结构体自引用的使用
cs
#include <stdio.h>
// 定义一个结构体,里面包含一个指向自身类型的指针
struct Node
{
// 数据域:存放数据
int data;
// 指针域:存下一个节点的地址 (结构体的自引用)
struct Node* next;
};
int main()
{
// 1. 创建 3 个节点
struct Node n1, n2, n3;
// 2. 给每个节点的数据域赋值
n1.data = 1;
n2.data = 2;
n3.data = 3;
// 3. 用 next 指针把节点"串起来"
n1.next = &n2; // n1 的下一个是 n2
n2.next = &n3; // n2 的下一个是 n3
n3.next = NULL; // 最后一个节点没有下一个,指向空
// 4. 遍历链表:从头节点开始,一个一个往后找
struct Node* p = &n1; // p 先指向第一个节点
while (p != NULL)
{
// 打印当前节点的数据
printf("节点数据: %d\n", p->data);
// 移动到下一个节点
p = p->next;
}
return 0;
}
1.5.3 typedef修饰结构体(类型重命名)
typedef用于对已定义的类型进行重命名,目的是简化代码,尤其适用于自定义类型。
语法格式:
cs
typedef struct Node
{
int data;
struct Node *next;
} Node;
int main()
{
Node n; // 直接用Node声明变量(=struct Node)
return 0;
}
注意!!!!!错误示例!!!!!

二、结构体的大小------内存对齐
在C语言中结构体大小时以内存对齐方式进行计算
2.1 内存对齐规则
(1)结构体的第一个成员要对齐到结构体变量起始位置位零的地址处
(2)其他变量成员要对齐到对齐数的整数倍地址处
编译器取对齐数为默认对齐数与该成员变量的较小值(对齐数取决于编译器,例如VS中默认对齐数为8)。
(3)结构体总大小为最大对成员的整数倍
举例说明:


2.2 修改默认对齐数
#pragrom pack 这个预处理指令可以改变编译器的默认对齐数
语法格式:
cs
// 设置默认对齐数为 n(n 通常取2的次方数(2\4\6\8) 等
#pragma pack(n)
// 恢复编译器默认对齐数
#pragma pack()
使用举例:


三、结构体传参
3.1 传值调用

3.2 传址调用

四、结构体实现位段
4.1 什么是位段
位段 是 一种按 "位(bit)" 分配内存 的特殊结构体,用来极度节省空间。
普通结构体是按字节 分配内存,位段是按二进制位分配内存。
C语言中为什么使用位段
位段与结构体相似
- 类型限制 :只能用
int / unsigned int / signed int / char等整型(不能用 float、double) - 冒号 + 数字 :数字表示该成员占用多少个二进制位
- 内存分配 :按位分配,不是按字节,极大节省空间
- 内存对齐 :位段会按存储单元(int 4 字节、char 1 字节)打包,不够存就新开单元
语法格式:
cs
struct 位段名
{
类型 成员名 : 占用的位数;
};
4.2 位段的大小
位段大小的计算
(1)位段按bit位来分配内存,根据位段成员的类型开辟空间( int(32bits)/ char (8bits) )
(2)将变量按位存储,不够时再开辟新的成员的类型空间
举例讲解:




换个例子来看:
用上面的讲解方法
struct Test3 {
int a:31;
int b:1;
int c:1;
};
- 顺序填充/反向分配:
a(31bit)+b(1bit) 刚好填满第一个int,c放第二个int→ 总大小 8 字节 - 整块跳过:
a用了 31bit,剩 1bit,b直接新开单元,c再新开单元 → 总大小变成3×4=12字节
4.3 位段的注意事项
(1)由位段的大小讲解可知,位段的具体分配细节是由编译器决定的,C 标准没有强制规定,所 以跨平台使用时要注意兼容性。
(2)
int a:32char a:32❌ (char 只有 8bit)
(3)位段是按二进制位来分配空间的(内存中每个字节有独立的地址,比特位没有),所以对位段不能用&取地址符,不能用scanf直接给位段输入值。
正确输入方法:先把输入值读到一个临时变量中,再通过赋值操作把值赋给位段成员


