按模块分类整理,每题给出要点和底层原理,而非泛泛的概念罗列,方便直接对答。
目录
-
C 语言基础与内存
-
指针与函数
-
C++ 特有
-
多线程与并发
-
Linux 系统编程与 IPC
-
嵌入式/驱动/硬件相关
-
编译链接与调试
-
复习建议
一、C 语言基础与内存
**1. **const** 、 **static** 、 **volatile** 的作用与底层含义? **
-
const:编译期只读约束,修饰指针要分清"指向常量的指针"(const int *p)和"常量指针"(int * const p)。 -
static:修饰局部变量延长生命周期到程序结束(存放在.data/.bss而非栈);修饰全局变量/函数限制链接作用域为当前编译单元(内部链接)。 -
volatile:告诉编译器每次访问都要真正读写内存,不能用寄存器缓存值、不能做优化重排。嵌入式场景必考:访问硬件寄存器、多线程/中断共享的标志位必须加 volatile,否则编译器优化可能让循环永远读到"缓存的旧值"。
**2. 内存分区(栈、堆、全局/静态区、常量区、代码区)分别存什么? **
-
栈:局部变量、函数参数,编译期确定大小,自动回收,向低地址增长。
-
堆:
malloc/new动态分配,手动释放,容易碎片化。 -
.bss:未初始化的全局/静态变量(不占可执行文件体积,运行时清零)。 -
.data:已初始化的全局/静态变量。 -
.text/.rodata:代码和字符串常量,只读。
**3. **malloc** / **free** 底层原理?为什么频繁小块分配会导致内存碎片? **
-
glibc 的
malloc用brk/mmap向内核申请大块内存,自己维护空闲链表(bins,按大小分级管理,如 fastbin/smallbin/largebin)做二次分配。小块请求走brk扩展堆,超过阈值(默认 128KB)走mmap独立映射。 -
碎片来源:反复分配释放不同大小的块,空闲链表里留下大量无法拼接利用的"洞",即使总空闲量够,也可能找不到连续满足需求的块(外部碎片)。
**4. 内存泄漏、野指针、内存越界怎么排查? **
-
工具:
valgrind --tool=memcheck(检测越界、未初始化读、泄漏)、AddressSanitizer(-fsanitize=address,速度比 valgrind 快很多,嵌入式如果目标平台支持也优先用这个)、gdb配合 core dump。 -
嵌入式资源受限场景没法跑重型工具时:自己封装内存分配器加统计/边界标记(内存池 + guard byte 校验)做轻量级检测。
**5. **struct** 内存对齐与字节对齐原理?为什么要对齐? **
-
CPU 按"自然边界"访问内存效率最高(很多架构对非对齐访问会触发异常或性能惨重下降),编译器默认按成员最大对齐要求(通常等于最大成员大小,不超过平台对齐粒度)填充字节。
-
#pragma pack(n)/__attribute__((packed))取消对齐节省空间,但访问未对齐字段在某些架构(ARM 严格对齐模式)上会直接触发总线错误——嵌入式协议解析、结构体映射硬件寄存器时特别要注意这个坑。
**6. 大端/小端存储,怎么判断当前系统的字节序? **
int a = 1;
char *p = (char*)&a;
printf(*p ? "小端" : "大端");
// 小端:低地址存低字节,*p 取到的是 1;大端反之
嵌入式跨平台通信(尤其网络字节序转换 htons/ntohl)和不同 CPU 架构对接协议时是必考点。
二、指针与函数
**7. 函数指针、回调函数、 **void** ***** ** **通用指针的使用场景? ****
-
嵌入式驱动框架大量用函数指针实现"面向对象"式的操作集(如
struct file_operations里的.read/.write),本质是手写虚函数表。 -
void*用于泛型容器/回调传参携带上下文,使用时要注意类型安全完全靠约定,没有编译期检查。
**8. **sizeof(数组名)** 和 **sizeof(指针)** 的区别?数组作为函数参数会退化成指针吗? **
-
数组名在
sizeof里代表整个数组,返回总字节数;但作为函数参数传递时会退化成指针,sizeof在函数内部拿到的只是指针大小(4/8 字节),这是经典陷阱。
**9. **memcpy** 和 **memmove** 的区别,为什么内存区域重叠时必须用 **memmove** ? **
-
memcpy不保证处理重叠区域(可能从前往后覆盖导致数据被破坏);memmove内部会判断重叠情况,选择合适的拷贝方向(从后往前或从前往后)保证正确性。嵌入式做环形缓冲区/数据搬移时容易踩这个坑。
三、C++ 特有
**10. 虚函数底层实现(vtable)原理? **
-
每个含虚函数的类有一份虚函数表(函数指针数组),对象内存布局开头存一个指向 vtable 的指针(vptr)。调用虚函数时通过 vptr 查表间接调用,实现运行时多态。构造函数执行期间 vptr 还没指向派生类的 vtable,这也是"构造函数里调虚函数不会走到派生类实现"的原因。
**11. 构造函数/析构函数调用顺序,虚析构函数为什么重要? **
-
构造顺序:基类 → 成员对象(按声明顺序) → 自身;析构顺序完全相反。
-
如果基类析构函数不是
virtual,通过基类指针delete派生类对象时,只会调用基类析构函数,派生类新增的资源不会被释放——嵌入式里管理硬件资源/内存句柄的基类必须声明虚析构函数。
**12. RAII 是什么,嵌入式场景为什么重要? **
-
Resource Acquisition Is Initialization:把资源(内存、文件描述符、锁、硬件句柄)的生命周期绑定到对象的构造/析构,利用栈对象析构的确定性保证资源一定被释放(即使中途异常/提前 return)。嵌入式没有 GC,RAII 是避免资源泄漏最可靠的手段之一(智能指针
unique_ptr/shared_ptr是典型应用)。
**13. **unique_ptr** 和 **shared_ptr** 的区别与实现原理? **
-
unique_ptr:独占所有权,不可拷贝只能移动,无额外引用计数开销,几乎零成本抽象。 -
shared_ptr:引用计数(原子操作保证多线程安全),配合weak_ptr打破循环引用。嵌入式资源敏感场景要注意shared_ptr的控制块额外堆分配和原子操作开销,高频路径慎用。
四、多线程与并发(嵌入式高频重灾区)
**14. 进程和线程的区别? **
-
进程有独立地址空间,线程共享所属进程的地址空间(代码段、堆、全局变量)但有独立栈和寄存器上下文。线程切换开销远小于进程切换(不需要切换页表/地址空间)。
**15. 互斥锁(mutex)、自旋锁(spinlock)、信号量(semaphore)分别用在什么场景? **
-
互斥锁:临界区较长、可能等待较久,锁不到就休眠让出 CPU。
-
自旋锁:临界区极短(几个指令级别),预期几乎不用等待,忙等避免上下文切换开销;嵌入式内核态/中断上下文中不能睡眠,只能用自旋锁(互斥锁可能引起睡眠调度,中断上下文不允许)。
-
信号量:不仅能互斥,还能做"计数"和线程间同步/通知(生产者-消费者模型)。
**16. 死锁产生的四个必要条件?怎么避免? **
-
互斥、请求保持、不可剥夺、循环等待。避免:统一加锁顺序、用
try_lock加超时、减少锁粒度/持锁时间、用无锁数据结构。
**17. **volatile** 能保证多线程安全吗?为什么不能替代锁? **
-
不能。
volatile只保证"不被编译器优化掉、每次真读真写",**不保证原子性、不保证内存屏障(其他线程看到的写入顺序) **。多线程同步必须用锁或原子操作(std::atomic),volatile常见误用是嵌入式面试的高频考点。
**18. 生产者-消费者模型怎么实现? **
-
环形缓冲区 + 互斥锁/信号量(缓冲区非空信号量、非满信号量);无锁场景可以用单生产者单消费者的无锁环形队列(利用内存屏障和原子读写指针实现,避免锁开销,实时性要求高的嵌入式场景常用)。
**19. 什么是内存屏障(Memory Barrier),为什么需要? **
-
CPU 和编译器为了性能会对指令做重排序,内存屏障强制保证屏障前后的读写操作按程序顺序对其他核心可见。多核嵌入式系统(无锁编程、DMA 与 CPU 之间的数据一致性)经常需要手动插入内存屏障(
__sync_synchronize/std::atomic_thread_fence),否则一个核心写的数据另一个核心可能看到"乱序"的结果。
五、Linux 系统编程与 IPC
**20. 常见 IPC 方式及适用场景? **
-
管道(pipe):父子进程间简单单向通信。
-
有名管道(FIFO):无关进程间通信,基于文件系统路径。
-
共享内存(
shm):效率最高(不经过内核拷贝),需要配合信号量/锁做同步。 -
消息队列:适合结构化消息传递,内核维护队列。
-
信号(Signal):异步通知,携带信息量极少。
-
Socket(含 Unix Domain Socket):既能本机通信也能跨网络,最通用。
**21. **fork()** 之后父子进程的内存关系?写时复制(COW)原理? **
-
fork后子进程复制父进程的页表,但物理页共享,标记为只读;任一进程尝试写入时触发页错误,内核才真正拷贝那一页给写入方(写时复制),避免fork瞬间做整份内存的物理拷贝,大幅提升fork效率。
**22. **select** / **poll** / **epoll** 的区别,为什么 **epoll** 效率更高? **
-
select:有 fd 数量上限(FD_SETSIZE),每次调用需要把整个 fd 集合从用户态拷贝到内核态,内核线性遍历检查,O(n)。 -
poll:解决了 fd 数量上限问题,但依然是线性遍历。 -
epoll:内核维护一个事件表(红黑树管理,就绪队列返回结果),用回调机制在 fd 就绪时主动加入就绪列表,epoll_wait只需要取就绪列表,不需要每次全量遍历所有 fd,时间复杂度接近 O(1)(相对就绪 fd 数量),大量并发连接场景优势明显。
**23. 阻塞 IO、非阻塞 IO、IO 多路复用、异步 IO 的区别? **
-
阻塞:调用不返回直到数据就绪。
-
非阻塞:立即返回,没数据返回错误码,需要自己轮询。
-
IO 多路复用:一个线程同时监控多个 fd,哪个就绪就处理哪个(
select/poll/epoll)。 -
异步 IO:发起请求后立即返回,数据准备好由内核主动通知(回调/信号),调用者完全不参与等待过程,Linux 下
io_uring/aio是相对现代和完整的实现。
六、嵌入式/驱动/硬件相关
**24. 中断处理的上半部(Top Half)和下半部(Bottom Half)为什么要分开? **
-
中断上下文要求尽快返回,不能做耗时操作(不能睡眠、不能被抢占太久,否则影响系统实时性和其他中断响应)。上半部只做最紧急的事(读寄存器清中断标志、记录数据),耗时处理挪到下半部(软中断、tasklet、工作队列)在合适的上下文异步执行。
**25. 什么是 DMA(Direct Memory Access),为什么能提升性能? **
-
DMA 控制器可以在不占用 CPU 的情况下直接在内存和外设之间搬运数据,CPU 只需要配置好源地址/目的地址/长度然后去做别的事,数据搬完后 DMA 控制器触发中断通知 CPU。避免了大量数据搬运场景下 CPU 被"数据搬运"这种简单重复工作占满的问题。
**26. Cache 一致性问题在嵌入式里怎么体现? **
-
CPU Cache 和 DMA 直接操作的内存之间可能不一致——CPU 写入的数据可能还停留在 Cache 里没刷回主存,DMA 直接从主存搬运会读到旧数据(反之 DMA 写入主存后 CPU Cache 里可能还是旧值)。需要手动做 Cache 刷新/失效操作(
dma_map_single/flush_cache之类的接口),这是嵌入式驱动开发中非常容易踩的坑。
**27. RTOS(实时操作系统)和通用 Linux 的核心区别? **
-
RTOS 强调确定性(任务响应时间有严格上界,尤其硬实时场景),调度算法通常是固定优先级抢占式;通用 Linux 是分时/公平调度为主,即使有实时调度类(
SCHED_FIFO/SCHED_RR)也难以做到微秒级确定性响应,因为内核路径普遍存在不可预测的延迟源(页错误、Cache miss、锁竞争等)。
**28. 看门狗(Watchdog)的作用和原理? **
-
独立于主 CPU 逻辑的硬件/软件计时器,要求应用定期"喂狗"(重置计数器),如果超时没喂狗说明主逻辑可能死锁/卡死,看门狗触发系统复位,是嵌入式系统提高可靠性的常见兜底机制。
**29. 位操作在嵌入式里的常见应用? **
- 直接操作硬件寄存器(读改写某一位而不影响其他位):
reg |= (1 << bit); // 置位
reg &= ~(1 << bit); // 清位
reg ^= (1 << bit); // 翻转
if (reg & (1 << bit)) // 判断
必须结合 volatile 修饰寄存器地址指针,避免编译器优化掉"看起来重复"的读写。
七、编译链接与调试
**30. 编译的四个阶段? **
预处理(宏展开、头文件包含)→ 编译(生成汇编)→ 汇编(生成目标文件 .o)→ 链接(合并目标文件和库,解析符号引用,生成可执行文件)。
**31. 静态库和动态库的区别?嵌入式场景怎么选? **
-
静态库编译期整份代码拷贝进可执行文件,运行时不依赖外部文件,体积大但部署简单、无版本冲突风险;动态库运行时加载,节省存储和内存(多进程共享同一份库的内存映射),但需要保证目标设备上库版本兼容。资源受限、单一固件部署的嵌入式场景更常用静态链接,避免动态库版本管理的复杂度和运行时依赖问题。
**32. **gdb** 常用调试手段?远程调试嵌入式设备怎么做? **
-
断点、单步、查看变量/内存/寄存器、
bt(backtrace)看调用栈、core dump 分析。 -
嵌入式远程调试常用
gdbserver(跑在目标设备上)+ 主机端gdb通过串口/网络连接(target remote),或用 JTAG/SWD 硬件调试器做更底层(芯片级)的调试,能在没有操作系统的裸机场景下也进行断点调试。
八、复习建议
嵌入式面试的区分点通常落在: ****** **volatile** /内存屏障/Cache 一致性这类"看起来简单但容易说错"的细节**,以及能不能结合具体硬件场景讲清楚原理(比如"为什么中断里不能用互斥锁"要能讲到"互斥锁可能引起睡眠调度,而中断上下文不允许被调度阻塞"这一层)。建议针对第 4、17、24、26 题这类"原理 + 场景"结合的题目重点准备,是简历上写了"嵌入式/驱动开发"经验后最容易被深挖的方向。