C语言:结构体与联合体核心知识点详解

前言:

本篇系统梳理 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字节
};

计算步骤:

  1. a 从偏移 0 开始,占 1 字节,偏移范围 0
  2. b 对齐数为 min (4, 默认 8)=4,偏移必须是 4 的倍数,填充 3 字节,从偏移 4 开始,占 4 字节,偏移范围 4-7
  3. c 对齐数为 1,直接从偏移 8 开始,占 1 字节,偏移范围 8
  4. 最大对齐数为 4,总大小必须是 4 的倍数,填充 3 字节,最终总大小 = 12 字节

示例 2:嵌套结构体大小计算

复制代码
struct S2 {
    char a;
    struct S1 s; // 嵌套S1,最大对齐数是4
    double d;    // 8字节
};

计算步骤:

  1. a 占偏移 0,共 1 字节
  2. 嵌套结构体 s 对齐数为 4,偏移到 4 开始,占 12 字节,偏移范围 4-15
  3. d 对齐数为 8,偏移必须是 8 的倍数,当前 16 符合,占 8 字节,偏移范围 16-23
  4. 最大对齐数为 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;

核心规则:

  1. 柔性数组必须是结构体的最后一个成员
  2. 结构体中至少还有一个其他成员
  3. 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;

柔性数组相比指针方案的三大优势:

  1. 内存连续:结构体和数组在同一块连续内存,只需要一次 malloc/free,管理简单,内存碎片更少
  2. 访问效率高:连续内存缓存友好,访问速度更快
  3. 释放方便:释放结构体时一次 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:结构体和联合体的区别?

答:

  1. 内存布局:结构体每个成员独立存储;联合体所有成员共享同一块内存
  2. 大小计算:结构体大小是所有成员大小 + 对齐填充之和;联合体大小是最大成员大小 + 对齐
  3. 使用方式:结构体可同时使用所有成员;联合体同一时间只有一个成员有效
  4. 用途:结构体用来封装对象多个属性;联合体用来节省内存、多场景复用空间

Q2:结构体可以直接赋值吗?

答:同类型的结构体可以直接用 = 赋值,编译器会做内存拷贝,等价于 memcpy。但注意如果结构体内部有指针,只会拷贝指针地址,属于浅拷贝,释放时要小心重复释放问题。

Q3:柔性数组和指针数组成员的区别?

答:柔性数组不占用结构体大小,和结构体内存连续,一次申请一次释放;指针成员占用指针大小(4/8 字节),内存不连续,需要单独申请和释放。

3. 常见易错坑点

  1. 结构体传值开销大:大结构体尽量传指针,避免栈溢出和拷贝开销
  2. 对齐忽略:计算结构体大小只算成员总和,忽略对齐填充字节
  3. 联合体误用:同时读写多个成员,误以为多个成员独立存储
  4. 位域跨平台:直接用位域做网络 / 跨设备数据传输,出现解析错乱
  5. 柔性数组位置错:把柔性数组放在结构体中间,或前面没有其他成员

以上就是 C 语言结构体、联合体、位域、柔性数组的全部核心内容,是 C 语言进阶和嵌入式开发的必备知识点,也是面试笔试的高频考点。


制作不易,如果对你有用,希望能点赞收藏支持一下。