# Linux C/C++ 嵌入式高频面试题

2 阅读13分钟

按模块分类整理,每题给出要点和底层原理,而非泛泛的概念罗列,方便直接对答。


目录

  1. C 语言基础与内存

  2. 指针与函数

  3. C++ 特有

  4. 多线程与并发

  5. Linux 系统编程与 IPC

  6. 嵌入式/驱动/硬件相关

  7. 编译链接与调试

  8. 复习建议


一、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 题这类"原理 + 场景"结合的题目重点准备,是简历上写了"嵌入式/驱动开发"经验后最容易被深挖的方向。