(嵌入式八股)第9章 嵌入式常考手撕题(一):嵌入式基础相关!
相比于前后端算法,嵌入式手撕题并不追求极限难度,而是更强调实际开发中的核心技能。经过累计100+场嵌入式面试实战,我发现无论是大厂还是中小企业(如 汇川、Momenta、算能科技 等),都会重点考察嵌入式基础相关的手撕代码。
然而,这些 高频考题往往不会出现在 LeetCode 热门题库或Hot100,导致许多同学在备战时容易忽略这一关键部分(比如结构体大小计算,位运算等等)。但在实际面试中,这些题目考察手撕的频率极高!甚至远远超过力扣里面的题目!!
为了帮助大家更高效备战,我将这些 不在力扣,却被频繁考察的嵌入式手撕题悉心整理,总结在这一篇文章里。希望能助你稳扎稳打,真正提升上岸率,避免在技术面“被卡”!
9.1 结构体对齐
1. 结构体对齐步骤
结构体的对齐主要遵循以下步骤:
步骤 1: 确定成员变量的大小和自身对齐字节数
每个数据类型在不同的环境下有不同的字节大小,常见情况如下(假设 64 位系统):
步骤 2: 按顺序放置成员变量,并根据其对齐要求调整偏移量
- 每个成员变量的起始地址必须是其对齐字节数的倍数。
- 如果当前地址不满足对齐要求,需要添加 填充字节(padding) 使其对齐。
步骤 3: 计算结构体的总大小
- 结构体的总大小必须是 结构体最大对齐字节数 的倍数。
- 如果不满足,则在末尾填充(padding)直到满足对齐规则
2. 结构体对齐规则
结构体的对齐主要有以下规则:
3. 代码示例
struct A:
int a
占 4 字节,偏移量 0。char b
占 1 字节,偏移量 4。short c
处于 6 偏移量。short c
占 2 字节,偏移量 6,对齐完成。max(4,1,2) = 4
。4
的倍数,当前大小 8,符合要求。struct B:
int a
占 4 字节,偏移量 0。short c
占 2 字节,偏移量 4,对齐完成。char b
占 1 字节,偏移量 6。max(4,2,1) = 4
。4
的倍数,当前大小 8,符合要求。4. 结论
- 结构体对齐是为了提高内存访问效率,避免跨字对齐带来的性能损失。
- 结构体大小取决于最大对齐字节数的倍数,会因为填充字节而变大。
- 优化结构体成员顺序 可以 减少填充字节,降低内存浪费。
9.2 修改默认对齐数指令: #pragma pack(n)
我们可以使用 #pragma pack(n)
指令来 修改结构体的默认对齐数,从而改变结构体的大小。
#pragma pack(n)
指定所有后续结构体成员的对齐方式,#pragma pack()
可以将对齐方式恢复为默认值。
1. #pragma pack(n) 的使用
语法:
其中 n
表示结构体成员的 最大对齐数,即结构体内的所有变量不会超过 n
进行对齐。
2. 示例代码
1. DefaultAlign
结构体(默认对齐方式,大小 8 字节)
int a
4 字节,偏移量0
char b
1 字节,偏移量4
- 填充 1 字节(使
short c
对齐到2
字节) short c
2 字节,偏移量6
- 结构体总大小 8(符合 4 字节对齐)
2. Pack1Align
结构体(1 字节对齐,大小 7 字节)
int a
4 字节,偏移量0
char b
1 字节,偏移量4
short c
2 字节,偏移量5
(无填充)- 结构体总大小 7
由于 #pragma pack(1)
强制 所有成员以 1 字节对齐,所以 c
无需填充,结构体大小减少为 7 字节。
结论
- 默认对齐方式 遵循系统对齐规则,结构体大小为 8 字节。
#pragma pack(1)
取消对齐填充,减少结构体大小,最终大小为 7 字节。- 降低对齐可能影响性能,适用于存储敏感场景(如二进制文件、协议解析)。
- 建议使用
offsetof()
确保结构体布局符合预期。
9.3 类大小计算
类的大小(sizeof(class)
) 取决于 成员变量、内存对齐规则、继承方式、虚函数等。将详细介绍如何计算 类的大小 并通过代码示例进行验证。
1. 计算规则
vtable
指针(通常 4 或 8 字节)。2. 基础示例:无继承,无虚函数
计算过程
3. 影响类大小的因素
(1)静态成员不占用对象空间
(2)空类的大小
- C++ 规定空类必须占用 至少 1 字节,以确保不同对象有 唯一地址。
(3)继承影响类的大小
D
大小 = 4
(int a
)E
继承 D
,但 char b
需要 填充,因此 sizeof(E) = 8
4. 虚函数对类大小的影响
(1)单个虚函数
int a = 4
字节vptr
):4 或 8 字节(取决于平台)(2)多重继承的 vptr 影响
G
只包含 vptr
,大小为 8(64位)。H
继承 G
,但 vptr
不会重复,最终大小仍为 8。5. 结构体 vs 类的大小:普通 struct 和 class
struct
和class
的大小规则 完全相同,区别仅在 默认访问权限。
6. 结论
9.4 位操作
1. 基本概念
位操作是对 整数的二进制位 进行 直接操作,可以实现高效的数值处理。
<<
):相当于 乘以 2^N
(位数 N
)。>>
):相当于 除以 2^N
(仅取整数部分)。&
):用于清零某些位,常用于 掩码操作。|
):用于设置某些位为1。^
):用于翻转某些位。~
):翻转所有位(1变0,0变1)。2. 左移(<<)
功能
左移操作 x << N
相当于 x * 2^N
,即 乘以 2
的 N
次方。
示例
3. 右移(>>)
功能
右移操作 x >> N
相当于 x / 2^N
,即 除以 2
的 N
次方,只取整数部分。
代码
4. 提取某个字节
功能
提取整数 x
的指定字节(从右往左编号为 0-3
)。
代码
5. 提取某一位
功能
提取 x
的某一位(bit
位)。
代码
6. 清零某一位
功能
将 x
的 bit
位置零。
代码
7. 置1某一位
功能
将 x
的 bit
位置 1。
代码
8. 翻转某一位
功能
翻转 x
的 bit
位。
代码
9.5 大小端判断
在计算机体系结构中,大小端(Big-Endian & Little-Endian) 描述的是多字节数据在内存中的存储顺序:
- 大端(Big-Endian):高字节存储在低地址,低字节存储在高地址(高位优先)。
- 小端(Little-Endian):低字节存储在低地址,高字节存储在高地址(低位优先)。
方法一:使用指针访问整数的最低字节
int a = 1
,int
类型通常占 4 字节。a
的地址 (char*)&a
,让 char*
指向 a
的最低字节。*(char*)&a == 1
,说明 1
被存储在 低地址,即小端存储方式。方法二:使用 union 联合体
union
共享所有成员的 内存空间,test.a
和 test.b
共享相同的地址。test.a = 1
,即存储 0x00000001
:test.b
访问的是 最低字节(0x01),输出 "小端"。- test.b 访问的是 最低字节(0x00),输出 "大端"。
方法三:使用 stdint.h 进行通用检查
赋值 0x12345678
并用 uint8_t*
指针按字节访问:
小端存储:
内存存储顺序: 78 56 34 12
大端存储:
内存存储顺序: 12 34 56 78
结论
9.6 求一个整数的二进制表示中 1
的个数
基本思想
核心是:
- 判断最低位是否是
1
:通过i & 1
与操作,检查i
的最低位是否是1
。 - 计数
1
的个数:如果i & 1
结果为1
,则计数器ret++
。 - 右移
i
:执行i = i >> 1
,相当于将i
除以2
,不断移除最低位。 - 循环进行,直到
i == 0
。
代码实现
i
的最低位是否是 1
。i
,直到 i == 0
。优化方法:Brian Kernighan 算法
原方法的时间复杂度是 O(32),因为最多执行 32
次 while
循环(对于 int
类型)。 可以优化为 O(k)(k
是二进制 1
的个数),使用 Brian Kernighan 算法:
核心优化点:
i & (i - 1)
可以 快速清除最低位的 1
。1
,只需执行 k
次(k
是 1
的个数),比 O(32) 更快。9.7 判断一个数是否为2的幂
算法原理
一个数是 2 的幂,它的 二进制表示只有 1 个 1
,其余都是 0
:
1 (2^0) = 0001
2 (2^1) = 0010
4 (2^2) = 0100
8 (2^3) = 1000
如果 n
是 2
的幂:
n - 1
的二进制表示 全是 1
:
2 = 00010
,2-1 = 00001
4 = 00100
,4-1 = 00011
8 = 01000
,8-1 = 00111
位与操作n & (n - 1)
:结果必须是 0
如果 n & (n-1) == 0
,则 n
是 2 的幂。
代码实现
if (i <= 0)
:0
不是 2 的幂,直接返回 1
(不是)。i &
:剩余60%内容,订阅专栏后可继续查看/也可单篇购买
作者简介:仅用几个月时间0基础天坑急转嵌入式开发,逆袭成功拿下华为、vivo、小米等15个offer,面试经验100+,收藏20+面经,分享求职历程与学习心得。 专栏内容:这是一份覆盖嵌入式求职过程中99%问题指南,详细讲解了嵌入式开发的学习路径、项目经验分享、简历优化技巧、面试心得及实习经验,从技术面,HR面,AI面,主管面,谈薪一站式服务,助你突破技术瓶颈、打破信息差,争取更多大厂offer。