3 道结构体 + 位段高频错题全拆解(附表格详解)

C 语言课后刷题复盘

最近上完 C 语言结构体与位段的课程,课后刷题真的踩了一堆坑。尤其是内存对齐、位段存储、宏定义这些细节,稍微不注意就掉进出题人的陷阱里。今天就把我复盘的 3 道题分享出来,每一步都带推导过程和表格详解,既帮大家避坑,也给自己巩固知识点。

一、题 1 :结构体内存对齐基础题(最容易忘整体对齐)

1.1原题呈现

在 VS2013 下,默认对齐数为 8 字节,这个结构体所占的空间大小是( )字节

cpp 复制代码
typedef struct{
  int a;
  char b;
  short c;
  short d;
}AA_t;

A.16 B.9 C.12 D.8

1.2解题方法

我把结构体内存对齐的核心规则,总结成了 3 条好记的方法,严格按这个算,绝对不会错:

  1. 成员对齐规则:每个成员的起始偏移量,必须是「该成员自身大小」和「默认对齐数」的较小值的整数倍。
  2. 整体对齐规则 :所有成员存放完成后,结构体的总大小,必须是所有成员中最大的自身大小默认对齐数的较小值的整数倍。
  3. 环境规则:VS2013 默认对齐数是 8,gcc 默认是 4,做题第一步先看环境!

1.3一步步推导过程

  1. 第一个成员 int a

    • 自身大小 4 字节,对齐数 = min (4,8)=4
    • 结构体首个成员默认从偏移 0 开始,0 是 4 的整数倍,符合规则
    • 占用偏移 0~3,共 4 字节,当前累计占用 4 字节
  2. 第二个成员 char b

    • 自身大小 1 字节,对齐数 = min (1,8)=1
    • 上一个成员结束在偏移 3,下一个可用地址是 4,4 是 1 的整数倍,符合规则
    • 占用偏移 4,共 1 字节,当前累计占用 5 字节
  3. 第三个成员 short c

    • 自身大小 2 字节,对齐数 = min (2,8)=2
    • 下一个可用地址是偏移 5,5 不是 2 的整数倍,无法存放
    • 填充 1 字节(偏移 5),找到下一个 2 的整数倍地址偏移 6
    • 占用偏移 6~7,共 2 字节,当前累计占用 8 字节
  4. 第四个成员 short d

    • 自身大小 2 字节,对齐数 = min (2,8)=2
    • 上一个成员结束在偏移 7,下一个可用地址是 8,8 是 2 的整数倍,符合规则
    • 占用偏移 8~9,共 2 字节,所有成员存放完成,当前累计占用 10 字节
  5. 最容易忘的整体对齐

    • 所有成员中最大自身大小是 int 的 4 字节,整体对齐数 = min (4,8)=4
    • 当前总大小 10 字节,不是 4 的整数倍,需要向上补齐到最小的 4 的倍数 12
    • 填充 2 字节(偏移 10、11),最终总大小 12 字节

1.4内存分布详情表

|----|-----|---------|----|---|---|------|----|
| 序号 | 偏移量 | 成员 | 大小 | 对齐数 (min (自身大小,8)) || 占用偏移 | 填充 |
| 1 | 0 | int a | 4 | 4 || 0~3 |   |
| 2 | 1 | int a | 4 | 4 || 0~3 |   |
| 3 | 2 | int a | 4 | 4 || 0~3 |   |
| 4 | 3 | int a | 4 | 4 || 0~3 |   |
| 5 | 4 | char b | 1 | 1 || 4 |   |
| 6 | 5 |   |   | shortc必须放在 2 的倍数地址 ||   | 1 |
| 7 | 6 | short c | 2 | 2 || 6~7 |   |
| 8 | 7 | short c | 2 | 2 || 6~7 |   |
| 9 | 8 | short d | 2 | 2 || 8~9 |   |
| 10 | 9 | short d | 2 | 2 || 8~9 |   |
| 11 | 10 |   |   | 10 向上取 4 的倍数为12 字节 ||   | 2 |
| 12 | 11 |   |   | 10 向上取 4 的倍数为12 字节 ||   | 2 |

1.5最终答案

C,结构体总大小 12 字节。

1.5避坑总结

  1. 不要直接把成员大小相加,内存对齐会有填充字节,必须逐个算偏移量
  2. 成员存放完成后,一定要做整体对齐,这是 80% 的人都会忘的一步
  3. 做题先看编译器环境,不同环境的默认对齐数不同,结果会有差异

二、题 2 宏定义 + 位段结构体综合题(三重陷阱连环坑)

2.1原题呈现

有如下宏定义和结构定义,当 A=2, B=3 时,pointer 分配( )个字节的空间。

cpp 复制代码
#define MAX_SIZE A+B
struct _Record_Struct
{
  unsigned char Env_Alarm_ID : 4;
  unsigned char Para1 : 2;
  unsigned char state;
  unsigned char avail : 1;
}*Env_Alarm_Record;

struct _Record_Struct *pointer = (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * MAX_SIZE);

A.20 B.15 C.11 D.9

2.2一步步拆解避坑

2.2.1陷阱 1:宏定义的纯文本替换规则

宏定义没有任何计算逻辑,就是无脑纯文本替换,题目里的宏没有加括号,这是第一个致命陷阱!

  • 错误写法:直接把 MAX_SIZE 算成 2+3=5
  • 正确替换:MAX_SIZE 直接替换成 2+3,不会先计算结果
2.2.2陷阱 2:位段结构体的大小计算(VS2013 环境)

先明确位段的核心规则:同类型的位段会优先挤在同一个存储单元,剩余空间放不下时才会开新字节,普通成员单独占用完整字节。我们逐个拆解结构体成员:

cpp 复制代码
struct _Record_Struct
{
  unsigned char Env_Alarm_ID : 4;  // 位段,占4bit
  unsigned char Para1 : 2;         // 位段,占2bit
  unsigned char state;              // 普通char成员,占1字节
  unsigned char avail : 1;          // 位段,占1bit
};
  1. 第一个字节:4bit + 2bit = 6bit,未满 8bit,两个位段共用 1 字节
  2. 第二个字节:普通成员 state,是完整的 unsigned char,单独占用 1 字节
  3. 第三个字节:avail 位段,基础类型是 unsigned char,不能和前面的普通 char 共用字节,新开 1 字节存放 1bit最终结构体总大小 = 3 字节。

2.3位段内存分布详情表

|----|-----|---|---|------|------|
| 字节 | bit | 成员 || 类型 | 占用空间 |
| 1 | 1 | unsigned char Env_Alarm_ID : 4 || 位段 | 4bit |
| 1 | 2 | unsigned char Env_Alarm_ID : 4 || 位段 | 4bit |
| 1 | 3 | unsigned char Env_Alarm_ID : 4 || 位段 | 4bit |
| 1 | 4 | unsigned char Env_Alarm_ID : 4 || 位段 | 4bit |
| 1 | 5 | unsigned char Para1 : 2 || 位段 | 2bit |
| 1 | 6 | unsigned char Para1 : 2 || 位段 | 2bit |
| 1 | 7 | 没满 8bit,Para1与其共用 1 字节 ||   |   |
| 1 | 8 | 没满 8bit,Para1与其共用 1 字节 ||   |   |
| 2 | 9 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 10 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 11 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 12 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 13 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 14 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 15 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 2 | 16 | unsigned char state 单独占 1 字节 || 普通字节 | 1字节 |
| 3 | 17 | unsigned char avail : 1 || 位段 | 1bit |
| 3 | 18 | avail:1bit,新开 1 字节 ||   |   |
| 3 | 19 | avail:1bit,新开 1 字节 ||   |   |
| 3 | 20 | avail:1bit,新开 1 字节 ||   |   |
| 3 | 21 | avail:1bit,新开 1 字节 ||   |   |
| 3 | 22 | avail:1bit,新开 1 字节 ||   |   |
| 3 | 23 | avail:1bit,新开 1 字节 ||   |   |
| 3 | 24 | avail:1bit,新开 1 字节 ||   |   |

2.3.1陷阱 3:运算符优先级

分配空间 的表达式:sizeof(struct _Record_Struct) * MAX_SIZE

  1. 先替换宏:3 * 2 + 3
  2. 运算符优先级:乘法 > 加法,先算 3*2=6,再算 6+3=9

2.4最终答案

D,最终分配 9 个字节的空间。

2.5避坑总结

  1. 宏定义永远先做文本替换,再计算,没有括号绝对不要先算结果
  2. 位段不能跨基础类型共用字节,不同类型的成员 / 位段必须开新的存储单元
  3. 混合运算一定要先看运算符优先级,不要想当然从左到右计算

三、题 3 位段 + 内存强制转换终极题(考点全覆盖,表格详解)

这道题是我这次刷题错的最离谱的一道,把普通结构体成员、位段存储、内存强制覆盖、数值截断全考了,堪称位段考点的集大成者,我特意做了详细的表格拆解,把每一步都扒得明明白白。

3.1原题呈现

下面代码的结果是( )

cpp 复制代码
int main()
{
  unsigned char puc[4];
  struct tagPIM
  {
    unsigned char ucPim1;
    unsigned char ucData0 : 1;
    unsigned char ucData1 : 2;
    unsigned char ucData2 : 3;
  }*pstPimData;

  pstPimData = (struct tagPIM*)puc;
  memset(puc,0,4);
  pstPimData->ucPim1 = 2; 
  pstPimData->ucData0 = 3;
  pstPimData->ucData1 = 4;
  pstPimData->ucData2 = 5;
  printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
  return 0;
}

A.02 03 04 05 B.02 29 00 00 C.02 25 00 00 D.02 29 04 00

3.2我的踩坑记录

第一次做的时候,不仅把位段的存储顺序搞反了(从高位到低位存了),还忘了数值截断,最后算出来选了 C,复盘的时候才发现,每一步都有坑,一步错结果就全错。

3.3核心逻辑梳理

这道题的核心是:我们定义了一个 unsigned char 数组 puc [4],把它的首地址强制转换成结构体指针,用结构体给成员赋值后,结构体的成员会直接覆盖数组的对应内存空间,最后打印数组的十六进制值,本质上就是考我们能不能精准算出结构体的内存布局。

第一步:结构体内存布局拆解(附数组对应表)

先看结构体定义,所有成员的基础类型都是 unsigned char(1 字节 = 8bit),我们先把结构体和数组的内存对应关系理清楚:

cpp 复制代码
struct tagPIM
{
  unsigned char ucPim1;               // 普通char成员,占1字节
  unsigned char ucData0 : 1;           // char型位段,占1bit
  unsigned char ucData1 : 2;           // char型位段,占2bit
  unsigned char ucData2 : 3;           // char型位段,占3bit
};
结构体与数组内存对应表
结构体成员 成员类型 占用总空间 对应数组元素 数组偏移地址 是否会被修改
ucPim1 普通 char 成员 1 字节 puc[0] 0x00
ucData0/1/2 位段 char 型位段组合 1 字节 puc[1] 0x01
无(未使用) 1 字节 puc[2] 0x02
无(未使用) 1 字节 puc[3] 0x03

这里有两个关键结论:

  1. 结构体总大小只有 2 字节,只会覆盖数组的前 2 个元素 puc [0] 和 puc [1]
  2. puc [2] 和 puc [3] 全程不会被修改,会保持 memset 初始化的 0 值不变

第二步:VS 环境位段必背核心规则

这道题的规则记错,结果直接全错,我把 VS 环境下的位段规则总结成 3 条,必须记死:

  1. 同类型合并规则:相同基础类型的位段,优先挤在同一个存储单元,不跨字节、不跨类型存储
  2. 存储顺序规则 :从字节的低 bit 位向高 bit 位依次存放(低位优先),绝对不能搞反
  3. 数值截断规则:赋值超过位段指定的 bit 数时,只保留低位有效 bit,高位直接丢弃

第三步:逐行赋值计算(附截断详情表)

先看前置操作:memset(puc,0,4);,把数组的 4 个字节全部初始化为 0,未被修改的位置会一直保持 0。

1. 普通成员赋值
cpp 复制代码
pstPimData->ucPim1 = 2;

这个成员直接对应 puc [0],十进制 2 转换成十六进制就是 0x02,所以puc[0] = 02,没有任何坑。

2. 位段赋值(核心难点)
cpp 复制代码
pstPimData->ucData0 = 3;  // 1bit位段
pstPimData->ucData1 = 4;  // 2bit位段
pstPimData->ucData2 = 5;  // 3bit位段

这一步是整道题的灵魂,我们先做数值截断,再做 bit 位分配。

位段赋值截断详情表
位段成员 分配 bit 数 赋值十进制 赋值二进制 有效 bit 数判断 截断后二进制 最终有效数值
ucData0 1bit 3 11 2bit>1bit,溢出 1 1
ucData1 2bit 4 100 3bit>2bit,溢出 00 00
ucData2 3bit 5 101 3bit=3bit,刚好 101 101

第四步:bit 位分配与最终数值计算

按照 VS 环境 "低 bit 到高 bit" 的存储顺序,我们把 puc [1] 这 1 字节的 8 个 bit 位,从 bit0(最低位)到 bit7(最高位)依次分配,表格如下:

字节内 bit 位 对应位段成员 占用 bit 数 存储二进制值 备注
bit0(最低位) ucData0 1bit 1 第一个位段,存最低位
bit1~bit2 ucData1 2bit 00 连续 2bit,第二个位段
bit3~bit5 ucData2 3bit 101 连续 3bit,第三个位段
bit6~bit7(最高位) 空闲 2bit 00 初始化为 0,无赋值

我们把 8 个 bit 位从最高位到最低位组合,得到完整的二进制数:

复制代码
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
0    0    1    0    1    0    0    1

二进制00101001转换成十六进制,就是0x29 ,所以puc[1] = 29

3.4最终结果锁定

  • puc [0] = 02(被 ucPim1 赋值)
  • puc [1] = 29(被三个位段赋值)
  • puc [2] = 00(未被修改,保持初始值)
  • puc [3] = 00(未被修改,保持初始值)

最终 printf 打印的结果是:02 29 00 00,选B

3.5避坑总结

  1. 位段赋值先做截断,只要超过分配的 bit 数,二话不说只留低位有效 bit
  2. VS 环境位段是从低 bit 向高 bit 存,千万不要搞反高低位顺序
  3. 强制类型转换只会覆盖对应大小的内存,未被覆盖的区域保持初始化值不变
  4. 同类型位段只会挤在同一个字节,不会跨字节存储

四、刷题复盘总心得

刷完这三道题,我最大的感受就是:C 语言的坑,全在细节里。很多时候不是我们不会,而是想当然 ------ 比如宏定义直接算结果,忘了文本替换;位段搞反了高低位;算完结构体成员忘了整体对齐。

最后给大家几个课后刷题和笔试的小建议:

  1. 算内存布局一定要画表格、写偏移量,不要心算,心算极其容易出错
  2. 做题第一步先看编译器环境,VS 和 gcc 的规则有差异,不看环境直接算必错
  3. 遇到宏定义,永远先做文本替换,再计算,没有括号绝对不先算结果
  4. 位段相关的题,先截断、再排 bit 位,一步都不能跳

这些都是 C 语言笔试的高频核心考点,把这些规则记死,坑摸透,以后再遇到同类型的题,直接秒杀,再也不会掉进出题人的陷阱里。

相关推荐
一定要AK2 小时前
Java流程控制
java·开发语言·笔记
chase。2 小时前
【学习笔记】基于扩散模型的运动规划学习与适应
人工智能·笔记·学习
xiaokangzhe2 小时前
MySQL主从复制读写分离笔记
笔记·mysql·adb
CheerWWW2 小时前
C++学习笔记——枚举、继承、虚函数、可见性
c++·笔记·学习
Heartache boy3 小时前
野火STM32_HAL库版课程笔记-TIM通道捕获应用之编码器模式
笔记·stm32·单片机·嵌入式硬件
老虎06273 小时前
LeetCode热题100 刷题笔记(第四天)二分 「 寻找两个正序数组的中位数」
笔记·算法·leetcode
llm大模型算法工程师weng4 小时前
在flomo中安放“不确定”:一款笔记产品如何让人“被看见”
笔记
今儿敲了吗4 小时前
51| 八皇后
c++·笔记·学习·算法·深度优先
Metaphor6924 小时前
使用 Python 改变 PDF 页面大小
经验分享