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 条好记的方法,严格按这个算,绝对不会错:
- 成员对齐规则:每个成员的起始偏移量,必须是「该成员自身大小」和「默认对齐数」的较小值的整数倍。
- 整体对齐规则 :所有成员存放完成后,结构体的总大小,必须是所有成员中最大的自身大小 和默认对齐数的较小值的整数倍。
- 环境规则:VS2013 默认对齐数是 8,gcc 默认是 4,做题第一步先看环境!
1.3一步步推导过程
-
第一个成员 int a
- 自身大小 4 字节,对齐数 = min (4,8)=4
- 结构体首个成员默认从偏移 0 开始,0 是 4 的整数倍,符合规则
- 占用偏移 0~3,共 4 字节,当前累计占用 4 字节
-
第二个成员 char b
- 自身大小 1 字节,对齐数 = min (1,8)=1
- 上一个成员结束在偏移 3,下一个可用地址是 4,4 是 1 的整数倍,符合规则
- 占用偏移 4,共 1 字节,当前累计占用 5 字节
-
第三个成员 short c
- 自身大小 2 字节,对齐数 = min (2,8)=2
- 下一个可用地址是偏移 5,5 不是 2 的整数倍,无法存放
- 填充 1 字节(偏移 5),找到下一个 2 的整数倍地址偏移 6
- 占用偏移 6~7,共 2 字节,当前累计占用 8 字节
-
第四个成员 short d
- 自身大小 2 字节,对齐数 = min (2,8)=2
- 上一个成员结束在偏移 7,下一个可用地址是 8,8 是 2 的整数倍,符合规则
- 占用偏移 8~9,共 2 字节,所有成员存放完成,当前累计占用 10 字节
-
最容易忘的整体对齐
- 所有成员中最大自身大小是 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避坑总结
- 不要直接把成员大小相加,内存对齐会有填充字节,必须逐个算偏移量
- 成员存放完成后,一定要做整体对齐,这是 80% 的人都会忘的一步
- 做题先看编译器环境,不同环境的默认对齐数不同,结果会有差异
二、题 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
};
- 第一个字节:4bit + 2bit = 6bit,未满 8bit,两个位段共用 1 字节
- 第二个字节:普通成员 state,是完整的 unsigned char,单独占用 1 字节
- 第三个字节: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
- 先替换宏:
3 * 2 + 3 - 运算符优先级:乘法 > 加法,先算 3*2=6,再算 6+3=9
2.4最终答案
选D,最终分配 9 个字节的空间。
2.5避坑总结
- 宏定义永远先做文本替换,再计算,没有括号绝对不要先算结果
- 位段不能跨基础类型共用字节,不同类型的成员 / 位段必须开新的存储单元
- 混合运算一定要先看运算符优先级,不要想当然从左到右计算
三、题 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 | 否 |
这里有两个关键结论:
- 结构体总大小只有 2 字节,只会覆盖数组的前 2 个元素 puc [0] 和 puc [1]
- puc [2] 和 puc [3] 全程不会被修改,会保持 memset 初始化的 0 值不变
第二步:VS 环境位段必背核心规则
这道题的规则记错,结果直接全错,我把 VS 环境下的位段规则总结成 3 条,必须记死:
- 同类型合并规则:相同基础类型的位段,优先挤在同一个存储单元,不跨字节、不跨类型存储
- 存储顺序规则 :从字节的低 bit 位向高 bit 位依次存放(低位优先),绝对不能搞反
- 数值截断规则:赋值超过位段指定的 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避坑总结
- 位段赋值先做截断,只要超过分配的 bit 数,二话不说只留低位有效 bit
- VS 环境位段是从低 bit 向高 bit 存,千万不要搞反高低位顺序
- 强制类型转换只会覆盖对应大小的内存,未被覆盖的区域保持初始化值不变
- 同类型位段只会挤在同一个字节,不会跨字节存储
四、刷题复盘总心得
刷完这三道题,我最大的感受就是:C 语言的坑,全在细节里。很多时候不是我们不会,而是想当然 ------ 比如宏定义直接算结果,忘了文本替换;位段搞反了高低位;算完结构体成员忘了整体对齐。
最后给大家几个课后刷题和笔试的小建议:
- 算内存布局一定要画表格、写偏移量,不要心算,心算极其容易出错
- 做题第一步先看编译器环境,VS 和 gcc 的规则有差异,不看环境直接算必错
- 遇到宏定义,永远先做文本替换,再计算,没有括号绝对不先算结果
- 位段相关的题,先截断、再排 bit 位,一步都不能跳
这些都是 C 语言笔试的高频核心考点,把这些规则记死,坑摸透,以后再遇到同类型的题,直接秒杀,再也不会掉进出题人的陷阱里。