
🏠个人主页:黎雁
🎬作者简介:C/C++/JAVA后端开发学习者
❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划
✨ 从来绝巘须孤往,万里同尘即玉京

文章目录
- 【C语言自定义类型】结构体全解析:声明/内存对齐/位段(含实战案例)📦
-
- [前景回顾:数据存储核心速记 📝](#前景回顾:数据存储核心速记 📝)
- [一、结构体基础:类型声明与核心特性 📝](#一、结构体基础:类型声明与核心特性 📝)
-
- [1. 结构体的声明](#1. 结构体的声明)
- [2. 特殊声明:匿名结构体(仅用一次)](#2. 特殊声明:匿名结构体(仅用一次))
- [3. 高频考点:结构体的自引用(实现链表核心)](#3. 高频考点:结构体的自引用(实现链表核心))
- [二、结构体成员访问:. 和 -> 操作符详解 🔍](#二、结构体成员访问:. 和 -> 操作符详解 🔍)
-
- [1. 错误案例:传值调用无法修改实参](#1. 错误案例:传值调用无法修改实参)
- [2. 正确方式:传址调用(高效修改实参)](#2. 正确方式:传址调用(高效修改实参))
- [三、高频考点:结构体内存对齐(笔试必问) 📏](#三、高频考点:结构体内存对齐(笔试必问) 📏)
-
- [1. 内存对齐规则(必须牢记)](#1. 内存对齐规则(必须牢记))
- [2. 用offsetof宏验证偏移量](#2. 用offsetof宏验证偏移量)
- [3. 为什么需要内存对齐?(空间换时间)](#3. 为什么需要内存对齐?(空间换时间))
- 4. 修改默认对齐数(#pragma指令)
- [四、结构体传参:效率优化的关键 ✨](#四、结构体传参:效率优化的关键 ✨)
- [五、结构体进阶:位段(节省空间的利器) 🪀](#五、结构体进阶:位段(节省空间的利器) 🪀)
-
- [1. 位段的声明规则](#1. 位段的声明规则)
- [2. 位段的空间优势](#2. 位段的空间优势)
- [3. 位段的不确定性(跨平台问题)](#3. 位段的不确定性(跨平台问题))
- [4. 位段的使用注意事项](#4. 位段的使用注意事项)
- [写在最后 📝](#写在最后 📝)
【C语言自定义类型】结构体全解析:声明/内存对齐/位段(含实战案例)📦
告别基础数据类型,我们正式进入C语言自定义类型的核心------结构体!结构体是描述复杂数据的关键工具,小到存储学生信息,大到实现链表、网络协议,都离不开它。这一篇我们用表格+案例的形式,从结构体的声明、成员访问,到高频考点内存对齐、传参优化,再到进阶的位段实现,全方位拆解,帮你彻底掌握结构体的使用与底层逻辑!
前景回顾:数据存储核心速记 📝
回顾数据存储的核心知识点,能帮我们更好理解结构体的内存布局:
- 不同数据类型(char/int/double)占用的内存大小不同,且存在对齐要求。
- 多字节数据存在大小端存储差异,会影响结构体成员的内存读取。
- 指针存储的是内存地址,占用固定大小(32位平台4字节,64位平台8字节),这是结构体自引用的基础。
一、结构体基础:类型声明与核心特性 📝
结构体是"不同类型数据的集合",和数组(同类型数据集合)形成鲜明对比,用表格对比更清晰:
| 特性 | 结构体 | 数组 |
|---|---|---|
| 成员类型 | 可以不同 | 必须相同 |
| 核心作用 | 描述复杂对象(如学生) | 存储同类型有序数据 |
| 访问方式 | 通过成员名(. / ->) | 通过下标索引 |
| 大小计算 | 遵循内存对齐规则 | 元素大小 × 元素个数 |
1. 结构体的声明
最常规的声明方式(带类型名):
c
// 声明结构体类型struct Stu
struct Stu
{
char name[20]; // 姓名
int age; // 年龄
double score; // 成绩
}; // 分号不能丢!
// 创建结构体变量(三种方式)
struct Stu s1; // 声明后单独创建
struct Stu s2 = {"lisi", 18, 95.5}; // 创建时初始化
struct Stu s3[3]; // 创建结构体数组
2. 特殊声明:匿名结构体(仅用一次)
如果结构体只需要使用一次,可以省略类型名,直接创建变量,这就是匿名结构体:
c
#include <stdio.h>
// 匿名结构体类型,直接创建变量A
struct
{
int a;
char b;
float c;
}A;
// 另一个匿名结构体类型,创建指针ps
struct
{
int a;
char b;
float c;
}*ps;
int main()
{
ps = &A; // 错误!编译器判定两个匿名结构体是不同类型
return 0;
}
💡 关键提醒:匿名结构体仅能使用一次,即使成员完全相同,编译器也会视为不同类型,无法相互赋值或赋值给指针。
3. 高频考点:结构体的自引用(实现链表核心)
结构体自引用是数据结构的基础,用于实现链表、树等线性/非线性结构。核心是用指针自引用,而非直接包含同类型结构体变量。我们用表格对比正确与错误写法:
| 写法类型 | 代码示例 | 正误判断 | 核心原因 |
|---|---|---|---|
| 直接包含变量 | struct Node{int data; struct Node n;}; |
❌ 错误 | 结构体包含自身变量,导致无限递归,大小无法计算 |
| 包含自身指针 | struct Node{int data; struct Node* next;}; |
✅ 正确 | 指针仅存储地址,大小固定,不会引发递归 |
| typedef+指针 | typedef struct Node{int data; struct Node* next;}Node; |
✅ 正确 | 先声明struct Node,内部用指针,最后重命名 |
| typedef+提前用Node | typedef struct{int data; Node* next;}Node; |
❌ 错误 | Node重命名在结构体声明后生效,内部无法识别 |
二、结构体成员访问:. 和 -> 操作符详解 🔍
访问结构体成员有两种核心方式,根据访问对象是"结构体变量"还是"结构体指针"选择,用表格总结用法:
| 操作符 | 使用场景 | 语法格式 | 等价写法 |
|---|---|---|---|
. |
访问结构体变量的成员 | 结构体变量.成员名 |
- |
-> |
访问结构体指针指向的成员 | 结构体指针->成员名 |
(*结构体指针).成员名 |
1. 错误案例:传值调用无法修改实参
很多新手会踩"传值调用修改结构体"的坑,我们通过代码演示:
c
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double score;
};
// 传值调用:形参ss是实参s的临时拷贝
void set_stu(struct Stu ss)
{
strcpy(ss.name, "zhangsan"); // 修改的是拷贝的成员
ss.age = 20;
ss.score = 100.0;
}
void print_stu(struct Stu ss)
{
printf("%s %d %lf\n", ss.name, ss.age, ss.score);
}
int main()
{
struct Stu s = {0}; // 初始化为0
set_stu(s); // 传值调用,实参s未被修改
print_stu(s); // 输出:(空) 0 0.000000
return 0;
}
❌ 原因:传值调用时,形参是实参的一份临时拷贝,对形参的修改不会影响实参本身。
2. 正确方式:传址调用(高效修改实参)
想要修改实参的结构体成员,必须传递结构体的地址(传址调用):
c
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
double score;
};
// 传址调用:形参ps指向实参s的地址
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "zhangsan"); // 等价于 (*ps).name
ps->age = 20; // 等价于 (*ps).age
ps->score = 100.0; // 等价于 (*ps).score
}
void print_stu(struct Stu ss)
{
printf("%s %d %lf\n", ss.name, ss.age, ss.score);
}
int main()
{
struct Stu s = {0};
set_stu(&s); // 传递实参地址
print_stu(s); // 输出:zhangsan 20 100.000000
return 0;
}
✅ 核心优势:传址调用仅传递4/8字节的地址,避免了结构体拷贝的空间和时间开销,效率更高。
三、高频考点:结构体内存对齐(笔试必问) 📏
结构体的大小不是成员大小的简单相加,而是遵循内存对齐规则计算的,这是笔试的热门考点。我们先看一个反常识的例子:
c
#include <stdio.h>
struct S1
{
char c1;
char c2;
int n;
};
struct S2
{
char c1;
int n;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct S1)); // 输出:8
printf("%zd\n", sizeof(struct S2)); // 输出:12
return 0;
}
为什么成员类型相同,顺序不同,大小就不一样?答案就是内存对齐!
1. 内存对齐规则(必须牢记)
| 规则序号 | 规则内容 | 补充说明 |
|---|---|---|
| 1 | 结构体第一个成员,对齐到结构体起始地址(偏移量为0) | 无例外,所有结构体都遵循 |
| 2 | 其他成员对齐到"对齐数"的整数倍地址处 | 对齐数 = min(编译器默认对齐数, 成员自身大小);VS默认8,GCC无默认对齐数 |
| 3 | 结构体总大小,是所有成员"最大对齐数"的整数倍 | 不足的部分会自动填充字节 |
| 4 | 嵌套结构体时,嵌套的结构体对齐到自身内部最大对齐数的整数倍 | 总大小是外层+内层最大对齐数的整数倍 |
2. 用offsetof宏验证偏移量
offsetof宏(需包含<stddef.h>)可计算成员相对于结构体起始位置的偏移量,帮我们直观理解内存布局。以struct S2为例:
c
#include <stdio.h>
#include <stddef.h> // offsetof宏的头文件
struct S2
{
char c1;
int n;
char c2;
};
int main()
{
printf("c1的偏移量:%d\n", offsetof(struct S2, c1)); // 0
printf("n的偏移量:%d\n", offsetof(struct S2, n)); // 4(对齐到4的整数倍)
printf("c2的偏移量:%d\n", offsetof(struct S2, c2)); // 8(对齐到4的整数倍)
return 0;
}
📊 S2的内存布局(VS环境,默认对齐数8):
| 偏移量区间 | 0 | 1-3 | 4-7 | 8 | 9-11 |
|---|---|---|---|---|---|
| 存储内容 | c1 | 填充字节 | n | c2 | 填充字节 |
| 占用大小 | 1 | 3 | 4 | 1 | 3 |
总大小12,是最大对齐数4的整数倍。
3. 为什么需要内存对齐?(空间换时间)
| 原因类型 | 具体说明 |
|---|---|
| 平台兼容性 | 某些硬件平台只能在特定地址(如4的整数倍)读取特定类型数据,否则抛出硬件异常 |
| 提升访问效率 | 未对齐的内存需要处理器访问两次,对齐的内存仅需一次。例如int成员在偏移量1处,需读两次内存再拼接 |
4. 修改默认对齐数(#pragma指令)
可通过#pragma pack(n)修改默认对齐数,n为新的对齐数;#pragma pack()还原默认:
c
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1(取消对齐,紧凑存储)
struct S2
{
char c1;
int n;
char c2;
};
#pragma pack() // 还原默认对齐数
int main()
{
printf("%zd\n", sizeof(struct S2)); // 输出:6(1+4+1)
return 0;
}
四、结构体传参:效率优化的关键 ✨
结构体传参有两种方式:传值调用和传址调用,我们用表格对比两者差异:
| 传参方式 | 语法格式 | 空间开销 | 效率 | 能否修改实参 |
|---|---|---|---|---|
| 传值调用 | 函数名(结构体变量) |
大(拷贝整个结构体) | 低 | ❌ 不能 |
| 传址调用 | 函数名(&结构体变量) |
小(仅拷贝地址) | 高 | ✅ 可以(加const可禁止修改) |
代码演示对比:
c
#include <stdio.h>
struct S
{
int data[1000]; // 4000字节
int num; // 4字节
};
// 传值调用:拷贝整个结构体(4004字节)
void Print1(struct S t)
{
for (int i = 0; i < 5; i++)
printf("%d ", t.data[i]);
printf("%d\n", t.num);
}
// 传址调用:仅拷贝地址(4/8字节)
void Print2(const struct S* ps) // const保护,避免误修改
{
for (int i = 0; i < 5; i++)
printf("%d ", ps->data[i]);
printf("%d\n", ps->num);
}
int main()
{
struct S s = {{1,2,3,4,5}, 100};
Print1(s); // 效率低,浪费空间
Print2(&s); // 效率高,推荐使用
return 0;
}
💡 核心结论:结构体传参时,优先选择传址调用 ,既节省空间,又提升效率;若无需修改结构体,建议用const修饰指针,避免误操作。
五、结构体进阶:位段(节省空间的利器) 🪀
位段是结构体的"紧凑版",通过指定成员占用的二进制位数,实现内存的精准分配,常用于需要节省空间的场景(如网络协议、嵌入式开发)。
1. 位段的声明规则
位段声明和结构体类似,但有两个核心差异,用表格总结:
| 对比项 | 普通结构体 | 位段 |
|---|---|---|
| 成员类型限制 | 无限制 | 必须是int/unsigned int/signed int(C99支持char) |
| 成员语法 | 类型 成员名; |
类型 成员名 : 位数; |
| 空间分配 | 按成员大小+对齐规则分配 | 按bit位分配,可共用字节 |
| 跨平台性 | 好 | 差(存在多种不确定因素) |
2. 位段的空间优势
对比普通结构体和位段的大小:
c
#include <stdio.h>
// 普通结构体:4个int,共16字节
struct A1
{
int _a;
int _b;
int _c;
int _d;
};
// 位段:总占用47位→8字节(按4字节/8字节开辟)
struct A2 // 位段
{
int _a : 2; // 2位
int _b : 5; // 5位
int _c : 10; // 10位
int _d : 30; // 30位
};
int main()
{
printf("A1大小:%zd\n", sizeof(struct A1)); // 16
printf("A2大小:%zd\n", sizeof(struct A2)); // 8
return 0;
}
✅ 效果:位段仅用8字节,就实现了普通结构体16字节的功能,空间节省50%!
3. 位段的不确定性(跨平台问题)
| 不确定点 | 具体表现 | 影响 |
|---|---|---|
| 位段存储方向 | 从左向右或从右向左存储 | 不同编译器读取结果不同 |
| 剩余空间处理 | 成员超出剩余位时,是否舍弃剩余空间 | 影响内存利用率 |
| int位段符号性 | 默认是有符号还是无符号 | 影响负数存储 |
| 最大位数限制 | 16位机器最大16位,32位机器最大32位 | 不同平台位数上限不同 |
4. 位段的使用注意事项
位段成员共用一个字节,没有独立地址,因此:
- 不能用
&取位段成员的地址; - 不能直接用
scanf给位段成员输入,需先输入到普通变量再赋值。
c
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
};
int main()
{
struct A sa = {0};
// scanf("%d", &sa._b); // 错误:无法取位段成员地址
int b = 0;
scanf("%d", &b);
sa._b = b; // 正确:间接赋值
return 0;
}
写在最后 📝
结构体是C语言描述复杂数据的核心工具,也是连接基础语法和数据结构的桥梁。掌握结构体的关键在于三点:
- 理解声明与自引用的逻辑,尤其是指针在自引用中的作用;
- 牢记内存对齐规则,能独立计算结构体大小(笔试高频);
- 分清传值与传址调用的差异,优先选择高效的传址调用;
- 位段是节省空间的利器,但要注意跨平台问题。
这些知识点不仅是日常开发的必备技能,更是笔面试的核心考点。建议大家把文中的案例(尤其是内存对齐和自引用)手动敲一遍,观察运行结果,加深对底层逻辑的理解。下一篇我们将讲解自定义类型的其他成员------枚举和联合,继续拓展C语言的编程边界!