前言
在C语言的世界里,我们不仅有 int、char 等基础数据类型,更有强大的自定义复合类型来应对复杂的编程场景。结构体、联合体与枚举正是其中的核心,它们让我们能够更精准地描述现实世界中的实体、更高效地利用内存,同时让代码更具可读性和可维护性。本文将从底层原理到实战应用,全方位拆解这三大自定义类型的使用场景与核心要点,为你打通从基础语法到数据结构的进阶之路。
一、结构体类型的声明
结构是不同类型值的集合,这些值被称为结构体的成员变量,每个成员可以是任意基本类型或自定义类型。
1.1 标准声明格式
cs
struct 结构体标签名(tag)
{
成员类型 成员名1; // 成员列表
成员类型 成员名2;
...
}结构体变量列表(可选); // 分号不可省略,标志声明结束
核心说明:
• tag:结构体标签,用于标识结构体类型,后续创建变量时需通过struct tag调用
• 成员列表:可包含任意基本类型/自定义类型,成员名不可重复
• 变量列表:声明时可直接创建全局变量,可选写
示例:描述学生实体
cs
// 声明学生结构体类型,全局作用域
struct Stu
{
char name[20]; // 名字:字符数组(固定长度,避免指针悬空)
int age; // 年龄:整型
char sex[5]; // 性别:字符数组
char id[20]; // 学号:字符数组
}; // 分号必须写,不可遗漏
1.2 特殊声明:匿名结构体
声明结构体时省略标签名(tag),即为匿名结构体,适用于仅需一次性使用的场景。
cs
// 匿名结构体:无tag,直接创建变量x
struct
{
int a;
char b;
float c;
}x;
// 匿名结构体:创建数组a和指针p
struct
{
int a;
char b;
float c;
}a[20], *p;
⚠️ 重要注意事项:
编译器会将成员列表相同的匿名结构体视为不同类型,因此以下操作非法:
p = &x; // 错误:p和x的结构体类型不同,编译器报错
使用场景:仅当结构体无需复用、仅需创建一次变量时使用,否则建议使用标准声明。
1.3 结构体的自引用
结构体中包含自身类型的成员称为自引用,常用于实现链表、树等数据结构,必须使用指针实现,不可直接使用结构体变量。
错误示例:直接使用结构体变量
cs
struct Node
{
int data; // 数据域
struct Node next; // 错误:自身结构体变量,导致内存大小无限递归
};
原因:结构体大小计算时,next作为自身类型变量,会再次包含next,形成无限嵌套,编译器无法确定结构体大小。
正确示例:使用自身类型指针
cs
struct Node
{
int data; // 数据域:存储节点数据
struct Node* next; // 指针域:指向同类型下一个节点,指针大小固定(4/8字节)
};
原因:指针是一种基本类型,大小固定(32位系统4字节,64位系统8字节),编译器可正常计算结构体大小。
结合typedef的自引用(易踩坑)
typedef用于给结构体重命名,自引用时不可提前使用重命名后的类型,需先通过struct tag声明。
错误示例
cs
typedef struct
{
int data;
Node* next; // 错误:Node是重命名后的类型,此时尚未定义
}Node;
正确示例
cs
// 先声明struct Node,再重命名为Node
typedef struct Node
{
int data;
struct Node* next; // 先使用struct Node,编译器可识别
}Node;
二、结构体变量的创建和初始化
结构体变量的创建分为全局创建(声明时直接创建/全局作用域创建)和局部创建(函数内创建),初始化支持顺序初始化和指定成员初始化(C99特性)。
2.1 全局变量创建
声明结构体时直接在变量列表中定义,或在全局作用域单独定义:
cs
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}s1; // 方式1:声明时直接创建全局变量s1
struct Stu s2; // 方式2:全局作用域单独创建全局变量s2
2.2 局部变量创建+初始化(最常用)
在函数内创建结构体变量,同时完成初始化,附完整代码+注释:
cs
#include <stdio.h>
// 声明学生结构体类型
struct Stu
{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
};
int main()
{
// 方式1:顺序初始化(严格按照成员列表顺序)
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("===== 顺序初始化 =====\n");
printf("name: %s\n", s.name); // 结构体变量.成员名 访问成员
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
// 方式2:指定成员初始化(C99支持,顺序可任意)
struct Stu s2 = { .age = 18, .name = "李四", .id = "20230818002", .sex = "女" };
printf("\n===== 指定成员初始化 =====\n");
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
运行结果:
===== 顺序初始化 =====
name: 张三
age : 20
sex : 男
id : 20230818001
===== 指定成员初始化 =====
name: 李四
age : 18
sex : 女
id : 20230818002
核心语法:结构体变量.成员名 是访问结构体成员的基本方式,适用于直接操作结构体变量。
2.3 结构体嵌套初始化
若结构体成员为另一个结构体类型,需使用嵌套大括号完成初始化:
cs
#include <stdio.h>
// 声明地址结构体
struct Address
{
char city[20]; // 城市
char street[50];// 街道
};
// 声明学生结构体,嵌套Address结构体
struct Stu
{
char name[20];
int age;
struct Address addr; // 结构体成员
};
int main()
{
// 嵌套初始化:addr成员对应大括号内为Address的初始化值
struct Stu s = { "张三", 20, { "北京", "海淀区中关村大街" } };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("city : %s\n", s.addr.city); // 多层.访问嵌套成员
printf("street: %s\n", s.addr.street);
return 0;
}
运行结果:
name: 张三
age : 20
city : 北京
street: 海淀区中关村大街
三、结构体内存对齐(核心难点)
结构体内存对齐是C语言的底层内存布局规则,决定了结构体变量在内存中占用的字节数,直接影响内存利用率和程序运行效率,是面试高频考点。
3.1 内存对齐的四大规则
编译器遵循以下4条规则计算结构体大小(VS默认对齐数8,GCC无默认对齐数):
-
第一个成员:对齐到结构体变量起始地址偏移量为0的位置。
-
后续成员:对齐到自身对齐数的整数倍偏移量位置。
◦ 对齐数 = 编译器默认对齐数 和 成员自身大小 的较小值。
◦ VS默认对齐数为8,GCC无默认对齐数(对齐数=成员自身大小)。
-
结构体总大小:必须是所有成员最大对齐数的整数倍,不足则补空字节(填充)。
-
嵌套结构体:嵌套的结构体成员对齐到自身最大对齐数的整数倍位置,整体总大小为所有最大对齐数(含嵌套)的整数倍。
3.2 对齐数的核心概念
• 自身大小:基本类型的大小(char=1,int=4,double=8)。
• 默认对齐数:编译器预设值(VS=8,GCC无),可通过预处理指令修改。
• 最大对齐数:结构体所有成员的对齐数中的最大值,是总大小的"最小单位"。
3.3 实战计算:单结构体大小
结合规则,通过3个示例手把手计算结构体大小(基于VS环境,默认对齐数8)。
示例1:char + int + char
cs
struct S1
{
char c1; // 成员1:char,大小1,对齐数=min(8,1)=1
int i; // 成员2:int,大小4,对齐数=min(8,4)=4
char c2; // 成员3:char,大小1,对齐数=min(8,1)=1
};
printf("sizeof(S1) = %zu\n", sizeof(struct S1)); // 输出12
分步计算:
-
c1:偏移0,占用1字节(0~0)。
-
i:需对齐到4的整数倍,偏移4开始,占用4字节(4~7)。
-
c2:需对齐到1的整数倍,偏移8开始,占用1字节(8~8)。
-
总占用9字节,最大对齐数为4,需补3字节至12(4的3倍)。
最终大小:12字节(3个有效字节,9个填充字节)。
示例2:char + char + int
cs
struct S2
{
char c1; // 对齐数1,偏移0,占用1字节(0~0)
char c2; // 对齐数1,偏移1,占用1字节(1~1)
int i; // 对齐数4,偏移4,占用4字节(4~7)
};
printf("sizeof(S2) = %zu\n", sizeof(struct S2)); // 输出8
分步计算:
-
c1、c2连续存储,共占用2字节(0~1)。
-
i对齐到4的整数倍,偏移4开始,占用4字节(4~7)。
-
总占用8字节,是最大对齐数4的整数倍,无需填充。
最终大小:8字节(6个有效字节,2个填充字节)。
示例3:double + char + int
cs
struct S3
{
double d; // 对齐数min(8,8)=8,偏移0,占用8字节(0~7)
char c; // 对齐数1,偏移8,占用1字节(8~8)
int i; // 对齐数4,偏移12,占用4字节(12~15)
};
printf("sizeof(S3) = %zu\n", sizeof(struct S3)); // 输出16
分步计算:
-
d占用8字节(0~7),c占用1字节(8~8)。
-
i对齐到4的整数倍,偏移12开始(8+1=9,最近的4的倍数为12),占用4字节(12~15)。
-
总占用16字节,是最大对齐数8的整数倍,无需填充。
最终大小:16字节。
3.4 实战计算:嵌套结构体大小
cs
// 复用示例3的S3(大小16,最大对齐数8)
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1; // 对齐数1,偏移0,占用1字节(0~0)
struct S3 s3; // 嵌套结构体,最大对齐数8,偏移8,占用16字节(8~23)
double d; // 对齐数8,偏移24,占用8字节(24~31)
};
printf("sizeof(S4) = %zu\n", sizeof(struct S4)); // 输出32
分步计算:
-
c1占用1字节(0~0),s3对齐到自身最大对齐数8,偏移8开始,占用16字节(8~23)。
-
d对齐到8的整数倍,偏移24开始,占用8字节(24~31)。
-
总占用32字节,是最大对齐数8的整数倍,无需填充。
最终大小:32字节。
3.5 内存对齐的原因
为什么要存在内存对齐?核心原因是平台兼容性和性能优化,即拿空间换时间:
-
平台原因(移植性):部分硬件平台(如嵌入式)只能在特定地址处访问特定类型的数据,未对齐的内存访问会触发硬件异常。
-
性能原因(效率):处理器访问内存时,以字长为单位(32位处理器4字节,64位8字节)连续读取,未对齐的内存需要处理器分两次读取并拼接,对齐后可一次读取,大幅提升效率。
3.6 内存对齐的优化技巧
内存对齐的本质是空间换时间,但我们可以通过调整成员顺序减少填充字节,提高内存利用率:
• 原则:将占用空间小的成员集中在一起。
对比示例:
cs
// 差的顺序:char + int + char,大小12字节,浪费9字节
struct S1 { char c1; int i; char c2; };
// 优的顺序:char + char + int,大小8字节,浪费2字节
struct S2 { char c1; char c2; int i; };
结论:相同的成员,不同的顺序,结构体大小可能不同,开发中应按"小类型集中"的原则定义结构体。
3.7 修改默认对齐数
使用C语言预处理指令 #pragma pack() 可修改/恢复编译器默认对齐数,适用于对内存利用率要求极高的场景(如嵌入式开发)。
语法格式
cs
#pragma pack(n) // 设置默认对齐数为n(n通常为1/2/4/8/16)
// 结构体定义
#pragma pack() // 恢复编译器默认对齐数
实战示例:设置默认对齐数为1
cs
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1,取消所有对齐(对齐数=成员自身大小)
struct S
{
char c1; // 对齐数1,偏移0,占用1字节
int i; // 对齐数1,偏移1,占用4字节
char c2; // 对齐数1,偏移5,占用1字节
};
#pragma pack() // 恢复默认对齐数8
int main()
{
// 总大小=1+4+1=6,无填充字节
printf("sizeof(S) = %d\n", sizeof(struct S)); // 输出6
return 0;
}
说明:设置默认对齐数为1时,所有成员均无对齐要求,连续存储,内存利用率最高,但可能牺牲性能且降低移植性。
四、结构体传参
结构体传参有值传递和地址传递两种方式,二者的效率和使用场景差异巨大,开发中首选地址传递。
4.1 两种传参方式对比
附完整代码+注释,对比两种方式的差异(结构体包含大数组,突出效率问题):
cs
#include <stdio.h>
// 定义大结构体(含1000个int的数组,占用4000字节)
struct S
{
int data[1000]; // 数组成员:4*1000=4000字节
int num; // 整型成员:4字节
};
// 全局结构体变量,初始化
struct S s = { {1,2,3,4}, 1000 };
// 方式1:值传递 - 形参是结构体变量,接收实参的拷贝
void print1(struct S s)
{
printf("num = %d\n", s.num);
}
// 方式2:地址传递 - 形参是结构体指针,接收实参的地址
void print2(struct S* ps)
{
printf("num = %d\n", ps->num); // 指针访问成员:->
}
int main()
{
print1(s); // 值传递:传结构体变量本身
print2(&s); // 地址传递:传结构体地址
return 0;
}
运行结果:
num = 1000
num = 1000
核心语法:结构体指针访问成员的方式为 指针->成员名,与 (*指针).成员名 等价,是指针操作结构体的标准语法。
4.2 为什么首选地址传递?
核心原因是函数传参的压栈开销:
-
函数传参时,参数会被压入栈区,栈区的内存空间有限且压栈/出栈需要消耗系统资源。
-
值传递:形参是实参的完整拷贝,若结构体过大(如示例中的4004字节),拷贝会消耗大量内存和CPU资源,导致性能急剧下降。
-
地址传递:形参是结构体指针,仅占用4/8字节(32/64位系统),压栈开销极小,且通过地址可直接操作原结构体变量,无需拷贝。
4.3 补充:const修饰结构体指针
地址传递时,若仅需读取结构体成员、无需修改,建议用const修饰结构体指针,防止误修改原结构体变量,提升代码健壮性:
cs
// const修饰:ps指向的结构体内容不可修改,仅可读
void print2(const struct S* ps)
{
// ps->num = 2000; // 错误:const禁止修改
printf("num = %d\n", ps->num); // 正确:仅读取
}
开发规范:只读不写的结构体指针传参,必须加const修饰。
五、结构体实现位段(进阶用法)
位段是结构体的特殊形式,用于按比特位(bit)分配内存,能大幅节省内存空间,适用于对内存要求极高的场景(如网络协议、嵌入式寄存器配置)。
5.1 位段的定义规则
位段的声明与结构体类似,但需满足2条特殊规则:
-
位段的成员必须是整型家族(int、unsigned int、signed int、char等)。
-
成员名后加冒号+数字,数字表示该成员占用的比特位(bit)数。
5.2 位段的基本语法
cs
struct 位段名
{
整型类型 成员名1:比特数1;
整型类型 成员名2:比特数2;
...
};
示例:定义位段并计算大小
cs
#include <stdio.h>
// 位段定义:成员后带冒号+比特数
struct A
{
int _a:2; // _a占用2个bit
int _b:5; // _b占用5个bit
int _c:10; // _c占用10个bit
int _d:30; // _d占用30个bit
};
int main()
{
// 总比特数=2+5+10+30=47bit,按int(32bit)开辟空间
// 47bit需要2个int(64bit),即8字节
printf("sizeof(A) = %d\n", sizeof(struct A)); // 输出8
return 0;
}
5.3 位段的内存分配规则
位段的内存分配是按需求动态开辟的,遵循3条规则:
-
按基本单位开辟:成员为int则按4字节(32bit)开辟,为char则按1字节(8bit)开辟。
-
比特位连续分配:在一个基本单位内,位段成员连续存储,不足则开辟下一个基本单位。
-
无跨平台标准:比特位的存储顺序(左→右/右→左)、剩余比特位是否复用,由编译器决定(VS默认从右向左存储,舍弃剩余位)。
5.4 位段的内存布局实战
以VS环境为例,分析位段的实际内存存储(char类型,1字节=8bit,从右向左存储):
cs
#include <stdio.h>
struct S
{
char a:3; // 3bit
char b:4; // 4bit
char c:5; // 5bit
char d:4; // 4bit
};
int main()
{
struct S s = {0};
s.a = 10; // 10的二进制:1010 → 仅存低3bit:010(十进制2)
s.b = 12; // 12的二进制:1100 → 存4bit:1100
s.c = 3; // 3的二进制:11 → 存5bit:00011
s.d = 4; // 4的二进制:100 → 存4bit:0100
printf("sizeof(S) = %d\n", sizeof(struct S)); // 输出3
return 0;
}
内存分配分析(VS,小端存储,从右向左分配):
- 开辟第1个字节(8bit):存储a(3bit)+b(4bit),剩余1bit舍弃,共占用7bit。
◦ a=010(低3bit),b=1100(中间4bit),组合为:0 1100 010 → 十进制98(0x62)。
- 开辟第2个字节(8bit):存储c(5bit),剩余3bit舍弃。
◦ c=00011 → 填充3bit0,组合为:000 00011 → 十进制3(0x03)。
- 开辟第3个字节(8bit):存储d(4bit),剩余4bit舍弃。
◦ d=0100 → 填充4bit0,组合为:0000 0100 → 十进制4(0x04)。
最终内存:3字节,值为0x62、0x03、0x04,总大小3字节。
5.5 位段的跨平台问题
位段的内存分配依赖编译器实现,存在4大跨平台问题,注重移植性的程序应避免使用:
-
int位段的符号性:signed int位段的最高位为符号位,unsigned int无符号,编译器未统一规定。
-
最大比特数限制:16位机器int为2字节(16bit),位段成员比特数超过16会报错,32位机器无此问题。
-
存储顺序:比特位的存储顺序(左→右/右→左)由编译器决定,无标准。
-
剩余比特位复用:一个基本单位的剩余比特位,编译器可选择复用或舍弃,无标准。
5.6 位段的使用注意事项
位段的成员是按比特位分配的,无独立的内存地址,因此存在2个使用限制:
-
不能使用&取位段成员的地址,编译报错。
-
不能用scanf直接给位段成员输入值,需先输入到普通变量,再赋值。
错误示例:
cs
struct A
{
int _a:2;
int _b:5;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._a); // 错误:位段成员无地址,&操作符非法
return 0;
}
正确示例:
cs
struct A
{
int _a:2;
int _b:5;
};
int main()
{
struct A sa = {0};
int a = 0;
scanf("%d", &a); // 先输入到普通变量
sa._a = a; // 再赋值给位段成员
printf("_a = %d\n", sa._a);
return 0;
}
5.7 位段的应用场景
位段的核心优势是极致节省内存,主要应用于以下场景:
-
网络协议:IP报头、TCP报头的字段仅需几个bit,使用位段可大幅减小数据包大小,提升传输效率。
-
嵌入式开发:寄存器配置通常按bit操作,位段可直接映射寄存器的bit位,简化代码。
-
硬件编程:外设配置(如GPIO、UART)的控制位,多为bit级,位段可精准操作。
六、结构体总结
结构体是C语言自定义复合类型的核心,是实现数据封装和复杂数据结构的基础,本文核心知识点梳理:
-
声明与初始化:支持标准声明、匿名声明,初始化有顺序初始化、指定成员初始化、嵌套初始化,核心访问语法为变量.成员和指针->成员。
-
内存对齐:四大规则是核心,本质是空间换时间,可通过调整成员顺序和修改默认对齐数优化内存利用率,是面试高频考点。
-
结构体传参:首选地址传递,减少压栈开销,只读传参需加const修饰,提升代码健壮性。
-
位段:结构体的特殊形式,按bit分配内存,极致节省空间,但存在跨平台问题,适用于嵌入式、网络协议等对内存要求极高的场景。
结构体是C语言从"基础语法"到"数据结构"的过渡,掌握结构体的使用和底层原理,是后续学习链表、栈、队列等数据结构的关键。建议结合本文代码反复调试,深入理解内存对齐和位段的底层逻辑,做到知其然且知其所以然。
七、联合体
7.1 联合体类型的声明
联合体(共用体)是由一个或多个成员构成的自定义类型,成员可以是不同类型。
它的核心特点是所有成员共用同一块内存空间,因此也叫"共用体"。
编译器只为最大的成员分配足够的内存空间。
cs
#include <stdio.h>
// 声明联合体类型
union Un
{
char c; // 成员1:字符型,占1字节
int i; // 成员2:整型,占4字节
};
int main()
{
// 定义联合体变量并初始化为0
union Un un = {0};
// 计算联合体变量的大小
printf("%d\n", sizeof(un)); // 输出:4,因为最大成员是int(4字节)
return 0;
}
7.2 联合体的特点
• 所有成员共用同一块内存空间,所以一个联合体变量的大小至少是最大成员的大小。
• 给联合体中一个成员赋值,其他成员的值也会跟着变化。
验证成员共用内存
cs
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = {0};
// 打印三个地址,会发现完全相同
printf("%p\n", &(un.i)); // 成员i的地址
printf("%p\n", &(un.c)); // 成员c的地址
printf("%p\n", &un); // 联合体变量的地址
return 0;
}
验证赋值影响其他成员
cs
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = {0};
un.i = 0x11223344; // 给i赋值十六进制数
un.c = 0x55; // 修改c的值(会覆盖i的低字节)
printf("%x\n", un.i); // 输出:11223355,低字节被修改为55
return 0;
}
内存布局说明
在小端存储模式下,int i = 0x11223344 的内存布局为:
地址:0x001AF85C → 44
地址:0x001AF85D → 33
地址:0x001AF85E → 22
地址:0x001AF85F → 11
当执行 un.c = 0x55 时,会修改低字节(地址 0x001AF85C)的值为 55,所以 un.i 的值变为 0x11223355。
7.3 相同成员的结构体和联合体对比
|------------------------------|-------------------------------|-----|
| 类型 | 内存布局 | 大小 |
| struct S { char c; int i; }; | 成员 c 和 i 各自独立占用内存,c 后会填充3字节对齐 | 8字节 |
| union Un { char c; int i; }; | 成员 c 和 i 共用同一块内存 | 4字节 |
示例:
cs
// 结构体
struct S
{
char c;
int i;
};
struct S s = {0}; // 大小:8字节
// 联合体
union Un
{
char c;
int i;
};
union Un un = {0}; // 大小:4字节
7.4 联合体大小的计算
-
基础规则:联合体的大小至少是最大成员的大小。
-
对齐规则:当最大成员大小不是最大对齐数的整数倍时,需要对齐到最大对齐数的整数倍。
示例1
cs
#include <stdio.h>
union Un1
{
char c[5]; // 大小5,对齐数1
int i; // 大小4,对齐数4
};
union Un2
{
short c[7]; // 大小14,对齐数2
int i; // 大小4,对齐数4
};
int main()
{
// Un1:最大成员大小5,最大对齐数4 → 需对齐到8(4的2倍)
printf("%d\n", sizeof(union Un1)); // 输出:8
// Un2:最大成员大小14,最大对齐数4 → 需对齐到16(4的4倍)
printf("%d\n", sizeof(union Un2)); // 输出:16
return 0;
}
7.5 联合体的应用场景:节省内存
当一个对象的多个属性不会同时使用时,可以用联合体来节省内存。
例如:礼品兑换单中,图书、杯子、衬衫的属性不会同时存在。
优化前(结构体)
cs
struct gift_list
{
int stock_number; // 库存量(公共属性)
double price; // 定价(公共属性)
int item_type; // 商品类型(公共属性)
char title[20]; // 书名(图书专属)
char author[20]; // 作者(图书专属)
int num_pages; // 页数(图书专属)
char design[30]; // 设计(杯子/衬衫专属)
int colors; // 颜色(杯子/衬衫专属)
int sizes; // 尺寸(衬衫专属)
};
优化后(结构体+联合体)
cs
struct gift_list
{
int stock_number; // 库存量(公共属性)
double price; // 定价(公共属性)
int item_type; // 商品类型(公共属性)
// 用联合体存储不同商品的专属属性
union
{
struct
{
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // 页数
}book;
struct
{
char design[30]; // 设计
}mug;
struct
{
char design[30]; // 设计
int colors; // 颜色
int sizes; // 尺寸
}shirt;
}item;
};
7.6 联合体练习:判断机器大小端
利用联合体成员共用内存的特点,可以快速判断当前机器是大端还是小端存储。
cs
// 返回1表示小端,返回0表示大端
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
// 小端:低字节存低地址 → c的值为1
// 大端:高字节存低地址 → c的值为0
return un.c;
}
大小端存储的背景
在计算机中,多字节数据(如 int 类型)在内存中的存储有两种方式:
• 小端模式(Little Endian):低字节数据存放在低地址,高字节数据存放在高地址。
• 大端模式(Big Endian):高字节数据存放在低地址,低字节数据存放在高地址。
以 int i = 1 为例(假设是4字节的 int):
• 十六进制表示:0x00000001
• 小端存储(低地址到高地址):01 00 00 00
• 大端存储(低地址到高地址):00 00 00 01
关键原理
• 联合体的内存共享:联合体的所有成员共用同一块内存空间,所以 un.i 和 un.c 的起始地址完全相同。
• char 类型的特性:char 只占用1字节,所以 un.c 会读取 un.i 占用的4字节内存中的低地址1字节。
运行逻辑
-
当 un.i = 1 时,内存中存储的是 0x00000001。
-
如果是小端模式:低地址存低字节 01,所以 un.c 读取到的是 1 → 返回 1。
-
如果是大端模式:低地址存高字节 00,所以 un.c 读取到的是 0 → 返回 0。
八、枚举类型
8.1 枚举类型的声明
枚举是将可能的取值一一列举出来的自定义类型,关键字为 enum。
枚举常量默认从0开始,依次递增1,也可以手动赋值。
示例
cs
// 枚举一周的星期
enum Day
{
Mon, // 默认值0
Tues, // 默认值1
Wed, // 默认值2
Thur, // 默认值3
Fri, // 默认值4
Sat, // 默认值5
Sun // 默认值6
};
// 枚举性别
enum Sex
{
MALE, // 默认值0
FEMALE, // 默认值1
SECRET // 默认值2
};
// 枚举颜色(手动赋值)
enum Color
{
RED=2, // 手动赋值2
GREEN=4, // 手动赋值4
BLUE=8 // 手动赋值8
};
8.2 枚举类型的优点
相比 #define 定义常量,枚举有以下优势:
-
增加可读性和可维护性:枚举常量有明确的语义,代码更易理解。
-
类型检查更严谨:枚举是一种类型,编译器会进行类型检查,而 #define 只是文本替换。
-
便于调试:#define 定义的符号在预处理阶段会被删除,枚举常量则会保留在调试信息中。
-
使用方便:一次可以定义多个常量,无需重复写 #define。
-
遵循作用域规则:枚举声明在函数内,只能在函数内使用。
8.3 枚举类型的使用
cs
#include <stdio.h>
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
// 使用枚举常量给枚举变量赋值
enum Color clr = GREEN;
printf("%d\n", clr); // 输出:2
// 在C语言中可以用整数给枚举变量赋值(C++中不允许)
clr = 3;
printf("%d\n", clr); // 输出:3
return 0;
}
尾言
结构体、联合体与枚举是C语言自定义类型的核心,更是夯实底层基础、衔接数据结构学习的关键。从结构体的封装与内存对齐,到联合体的内存共享与大小端检测,再到枚举的规范化常量定义,三者各有妙用,既体现了C语言贴近硬件、高效灵活的特性,也是开发与面试中的高频考点。
吃透这三大自定义类型的底层原理与使用技巧,不仅能解决实际开发中的内存优化、数据封装问题,更能为后续链表、嵌入式开发等内容筑牢基础。C语言的进阶之路,始于对基础细节的深究,愿本文能为你扫清障碍,后续也将持续更新C语言底层干货,陪你稳步提升编程能力。