C语言——自定义类型:结构体、联合体、枚举

前言

在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无默认对齐数):

  1. 第一个成员:对齐到结构体变量起始地址偏移量为0的位置。

  2. 后续成员:对齐到自身对齐数的整数倍偏移量位置。

◦ 对齐数 = 编译器默认对齐数 和 成员自身大小 的较小值。

◦ VS默认对齐数为8,GCC无默认对齐数(对齐数=成员自身大小)。

  1. 结构体总大小:必须是所有成员最大对齐数的整数倍,不足则补空字节(填充)。

  2. 嵌套结构体:嵌套的结构体成员对齐到自身最大对齐数的整数倍位置,整体总大小为所有最大对齐数(含嵌套)的整数倍。

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

分步计算:

  1. c1:偏移0,占用1字节(0~0)。

  2. i:需对齐到4的整数倍,偏移4开始,占用4字节(4~7)。

  3. c2:需对齐到1的整数倍,偏移8开始,占用1字节(8~8)。

  4. 总占用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

分步计算:

  1. c1、c2连续存储,共占用2字节(0~1)。

  2. i对齐到4的整数倍,偏移4开始,占用4字节(4~7)。

  3. 总占用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

分步计算:

  1. d占用8字节(0~7),c占用1字节(8~8)。

  2. i对齐到4的整数倍,偏移12开始(8+1=9,最近的4的倍数为12),占用4字节(12~15)。

  3. 总占用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

分步计算:

  1. c1占用1字节(0~0),s3对齐到自身最大对齐数8,偏移8开始,占用16字节(8~23)。

  2. d对齐到8的整数倍,偏移24开始,占用8字节(24~31)。

  3. 总占用32字节,是最大对齐数8的整数倍,无需填充。

最终大小:32字节。

3.5 内存对齐的原因

为什么要存在内存对齐?核心原因是平台兼容性和性能优化,即拿空间换时间:

  1. 平台原因(移植性):部分硬件平台(如嵌入式)只能在特定地址处访问特定类型的数据,未对齐的内存访问会触发硬件异常。

  2. 性能原因(效率):处理器访问内存时,以字长为单位(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 为什么首选地址传递?

核心原因是函数传参的压栈开销:

  1. 函数传参时,参数会被压入栈区,栈区的内存空间有限且压栈/出栈需要消耗系统资源。

  2. 值传递:形参是实参的完整拷贝,若结构体过大(如示例中的4004字节),拷贝会消耗大量内存和CPU资源,导致性能急剧下降。

  3. 地址传递:形参是结构体指针,仅占用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条特殊规则:

  1. 位段的成员必须是整型家族(int、unsigned int、signed int、char等)。

  2. 成员名后加冒号+数字,数字表示该成员占用的比特位(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条规则:

  1. 按基本单位开辟:成员为int则按4字节(32bit)开辟,为char则按1字节(8bit)开辟。

  2. 比特位连续分配:在一个基本单位内,位段成员连续存储,不足则开辟下一个基本单位。

  3. 无跨平台标准:比特位的存储顺序(左→右/右→左)、剩余比特位是否复用,由编译器决定(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. 开辟第1个字节(8bit):存储a(3bit)+b(4bit),剩余1bit舍弃,共占用7bit。

◦ a=010(低3bit),b=1100(中间4bit),组合为:0 1100 010 → 十进制98(0x62)。

  1. 开辟第2个字节(8bit):存储c(5bit),剩余3bit舍弃。

◦ c=00011 → 填充3bit0,组合为:000 00011 → 十进制3(0x03)。

  1. 开辟第3个字节(8bit):存储d(4bit),剩余4bit舍弃。

◦ d=0100 → 填充4bit0,组合为:0000 0100 → 十进制4(0x04)。

最终内存:3字节,值为0x62、0x03、0x04,总大小3字节。

5.5 位段的跨平台问题

位段的内存分配依赖编译器实现,存在4大跨平台问题,注重移植性的程序应避免使用:

  1. int位段的符号性:signed int位段的最高位为符号位,unsigned int无符号,编译器未统一规定。

  2. 最大比特数限制:16位机器int为2字节(16bit),位段成员比特数超过16会报错,32位机器无此问题。

  3. 存储顺序:比特位的存储顺序(左→右/右→左)由编译器决定,无标准。

  4. 剩余比特位复用:一个基本单位的剩余比特位,编译器可选择复用或舍弃,无标准。

5.6 位段的使用注意事项

位段的成员是按比特位分配的,无独立的内存地址,因此存在2个使用限制:

  1. 不能使用&取位段成员的地址,编译报错。

  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 位段的应用场景

位段的核心优势是极致节省内存,主要应用于以下场景:

  1. 网络协议:IP报头、TCP报头的字段仅需几个bit,使用位段可大幅减小数据包大小,提升传输效率。

  2. 嵌入式开发:寄存器配置通常按bit操作,位段可直接映射寄存器的bit位,简化代码。

  3. 硬件编程:外设配置(如GPIO、UART)的控制位,多为bit级,位段可精准操作。

六、结构体总结

结构体是C语言自定义复合类型的核心,是实现数据封装和复杂数据结构的基础,本文核心知识点梳理:

  1. 声明与初始化:支持标准声明、匿名声明,初始化有顺序初始化、指定成员初始化、嵌套初始化,核心访问语法为变量.成员和指针->成员。

  2. 内存对齐:四大规则是核心,本质是空间换时间,可通过调整成员顺序和修改默认对齐数优化内存利用率,是面试高频考点。

  3. 结构体传参:首选地址传递,减少压栈开销,只读传参需加const修饰,提升代码健壮性。

  4. 位段:结构体的特殊形式,按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. 基础规则:联合体的大小至少是最大成员的大小。

  2. 对齐规则:当最大成员大小不是最大对齐数的整数倍时,需要对齐到最大对齐数的整数倍。

示例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字节。

运行逻辑

  1. 当 un.i = 1 时,内存中存储的是 0x00000001。

  2. 如果是小端模式:低地址存低字节 01,所以 un.c 读取到的是 1 → 返回 1。

  3. 如果是大端模式:低地址存高字节 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 定义常量,枚举有以下优势:

  1. 增加可读性和可维护性:枚举常量有明确的语义,代码更易理解。

  2. 类型检查更严谨:枚举是一种类型,编译器会进行类型检查,而 #define 只是文本替换。

  3. 便于调试:#define 定义的符号在预处理阶段会被删除,枚举常量则会保留在调试信息中。

  4. 使用方便:一次可以定义多个常量,无需重复写 #define。

  5. 遵循作用域规则:枚举声明在函数内,只能在函数内使用。

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语言底层干货,陪你稳步提升编程能力。

相关推荐
子木鑫2 小时前
[SUCTF 2019] CheckIn1 — 利用 .user.ini 与图片马构造 PHP 后门并绕过上传检测
android·开发语言·安全·php
日更嵌入式的打工仔2 小时前
嵌入式软件开发工具与方法
笔记
mirror_zAI2 小时前
C语言中的sscanf用法详解
c语言·开发语言
sayang_shao2 小时前
YOLOv8n 输入输出格式笔记
笔记·yolo
AI视觉网奇2 小时前
ue slot 插槽用法笔记
笔记·学习·ue5
fie88892 小时前
MATLAB中LASSO方法的特征矩阵优化与特征选择实现
开发语言·matlab·矩阵
Jack___Xue2 小时前
LangGraph学习笔记(二)---核心组件与工作流人机交互
笔记·学习·人机交互
仰泳的熊猫2 小时前
题目1433:蓝桥杯2013年第四届真题-危险系数
数据结构·c++·算法·蓝桥杯·深度优先·图论
cyforkk2 小时前
14、Java 基础硬核复习:数据结构与集合源码的核心逻辑与面试考点
java·数据结构·面试