【小曾的C语言复习笔记01】结构体、联合体、枚举一次讲透,内存对齐原来没那么难

发布时间:2026/11/5 9:14:27
【小曾的C语言复习笔记01】结构体、联合体、枚举一次讲透,内存对齐原来没那么难

小曾同学 · 25软件工程

原创 · C语言打卡笔记 第1篇(复盘整理版)

🌱

哈喽我是小曾。这个系列是我C语言阶段学习笔记的复盘整理版,把当时零零散散的打卡重新梳理成体系化的文章。第1篇,聊聊C语言的三大自定义类型:结构体、联合体、枚举。

📖 文章目录

  • 1. 结构体 struct:把散数据捏成一个整体
  • 2. 内存对齐:为什么 sizeof 结果总比你想象的大?
  • 3. 位段:把内存抠到比特级
  • 4. 联合体 union:一块内存,多种解读
  • 5. 枚举 enum:#define 的优雅升级版
  • 6. 小结 & 思考题

01结构体 struct:把散数据捏成一个整体

C语言的内置类型(int、char、double……)只能描述单一数据,但现实中一个"学生"有姓名、年龄、成绩好几个属性。结构体就是用来把不同类型的数据打包成一个新的复合类型:

#include <stdio.h> #include <string.h> struct Student { char name[20]; int age; double score; }; int main() { struct Student s; strcpy(s.name, "小明"); s.age = 18; s.score = 95.5; printf("%s %d %.1f\n", s.name, s.age, s.score); return 0; }

结构体指针访问成员用->,这是后面数据结构(链表、树)的基本功:

struct Student* p = &s; p->age = 19; // 等价于 (*p).age = 19; printf("%d\n", p->age);

02内存对齐:为什么 sizeof 结果总比你想象的大?

学结构体绕不开的一个坑:sizeof 算出来的大小,经常不是成员大小简单相加。看例子:

struct S1 { char c1; // 1字节 int i; // 4字节 char c2; // 1字节 }; // 直观感觉:1 + 4 + 1 = 6 字节 // 实际 sizeof(struct S1) = 12 字节!

2.1 对齐规则(三条背下来)

  1. 第一个成员放在偏移量 0 处
  2. 其他成员放在自己对齐数的整数倍偏移处(对齐数 = min(成员大小, 默认对齐数8))
  3. 结构体总大小必须是最大对齐数的整数倍,不够就填充

套到S1上画个内存图:

偏移: 0 1-3(浪费) 4-7 8 9-11(浪费) 内容: c1 填充 i c2 尾部填充 1字节 3字节 4字节 1字节 3字节 总计 = 12 字节(12 是最大对齐数 4 的整数倍 ✅)

2.2 为什么要对齐?

  • 平台原因:某些硬件平台只能在特定地址读取特定类型,不对齐直接报错
  • 性能原因:对齐后 CPU 一次就能取完数据,不用拼接两次读取的结果,本质是拿空间换时间

2.3 自定义对齐数 #pragma pack

默认对齐数(VS下是8)可以改,网络编程、嵌入式里经常用:

#pragma pack(1) // 按1字节对齐,等于取消对齐 struct S2 { char c1; int i; char c2; }; // sizeof = 6,没有填充了 #pragma pack() // 恢复默认

💡省内存技巧:同样的成员,把小的放一起、大的放一起,可以减少填充浪费。char,char,int占 8 字节,而char,int,char占 12 字节。

03位段:把内存抠到比特级

如果一个成员只需要表示 0~3,用 int 存太浪费(4字节=32位,只用2位)。位段(bit-field)允许我们按比特位来分配成员空间:

struct Flags { unsigned int a : 2; // a 只占 2 个比特位 unsigned int b : 5; // b 占 5 个比特位 unsigned int c : 10; // c 占 10 个比特位 }; // sizeof 通常 = 4(一个 unsigned int 单元内挤下)

位段的典型用途:网络协议的数据包头部。比如 TCP 报文头里各种标志位,用位段可以精确控制每个字段占几位,减轻数据传输时的内存负担,提升效率。

⚠️位段的两个大坑:
1.跨平台不确定:位段成员在内存中从左往右还是从右往左分配,标准没有规定,不同编译器行为可能不同,跨平台代码慎用
2.不能用取地址符 &:位段成员的起始地址可能不是整字节,"地址"这个概念对它不成立,所以scanf("%d", &flags.a)这种写法是错的

04联合体 union:一块内存,多种解读

联合体(也叫共用体)的所有成员共用同一块内存,这块内存的大小按最大成员来开辟。同一时间只能有效使用其中一个成员:

union U { int i; char c; }; int main() { union U u; u.i = 0x11223344; printf("%x\n", u.c); // 打印 44(小端机器上) // 改了 i,c 看到的是同一块内存的低字节 return 0; }

4.1 经典应用:判断电脑大小端

这是我在学习时觉得最巧妙的用法,面试也常考:

int check_endian() { union { int i; char c; } u; u.i = 1; // 小端:01 00 00 00,低地址存 01,u.c = 1 // 大端:00 00 00 01,低地址存 00,u.c = 0 return u.c; // 返回1是小端,返回0是大端 }

原理就是联合体成员共享内存:i和c看的是同一块空间的同一段字节,只是解读方式不同。

05枚举 enum:#define 的优雅升级版

要表示"星期一到星期日",你可以写 7 个 #define,但枚举明显更优雅:

// ❌ #define 方式 #define MON 1 #define TUE 2 // ... 写7遍,松散无关联 // ✅ 枚举方式 enum Weekday { MON = 1, // 可指定初值 TUE, // 自动 = 2 WED, THU, FRI, SAT, SUN // 依次递增到 7 };

我当时的理解是:枚举就是 #define 的规模化包装运用。但它比 #define 强在哪?

对比项#defineenum 枚举
类型检查无,纯文本替换有,是类型
调试可见性预编译后被替换,调试看不见名字调试时能看到枚举名
批量定义一行一个自动递增
作用域全局生效易污染遵循作用域规则

06小结 & 思考题

📌 本篇总结

  1. 结构体:打包多种类型数据;内存对齐三规则要背,本质是空间换时间
  2. #pragma pack(n):自定义对齐数,省内存场景用
  3. 位段:比特级分配,省空间但跨平台不确定、不能取地址
  4. 联合体:成员共享一块内存,经典应用是判断大小端
  5. 枚举:#define 的升级版,有类型、可调试、自动递增

🤔 思考题

1. 计算 sizeof:在 64 位 VS 环境(默认对齐数8)下
struct Test { double d; char c; int i; };的大小是多少?
2. 把成员顺序改成char c; int i; double d;,大小变成多少?
3. 用联合体写一个函数判断大小端,并在 main 里验证你的机器是哪种。

建议大家先自己动手算一算、敲一敲,参考答案我会置顶到评论区,记得来对答案~

📅 下一篇预告

结构体搞懂了,下一篇聊C语言最"刺激"的部分——动态内存管理:malloc/calloc/realloc/free 四兄弟,还有让无数新手栽跟头的野指针问题,以及结构体里藏着的"柔性数组"黑科技。

C语言 结构体 内存对齐 联合体 枚举 学习笔记

✍️ 本文由「小曾同学」原创,25软件工程 · C语言打卡笔记复盘版

🌱 系列:小曾的C语言打卡笔记 —— 把零散笔记沉淀成体系

觉得有用就点个赞 👍 关注小曾,下一篇动态内存管理见~