前言:
本篇系统梳理 C 语言结构体、联合体、位域、柔性数组全部核心知识点,从基础语法、内存对齐规则到嵌入式实战场景、面试高频真题全覆盖,搭配代码示例与易错坑点总结,适合零基础入门、知识点复盘与面试突击复习。
目录
[1. 结构体的定义与初始化](#1. 结构体的定义与初始化)
[2. 结构体成员访问](#2. 结构体成员访问)
[3. 结构体传参:传值 vs 传址](#3. 结构体传参:传值 vs 传址)
[4. typedef 简化结构体类型](#4. typedef 简化结构体类型)
[5. 嵌套结构体](#5. 嵌套结构体)
[1. 为什么需要内存对齐](#1. 为什么需要内存对齐)
[2. 四大对齐核心规则](#2. 四大对齐核心规则)
[3. 结构体大小计算三步法](#3. 结构体大小计算三步法)
[4. 手动控制对齐:#pragma pack](#pragma pack)
[5. 成员顺序优化技巧](#5. 成员顺序优化技巧)
[1. 联合体的定义与内存特性](#1. 联合体的定义与内存特性)
[2. 联合体大小计算](#2. 联合体大小计算)
[3. 经典应用场景](#3. 经典应用场景)
[4. 注意事项与风险](#4. 注意事项与风险)
[1. 位域的定义与基本用法](#1. 位域的定义与基本用法)
[2. 位域的大小与存储规则](#2. 位域的大小与存储规则)
[3. 嵌入式典型应用:寄存器封装](#3. 嵌入式典型应用:寄存器封装)
[4. 可移植性与使用限制](#4. 可移植性与使用限制)
[1. 柔性数组的定义语法](#1. 柔性数组的定义语法)
[2. 柔性数组的使用方法](#2. 柔性数组的使用方法)
[3. 对比指针方案的优势](#3. 对比指针方案的优势)
[4. 适用场景](#4. 适用场景)
[1. 经典大小计算题](#1. 经典大小计算题)
[2. 高频面试问答](#2. 高频面试问答)
[3. 常见易错坑点](#3. 常见易错坑点)
一、结构体基础全解
结构体是 C 语言中自定义的复合数据类型,可以将多个不同类型的变量打包在一起,用来描述一个对象的多个属性,是数据封装的基础手段。
1. 结构体的定义与初始化
// 定义结构体类型
struct Student {
char name[20];
int age;
float score;
};
int main() {
// 方式1:定义时按顺序初始化
struct Student s1 = {"张三", 20, 92.5f};
// 方式2:指定成员初始化(C99支持,顺序任意)
struct Student s2 = {
.age = 21,
.name = "李四",
.score = 88.0f
};
return 0;
}
思路:struct 关键字定义结构体类型,内部包含多个不同类型的成员;初始化支持顺序赋值和指定成员赋值两种方式。
2. 结构体成员访问
int main() {
struct Student s = {"王五", 19, 95.0f};
// 结构体变量用 . 访问成员
printf("%s %d\n", s.name, s.age);
s.score = 96.0f;
// 结构体指针用 -> 访问成员
struct Student *p = &s;
printf("%f\n", p->score); // 等价于 (*p).score
return 0;
}
思路:普通结构体变量用
.访问成员;结构体指针用->访问成员,本质是解引用后访问的语法糖。
3. 结构体传参:传值 vs 传址
// 传值:传递结构体副本,函数内修改不影响外部
void printByVal(struct Student s) {
printf("%s %d\n", s.name, s.age);
}
// 传址:传递地址,直接修改原结构体,节省内存
void updateByAddr(struct Student *s, int newAge) {
s->age = newAge;
}
核心对比:
- 传值:安全不修改原数据,但结构体较大时拷贝开销大,占用栈空间多
- 传址:效率高、可修改原数据,推荐大结构体优先使用,需注意判空
4. typedef 简化结构体类型
// 定义同时起别名,使用时无需再加struct关键字
typedef struct {
char name[20];
int age;
} Student;
int main() {
Student s = {"赵六", 22, 90.0f}; // 直接用类型名定义
return 0;
}
思路:typedef 给结构体类型起别名,简化代码书写,是工程开发中的通用写法。
5. 嵌套结构体
typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
char name[20];
Date birthday; // 结构体嵌套
} Person;
int main() {
Person p = {"小明", {2004, 5, 10}};
printf("%d年\n", p.birthday.year);
return 0;
}
思路:结构体的成员可以是另一个结构体类型,访问时需要逐层用
.访问。
二、结构体内存对齐(核心面试考点)
内存对齐是结构体最核心的考点,也是绝大多数初学者的易错点,直接决定结构体的实际内存占用。
1. 为什么需要内存对齐
- 提升访问效率:CPU 按固定字长读取内存,对齐后的数据一次即可读完,非对齐需要多次读取拼接
- 硬件兼容性:部分硬件架构(如 ARM)不支持非对齐内存访问,直接触发硬件异常
- 标准统一:编译器按固定规则自动填充字节,保证不同平台下的访问行为一致
2. 四大对齐核心规则
- 起始偏移:结构体第一个成员的偏移量为 0
- 成员对齐:其余每个成员的偏移量,必须是「该成员大小」与「默认对齐数」中较小值的整数倍
- 整体对齐:结构体总大小,必须是内部最大对齐数的整数倍
- 嵌套对齐:嵌套结构体的对齐数,是该嵌套结构体内部的最大对齐数
补充:Windows 默认对齐数为 8,Linux 默认对齐数为 4,32 位 / 64 位系统不改变默认对齐数规则。
3. 结构体大小计算三步法
示例 1:基础结构体大小计算
struct S1 {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
计算步骤:
- a 从偏移 0 开始,占 1 字节,偏移范围 0
- b 对齐数为 min (4, 默认 8)=4,偏移必须是 4 的倍数,填充 3 字节,从偏移 4 开始,占 4 字节,偏移范围 4-7
- c 对齐数为 1,直接从偏移 8 开始,占 1 字节,偏移范围 8
- 最大对齐数为 4,总大小必须是 4 的倍数,填充 3 字节,最终总大小 = 12 字节
示例 2:嵌套结构体大小计算
struct S2 {
char a;
struct S1 s; // 嵌套S1,最大对齐数是4
double d; // 8字节
};
计算步骤:
- a 占偏移 0,共 1 字节
- 嵌套结构体 s 对齐数为 4,偏移到 4 开始,占 12 字节,偏移范围 4-15
- d 对齐数为 8,偏移必须是 8 的倍数,当前 16 符合,占 8 字节,偏移范围 16-23
- 最大对齐数为 8,总大小 24 是 8 的倍数,最终总大小 = 24 字节
4. 手动控制对齐:#pragma pack
#pragma pack(2) // 设置默认对齐数为2
struct S3 {
char a;
int b;
char c;
};
#pragma pack() // 恢复默认对齐数
- 计算结果:对齐数改为 2 后,b 从偏移 2 开始,总大小 = 2+4+1=7,补 1 对齐到 2 的倍数,最终大小为 8 字节。
- 适用场景:嵌入式通信协议、网络数据包打包,需要严格控制结构体大小,避免填充字节。
5. 成员顺序优化技巧
将相同大小的成员集中排列、小成员放在一起,可以减少对齐填充,节省内存。
// 写法1:大小穿插,浪费空间(总大小12)
struct Bad {
char a;
int b;
char c;
};
// 写法2:小成员集中,节省空间(总大小8)
struct Good {
char a;
char c;
int b;
};
三、联合体(共用体)详解
联合体也叫共用体,所有成员共享同一块内存空间,同一时间只能存储一个成员的值,是极致节省内存的语法。
1. 联合体的定义与内存特性
union Data {
int i;
char c;
float f;
};
int main() {
union Data d;
d.i = 0x12345678;
printf("%x\n", d.c); // 读取低字节内容
return 0;
}
核心特性:
- 所有成员共用同一块起始地址的内存
- 同一时间只有一个成员有效,修改一个成员会覆盖其他成员
- 联合体大小至少能容纳最大的成员,同时满足对齐要求
2. 联合体大小计算
union U1 {
char arr[5]; // 5字节
int i; // 4字节
};
计算:最大成员是 5 字节的数组,最大对齐数是 4,总大小必须是 4 的倍数,最终大小为 8 字节。
3. 经典应用场景
场景 1:判断系统大小端
int isLittleEndian() {
union {
int a;
char b;
} u;
u.a = 1;
return u.b; // 小端返回1,大端返回0
}
思路:利用联合体共享内存,给 int 赋值 1,读取 char 首字节,判断低字节是否存在低地址。
场景 2:通信协议字段复用
嵌入式 / 网络开发中,同一数据包不同场景下承载不同类型数据,用联合体可以在固定大小内存中复用空间,节省传输开销。
4. 注意事项与风险
- 不能同时使用多个成员,写入一个成员后读取另一个属于类型双关,结果由内存布局决定
- 可移植性受限,大小端、对齐规则不同会导致内存布局差异
- 不能用联合体做类型强制转换,属于未定义行为
四、位域(位段)详解
位域允许将一个字节拆分成多个二进制位来使用,专门用来节省内存,在嵌入式寄存器操作中极为常用。
1. 位域的定义与基本用法
// 用1个字节表示3个状态位
struct Status {
unsigned int run : 1; // 占1位,0/1
unsigned int error : 1; // 占1位
unsigned int mode : 2; // 占2位,0-3
};
int main() {
struct Status s;
s.run = 1;
s.mode = 2;
printf("%zu\n", sizeof(s)); // 整体占4字节(unsigned int大小)
return 0;
}
- 语法:成员名:位数,表示该成员占用几个二进制位。
- 要求:位域的类型必须是 unsigned int、signed int 或 char 等整型。
2. 位域的大小与存储规则
-
一个位域必须存储在同一个存储单元内,不能跨两个单元
-
剩余位数放不下下一个位域时,会从下一个存储单元开始
-
可以用无名位域、宽度为 0 的位域强制对齐到下一个存储单元
struct Demo {
unsigned char a : 4;
unsigned char : 0; // 强制跳到下一字节
unsigned char b : 4;
};
// 总大小为2字节,a和b分属不同字节
3. 嵌入式典型应用:寄存器封装
// 封装32位控制寄存器的各个位
typedef union {
unsigned int reg;
struct {
unsigned int en : 1; // 使能位
unsigned int clk : 2; // 时钟分频
unsigned int mode : 3; // 工作模式
unsigned int : 26; // 保留位
} bit;
} CtrlReg;
思路:联合体 + 位域组合,既可以整体操作整个寄存器,也可以单独修改某一位,代码可读性远高于直接位运算。
4. 可移植性与使用限制
- 位域的内存分配顺序(从高位到低位还是低位到高位)由编译器决定,不同平台结果不同
- 跨平台传输的协议不建议直接用位域,避免大小端、分配顺序导致的解析错误
- 位域不能取地址,因为地址最小单位是字节,无法定位到单个二进制位
五、柔性数组详解
柔性数组是 C99 引入的特性,允许结构体最后一个成员是大小未知的数组,配合动态内存使用,实现可变长的结构体。
1. 柔性数组的定义语法
typedef struct {
int len;
int data[]; // 柔性数组成员,放在最后,不指定大小
} FlexArray;
核心规则:
- 柔性数组必须是结构体的最后一个成员
- 结构体中至少还有一个其他成员
- sizeof 计算结构体大小时,不包含柔性数组的内存
2. 柔性数组的使用方法
int main() {
// 申请结构体+10个int数组的总内存
FlexArray *p = (FlexArray*)malloc(sizeof(FlexArray) + sizeof(int)*10);
p->len = 10;
for (int i = 0; i < 10; i++) {
p->data[i] = i;
}
// 扩容:realloc调整数组大小
FlexArray *tmp = (FlexArray*)realloc(p, sizeof(FlexArray) + sizeof(int)*20);
if (tmp != NULL) {
p = tmp;
p->len = 20;
}
free(p);
p = NULL;
return 0;
}
3. 对比指针方案的优势
指针方案写法
typedef struct {
int len;
int *data;
} PointArray;
柔性数组相比指针方案的三大优势:
- 内存连续:结构体和数组在同一块连续内存,只需要一次 malloc/free,管理简单,内存碎片更少
- 访问效率高:连续内存缓存友好,访问速度更快
- 释放方便:释放结构体时一次 free 即可,不会遗漏释放指针成员,避免内存泄漏
4. 适用场景
- 可变长度的数据包、消息体
- 动态大小的数组封装
- 需要连续内存的高性能场景
六、面试高频真题与易错坑点
1. 经典大小计算题
题 1:32 位系统下,以下结构体大小是多少?
struct A {
char a;
short b;
int c;
char d;
};
答案:12 字节。a 偏移 0 占 1,补 1 字节对齐 short,b 偏移 2 占 2,c 偏移 4 占 4,d 偏移 8 占 1,补 3 字节整体对齐到 4 的倍数,总大小 12。
题 2:联合体大小是多少?
union U {
char a[7];
int b;
double c;
};
答案:8 字节。最大成员是 8 字节的 double,整体对齐数为 8,7 字节补到 8 字节。
2. 高频面试问答
Q1:结构体和联合体的区别?
答:
- 内存布局:结构体每个成员独立存储;联合体所有成员共享同一块内存
- 大小计算:结构体大小是所有成员大小 + 对齐填充之和;联合体大小是最大成员大小 + 对齐
- 使用方式:结构体可同时使用所有成员;联合体同一时间只有一个成员有效
- 用途:结构体用来封装对象多个属性;联合体用来节省内存、多场景复用空间
Q2:结构体可以直接赋值吗?
答:同类型的结构体可以直接用
=赋值,编译器会做内存拷贝,等价于 memcpy。但注意如果结构体内部有指针,只会拷贝指针地址,属于浅拷贝,释放时要小心重复释放问题。
Q3:柔性数组和指针数组成员的区别?
答:柔性数组不占用结构体大小,和结构体内存连续,一次申请一次释放;指针成员占用指针大小(4/8 字节),内存不连续,需要单独申请和释放。
3. 常见易错坑点
- 结构体传值开销大:大结构体尽量传指针,避免栈溢出和拷贝开销
- 对齐忽略:计算结构体大小只算成员总和,忽略对齐填充字节
- 联合体误用:同时读写多个成员,误以为多个成员独立存储
- 位域跨平台:直接用位域做网络 / 跨设备数据传输,出现解析错乱
- 柔性数组位置错:把柔性数组放在结构体中间,或前面没有其他成员
以上就是 C 语言结构体、联合体、位域、柔性数组的全部核心内容,是 C 语言进阶和嵌入式开发的必备知识点,也是面试笔试的高频考点。
制作不易,如果对你有用,希望能点赞收藏支持一下。