1. FreeRTOS 的调度策略是什么FreeRTOS单核默认固定优先级抢占式调度 同优先级时间片轮转w.freertos...。 由两个宏控制configUSE_PREEMPTION 1开启抢占默认configUSE_TIME_SLICING 1开启同优先级时间片默认清华大学出...抢占不同优先级只要出现更高优先级就绪任务立刻抢占当前低优先级任务不需要等当前任务主动让出 CPU哪怕是在中断里唤醒高优先级任务退出中断时就触发上下文切换保证实时性。注意任务优先级是固定优先级互斥量会触发优先级继承临时提升低优先级任务优先级解决优先级反转。时间片轮转相同优先级多个同优先级就绪任务在 tick 定时器中断到来时轮流执行每个任务占用一个 tick 时间片。如果关闭configUSE_TIME_SLICING同优先级任务一旦拿到 CPU会一直运行直到主动阻塞 / 调用taskYIELD()才切换。合作式调度可选configUSE_PREEMPTION0没有抢占。只有当前运行任务主动调用taskYIELD()、阻塞才会切换任务高优先级就绪了也不能抢 CPU实时性差很少用。2. SysTick 与 FreeRTOS 任务调度是什么关系FreeRTOS 使用 SysTick 定时器产生系统节拍 Tick 中断用来维护系统时间、唤醒延时到期任务、实现同优先级时间片轮转。SysTick 只是节拍源任务抢占不一定依赖 SysTick当别的中断唤醒更高优先级任务时直接触发 PendSV 完成上下文切换无需等待 SysTick 中断。SVC SVCall异常 11【只执行一次】触发执行汇编指令svc 0调用位置vTaskStartScheduler()末尾。 作用启动第一个任务。为什么不能用 PendSV 启动第一个任务 还没有正在运行的任务PendSV 上来第一件事是保存当前任务上下文但是此时不存在旧任务保存就会出错。 SVC handler 直接只恢复新任务现场不保存旧现场。 启动完第一个任务之后后面所有任务切换高优先级抢占、时间片轮转全部交给 PendSVSVC 不再触发。一句话背诵SVC 用于启动调度器运行第一个任务整个程序生命周期只触发一次PendSV 负责后续所有任务上下文切换SysTick 提供系统节拍。PendSV异常 14软件置位触发优先级最低。所有常规任务切换都在这里。 每次切换保存当前任务 R4~R11选下一个任务恢复新任务寄存器。 SysTick /portYIELD()/portYIELD_FROM_ISR()本质都是置 PendSV 挂起标志。SysTick异常 15定时器硬件周期性触发产生 tick 节拍。 更新 tickcount唤醒延时到期任务、时间片轮转判断。SysTick不是用来做上下文切换只是标记需要切换然后触发 PendSV。3. 高优先级任务从 Blocked 状态进入 Ready 状态后调度器如何处理当高优先级任务从阻塞态Blocked变成就绪态Ready唤醒场景可能是vTaskDelay()时间到、信号量 / 队列收到数据、事件组置位。分两种上下文任务上下文唤醒和中断上下文唤醒① 在【任务上下文】唤醒普通任务里调用比如任务 A 调用xSemaphoreGive()把高优先级任务 B 唤醒。唤醒 BB 优先级 当前正在运行 A 的优先级 → B 进入就绪。调度器立刻发现就绪列表有更高优先级任务。内核调用taskYIELD()置 PendSV 挂起位触发上下文切换。PendSV 执行保存 A 现场切换去跑高优先级任务 B。直接抢占不等 tick② 在【中断上下文】唤醒ISR 版本 API如 xSemaphoreGiveFromISR中断里面释放信号量唤醒高优先级任务 B。唤醒 BB 优先级高于被中断的那个正在运行的任务。xSemaphoreGiveFromISR的返回参数pxHigherPriorityTaskWoken被置为pdTRUE代表有更高优先级任务就绪。中断服务函数末尾判断pxHigherPriorityTaskWoken pdTRUE调用portYIELD_FROM_ISR()。portYIELD_FROM_ISR()置 PendSV 挂起标志。不会在中断里面直接切任务中断继续执行完退出中断。中断全部返回之后PendSV最低优先级异常才执行上下文切换运行高优先级任务 B。补充如果唤醒的任务优先级 ≤ 当前运行任务任务只是进入就绪队列不触发调度不切换继续跑当前任务等到下次调度点tick 时间片、主动阻塞、yield才会考虑。4. 同优先级任务之间如何调度FreeRTOS 同优先级就绪任务采用时间片轮转调度。 系统节拍 SysTick 按configTICK_RATE_HZ产生 tick 中断。每一次 tick 到来时调度器把当前任务放回就绪链表尾部取出同优先级就绪链表头部任务投入运行。每个任务分得一个 tick 长度的时间片。5. configUSE_TIME_SLICING 的作用宏定义在 FreeRTOSConfig.h 里取值 0 或 1只有抢占调度开启configUSE_PREEMPTION 1时这个宏才生效。✅configUSE_TIME_SLICING 1默认开启 开启同优先级时间片轮转。 SysTick 每一次 tick 中断xTaskIncrementTick()会判断 当前任务运行满 1 个 tick 时间片并且同优先级还有其他就绪任务就调用taskYIELD()触发 PendSV切换同优先级下一个就绪任务。6. 关闭时间片轮转后同优先级任务如何运行当前任务进入阻塞态vTaskDelay、等待信号量 / 队列 / 事件组主动让出 CPU。调度器从就绪链表取出同优先级下一个就绪任务运行。当前任务手动调用taskYIELD()主动放弃 CPU切换同优先级下一个就绪任务。出现更高优先级任务就绪→ 直接抢占这个不受时间片开关影响。7. FreeRTOS 任务切换时需要保存和恢复哪些上下文硬件自动入栈进入异常时内核硬件自动压到任务栈不用软件写进入 PendSV 异常硬件自动压这 8 个寄存器R0, R1, R2, R3, R12, LR, PC, xPSR软件手动保存PendSV 汇编里代码手动压栈FreeRTOS PendSV handler 汇编手动保存R4, R5, R6, R7, R8, R9, R10, R11这几个是被调用保存寄存器callee-savedC 编译器规定子函数不能破坏RTOS 切换任务必须保存。如果开启 FPU 硬件浮点configUSE_FPU 1还要额外保存浮点寄存器S0~S15FPSCR。注意M4/M7 的浮点是懒保存lazy stacking只有任务真的用了浮点才保存浮点上下文减少开销。切换完整流程简述触发 PendSV 异常硬件自动压栈R0-R3,R12,LR,PC,xPSRPendSV 汇编手动把 R4~R11 压入当前任务栈更新当前任务 TCB 里的pxTopOfStack记录现在栈指针位置调用vTaskSwitchContext()在就绪链表选下一个要运行的任务取出新任务 TCB 里保存的pxTopOfStack恢复栈指针弹出 R4~R11软件恢复异常返回硬件自动弹出 R0-R3,R12,LR,PC,xPSR开始跑新任务8. 什么是优先级反转9. FreeRTOS 如何解决优先级反转问题10. 进程和线程的区别内存资源进程有独立的地址空间。进程和进程之间内存隔离有 MMU 硬件保护。A 进程不能直接访问 B 进程的内存访问非法地址会触发段错误。线程共享所属进程的地址空间。同一个进程里所有线程共享全局变量、堆、文件描述符。线程有自己私有栈、寄存器上下文就是我们前面讲的上下文。线程栈独立其余内存共享。资源分配进程操作系统给进程分配资源内存、文件句柄等资源属于进程。线程本身不拥有独立资源借用进程的资源。调度对象CPU 调度的基本单元是线程不是进程。切换进程进程切换 地址空间切换MMU 页表切换 线程上下文切换开销很大。切换同进程内线程只切换寄存器上下文不用换页表开销小很多。 对应 FreeRTOS 任务切换只保存寄存器没有 MMU 地址空间切换类似线程切换。通信方式进程间通信 IPC管道、消息队列、共享内存需要内核协助、socket跨进程不能直接读内存开销大。同进程线程通信直接读写全局变量但要加互斥锁保护也可以用信号量。简单但是容易竞争。生命周期、崩溃影响进程一个进程崩溃一般不会影响别的进程MMU 隔离。线程同一个进程里如果一个线程访问内存越界崩溃整个进程直接挂掉所有线程一起消亡。和 FreeRTOS 任务的对比面试官很爱追加这一问Linux 线程内核有 MMU进程地址空间隔离同进程线程共享地址空间。 FreeRTOS 任务Task没有 MMU没有独立地址空间所有任务共享全局内存。所以 FreeRTOS 任务更像线程但是没有进程的隔离保护。任何一个任务野指针写坏内存整个系统直接崩所有任务一起挂。这点是嵌入式和 Linux 最大差异。11. 进程通常拥有和管理哪些资源独立虚拟地址空间最重要每个进程有一套独立虚拟内存映射MMU 页表。虚拟地址对每个进程是独立的。这是进程和线程最核心区别。包含代码段 (.text)、数据段 (.data/.bss)、堆。线程没有独立虚拟地址空间共享进程的页表。文件描述符表打开的文件、管道、socket 套接字都记录在进程的文件描述符表。同进程内所有线程共享这张表不同进程 fd 相互独立。信号处理相关资源信号向量表信号的处理函数、信号屏蔽掩码。信号是发给进程的再由内核投递到进程内线程。进程 ID (PID)、用户 ID、组 IDuid/gid、权限信息PID 是进程唯一标识用户 / 组 ID 决定访问权限。工作目录cwd、umask 掩码当前工作路径、新建文件的默认权限掩码进程内所有线程共享。环境变量表env 环境变量。资源限制rlimit内核记录该进程能占用资源上限最大打开文件数、最大栈大小、CPU 时间、内存上限等。句柄 / 内核对象Linux共享内存段、消息队列、信号量System V IPC 对象绑定到进程。注意寄存器、栈不是进程资源寄存器属于线程上下文线程私有栈是在进程虚拟地址空间里分配出来的内存属于进程地址空间的一块内存不是独立进程资源。进程管理独立虚拟地址空间页表、文件描述符表、信号处理配置、PID/UID 等身份权限、当前工作目录、环境变量、资源限额、System V IPC 资源。线程共享进程全部这些资源线程只拥有私有栈、寄存器上下文。12. 线程有哪些私有资源一句话总结线程共享进程绝大部分资源线程私有的是寄存器上下文、线程栈、线程局部存储、线程 ID、线程信号屏蔽字。13. FreeRTOS Task 更接近进程还是线程为什么FreeRTOS 任务更接近线程。因为 FreeRTOS 大多运行在没有 MMU 的 MCU 上所有任务共享同一个地址空间任务切换只保存寄存器上下文不需要切换 MMU 页表。每个任务拥有独立栈和寄存器现场这点和线程一致而进程最重要特征是拥有独立虚拟地址空间和 MMU 内存隔离FreeRTOS 任务没有因此不是进程。FreeRTOS 不存在进程这个内核对象。14. 常见的进程间通信方式有哪些管道匿名管道 pipe半双工只能用于有亲缘关系进程父子进程。基于内核缓冲区流式字节流没有消息边界。只能单向通信读端关闭写会收到 SIGPIPE 信号。命名管道FIFO也是管道文件系统有名字。无亲缘进程也能通信。仍然半双工、字节流。以文件形式存在但数据不在磁盘在内核缓冲区。消息队列System V 消息队列 / POSIX mq内核维护消息链表带消息类型是有边界的消息不是字节流。进程可以按消息类型读取可以异步收发。消息队列在内核进程退出消息不会自动删除要手动清理容易遗留。共享内存 Shared Memoryshmem最高效 IPC。内核把同一块物理内存映射到多个进程的虚拟地址空间。进程直接读写内存不需要内核拷贝数据。⚠️ 仅仅共享内存没有自带同步机制必须配合信号量 / 互斥锁做同步防止竞态。信号Signal异步通知机制传递的只是一个信号编号不带数据早期。内核发给进程的通知。比如 SIGINT (CtrlC)、SIGSEGV 段错误。只能传简单事件通知不适合传大量数据。信号量System V 信号量主要做同步、互斥不是传数据用来保护共享资源实现进程互斥访问。 注意区分FreeRTOS 信号量是任务同步System V 信号量是 IPC内核维护跨进程。Socket套接字通用支持本机进程通信Unix 域 socket也支持跨主机网络通信。Unix 域 socket在内核不走网卡比网络 socket 快可字节流 (TCP) 或者数据报 (UDP)。15. Qt 信号槽属于线程通信还是进程通信答案Qt 信号槽是【同进程内】的通信机制属于线程通信不是进程通信。Qt一句话考点背诵信号槽只能在同一个进程里工作不能直接跨进程。跨进程要另外用 QDBus、QTcpSocket、QLocalSocket 等。16. 实际工程中如何选择合适的 IPC 机制跨主机网络 socket。 本机大数据、低延迟共享内存 信号量。本机 C/S 架构、Qt 多进程Unix 域套接字QLocalSocket。父子简单单向数据流匿名管道。本机任意进程结构化小消息消息队列。只发事件通知、不传业务数据信号。桌面 Qt 服务控制D-Bus。✅精简口述答案面试直接说IPC 就是进程间通信。进程地址空间相互隔离操作系统提供 IPC用来让不同进程交换数据、同步动作。 工程选型优先判断是否跨机器跨主机就用网络 socket。本机场景大数据、低延迟用共享内存但是必须加同步Qt 项目里多进程一般优先 Unix 域套接字简单稳定父子进程简单传输用匿名管道结构化小消息用消息队列信号只用来事件通知不能传大量数据D-Bus 适合桌面应用的服务控制。选型同时权衡性能、开发复杂度、异常处理。17. 共享内存和消息队列分别适合哪些场景消息队列适合传输短小的结构化消息比如控制指令、状态通知。内核负责消息缓存、同步消息有边界收发进程不必同时在线缺点存在两次数据拷贝不适合大数据。共享内存是 IPC 中速度最高的适合大块连续数据比如图像帧、点云。它没有数据拷贝但内核不提供同步必须搭配信号量或者互斥锁来防止多进程同时读写产生竞态开发难度更大要处理锁和异常。18. 共享内存相比其他 IPC 方式有什么优势共享内存最大优势是减少数据拷贝速度是本机 IPC 里最快的。管道、消息队列、域套接字都需要用户态和内核态之间两次数据拷贝而共享内存是多个进程映射同一块物理内存数据写入后其他进程直接访问不需要拷贝。而且没有消息大小的硬性上限带宽高适合图像、点云这类大块数据。19. 消息队列为什么通常存在数据拷贝开销一句话消息队列的缓冲区在内核空间数据要先拷贝进内核再从内核拷贝到接收进程用户空间一共两次拷贝。原理 进程的用户空间和内核空间是隔离的。消息队列System V / POSIX mq的消息缓冲区由内核维护不在应用进程内存里。发送进程调用mq_send()/msgsnd()把用户缓冲区的数据拷贝到内核里的消息队列缓冲区用户态 → 内核态第一次拷贝。接收进程调用mq_receive()/msgrcv()内核把消息从内核缓冲区拷贝到接收进程的用户缓冲区内核态 → 用户态第二次拷贝。对比共享内存共享内存直接映射物理内存到进程虚拟地址没有这两次拷贝所以更快。20. 消息队列中能否只传递指针不能直接传递普通用户态指针。各个进程有独立虚拟地址空间发送进程的虚拟地址对接收进程无效。接收方拿到指针数值之后解引用会触发段错误。只有当指针指向共享内存的时候才可以传递这个地址值但要保证共享内存一直存在这种写法风险较高一般工程不推荐。消息队列常规做法是传递数据本身而不是指针。21. 跨进程传递普通指针后能否直接使用Linux 下每个进程拥有独立的虚拟地址空间。指针存的是发送进程的虚拟地址。这个虚拟地址在接收进程的地址空间里并没有映射到对应的物理内存。接收进程拿到这个地址数值看起来是个数字但解引用访问*ptr时MMU 做地址翻译找不到合法页触发段错误 SIGSEGV。22. 线程间通信与线程同步有什么区别线程间通信目的是线程之间传递数据和消息。因为线程共享进程地址空间可以通过共享变量或者消息队列、Qt 跨线程信号槽完成数据传递。线程同步目的是协调多个线程执行时序解决并发访问共享资源带来的数据竞争。同步工具包括互斥锁、条件变量、信号量等。 简单说通信解决数据怎么传同步解决读写不能撞车。通信常常配合同步一起使用但是同步本身不传递业务数据。23. 常见的线程同步机制有哪些常见线程同步机制包括互斥锁、自旋锁、条件变量、信号量、原子变量、读写锁。互斥锁保证同一时间只有一个线程访问临界资源拿不到锁就阻塞。自旋锁也是互斥但拿不到锁时忙等适合临界区很短的场景。条件变量要配合互斥锁使用用于线程等待某个条件常用于生产者消费者。信号量带有计数器可以控制允许同时访问资源的线程数量二元信号量类似互斥锁但没有锁所有者。原子变量依靠硬件 CAS 指令实现无锁同步只适合简单数值操作。读写锁是读共享写独占适合读多写少场景。24. 中断上下文中为什么不能使用 MutexLinux 中断上下文不能使用 mutex。因为 mutex 锁竞争失败时会让当前执行线程休眠并触发进程调度。但中断上下文不属于任何进程没有 task_struct无法休眠和调度。如果在中断中调用 mutex_lock会造成内核崩溃。中断上下文保护共享资源一般使用自旋锁。25. 中断上下文中为什么可以使用 Spinlock自旋锁获取失败时不会休眠只是循环忙等不触发进程调度。中断上下文不能休眠所以自旋锁可以用。 但是要注意如果是进程上下文和中断共享同一个资源进程侧获取自旋锁时需要关闭本地中断避免本 CPU 中断嵌套造成死锁。并且持有自旋锁期间不能调用任何会休眠的函数临界区代码要简短。26. 原子操作的底层实现原理原子操作依靠 CPU 硬件原子指令保证读修改写这一整个操作不可分割。普通自增会拆成读、改、写三步多线程下会发生竞争。 x86 使用 lock 前缀指令ARM/RISC-V 使用 LL/SC 机制实现 CAS。原子操作不需要 mutex 或者自旋锁属于无锁同步。但是原子操作只能用于单个简单变量不能保护一段临界代码CAS 还存在 ABA 问题。27. CAS 的基本原理CAS 全称比较并交换是硬件支持的原子操作属于乐观锁。它接收内存地址、预期旧值、新值。原子地判断内存当前值是否等于预期旧值如果相等就写入新值返回成功否则返回失败。失败的时候一般循环重试。 CAS 有三个缺点ABA 问题、高竞争下 CPU 空转、只能操作单个变量无法保护多变量的复合逻辑。28. CAS 中 Compare 和 Swap 分别操作什么Compare 就是比较内存当前值和预期旧值判断这段内存有没有被其他线程改动。 Swap 是条件写操作只有比较相等的时候才把新值写入内存。 Compare 和 Swap 整体是硬件原子操作这两步中间不会被其他线程打断。注意不是 A 和 B 互相交换很多人在这里理解错。29. C 中什么是面向对象面向对象是一种编程范式把数据和操作数据的方法封装成类。核心特性是封装、继承、多态、抽象。 封装是把数据和函数打包隐藏内部细节通过访问权限控制对外接口继承实现代码复用派生类可以继承基类成员多态就是同一接口不同实现分为编译期静态多态重载、模板和运行期动态多态动态多态依靠虚函数实现抽象用纯虚函数定义接口抽象类不能创建实例。面向对象关注对象而不是执行步骤便于大型项目扩展和维护。30. C 中什么是多态多态同一个接口不同对象有不同实现一句话**** 一个名字多种形态。C 多态分为静态多态和动态多态。静态多态发生在编译期包括函数重载和模板。动态多态就是通常所说的多态满足继承、基类有虚函数、派生类重写虚函数并用基类指针或引用指向派生类对象。程序运行时通过虚指针查找虚函数表调用对应对象的函数实现同一个接口不同对象不同行为。基类析构一般声明为虚析构避免内存泄漏。1. 虚函数 vs 纯虚函数虚函数有实现可被重写纯虚函数没有实现强制子类必须重写。✅ 虚函数virtual void foo(){}virtual void func() { cout base endl; }有函数体有默认实现派生类可选重写不重写就继承基类这个版本类可以实例化能创建对象✅ 纯虚函数virtual void foo() 0;virtual void func() 0;虚函数使用 virtual 关键字声明拥有函数实现派生类可选择重写基类能够实例化。 纯虚函数写法是virtual func() 0;一般没有默认实现包含纯虚函数的类叫抽象类抽象类不能实例化派生类必须重写纯虚函数否则派生类仍是抽象类。 含有虚函数的类属于多态类型但有虚函数不等于一定发生动态多态。动态多态需要继承、重写虚函数并且使用基类指针或者引用指向派生类对象才会触发运行时绑定。如果直接对象赋值会发生切片此时不会有多态。31. C 多态的实现机制C 动态多态是由编译器通过虚函数表和虚指针实现。含有虚函数的类编译器会为该类生成一张虚函数表存放所有虚函数的地址虚表存放在只读数据段。每个类的对象中会自动增加一个隐藏虚指针 vptr指向本类的虚函数表。当使用基类指针或引用指向派生类对象进行虚函数调用时运行阶段通过对象里的 vptr 找到虚函数表查表取出虚函数地址然后调用对应函数实现运行时绑定。 如果派生类重写虚函数虚表里面对应的函数地址就替换成派生类函数地址没有重写则保留基类虚函数地址。32. 什么是虚函数定义用virtual关键字修饰的成员函数用来实现运行期动态绑定动态多态。33. 虚函数表和虚函数指针是什么一句话虚函数表是存虚函数地址的表虚函数指针是对象里的隐藏指针指向虚函数表。34. 父类指针指向子类对象并调用虚函数时的底层流程p里面存的是子类对象的起始内存地址。通过这个对象起始地址拿到对象内部隐藏的虚指针 vptrvptr 在对象最开头gcc。vptr 保存虚函数表的地址找到Derived 类的虚函数表 vtable不是 Base 的虚表。编译器编译p-func()的时候已经确定func在虚表里的索引下标。用这个下标去虚表对应的槽位取出函数地址。CPU 跳转到取出的这个函数地址执行Derived::func()。35. C 模板是什么模板是 C 实现泛型编程的工具把类型当作参数编写和类型无关的代码编译期根据传入的类型实例化出对应版本代码。36. 为什么 vector 和 vector 可以基于同一个类模板生成类模板只是蓝图不是真正的类。传入不同模板参数编译器就根据这份蓝图实例化出不同的类vector 是类模板。类模板本身不是真实的类只是一份代码蓝图。当使用vectorint时编译器把模板里的 T 替换成 int实例化出 int 版本的 vector 类使用vectorstring时把 T 替换成 string实例化 string 版本 vector 类。两份类代码都是基于同一个模板生成但它们是两个完全独立、互不相关的类型。这个过程发生在编译期是模板实例化。缺点是类型多了会出现代码膨胀。37. C 函数重载是如何实现的C 函数重载是在同一个作用域下函数名相同但参数的个数、类型或者参数顺序不同。C 编译器使用名字修饰name mangling名字改编编译时把参数类型信息编码到函数符号名里生成唯一符号。链接器根据修饰后的名字区分各个重载函数从而实现重载。返回值不参与重载判断因为名字修饰不包含返回值无法区分。函数重载属于编译期静态多态是早绑定。注意区分重载 overload、重写 override二者完全不同。38. C 智能指针有哪些主要解决什么问题智能指针本质是 RAII 封装的类模板内部持有裸指针在对象生命周期结束析构时自动释放堆内存用来手动 new 之后忘记 delete 造成的内存泄漏。1. unique_ptr 独占智能指针独占所有权同一时刻只能有一个 unique_ptr 拥有这块堆内存不能拷贝只能移动 (std::move)。unique_ptrint p1(new int(10)); // unique_ptrint p2 p1; // ❌拷贝禁止编译报错 unique_ptrint p2 move(p1); // ✅移动p1变成空特点开销几乎和裸指针一样没有引用计数性能最好。使用场景资源只需要一个所有者大部分场景优先用 unique_ptr。2. shared_ptr 共享智能指针共享所有权引用计数机制。多个 shared_ptr 可以指向同一块堆内存。内部裸指针 引用计数在堆上单独开一块控制块。每拷贝一份 shared_ptr → 引用计数 1每一个 shared_ptr 析构 → 引用计数 - 1当计数减到 0释放堆内存。shared_ptrint p1 make_sharedint(100); shared_ptrint p2 p1; // 拷贝计数1make_shared推荐优先使用一次性分配内存对象 控制块放一起比shared_ptr(new ...)少一次内存分配异常安全。 缺点引用计数是原子操作线程安全有一点性能开销存在循环引用问题内存不会释放。3. weak_ptr 弱智能指针不拥有资源不增加引用计数。专门用来解决 shared_ptr 的循环引用。weak_ptr 不能直接解引用要调用.lock()尝试提升成 shared_ptr如果资源还存在返回有效的 shared_ptr计数 1如果资源已经释放返回空 shared_ptrweak_ptr 必须配合 shared_ptr 使用不能单独使用。39. unique_ptr 的所有权模型一句话独占所有权模型。同一时刻只能有一个 unique_ptr 拥有这个堆对象的所有权所有权可以转移但不能共享、不能拷贝。所有权谁拥有这个指针谁就负责在它析构的时候 delete 释放内存。40. shared_ptr 的所有权模型共享所有权模型。多个 shared_ptr 可以同时拥有同一块堆资源所有 shared_ptr 共同负责管理这块内存。用引用计数记录当前有多少个 shared_ptr 持有资源。41. shared_ptr 如何判断被管理对象何时释放依靠控制块里的【强引用计数 use_count】。每析构一个 shared_ptr强引用计数减 1当强引用计数降到 0就释放托管对象。注意弱引用计数weak_count只影响控制块什么时候释放不决定托管对象什么时候释放42. 堆和栈的区别一句话栈由编译器自动管理分配在用户空间栈区堆由程序员手动管理分配在堆区。栈stack存放局部变量、函数参数、返回地址。自动分配自动释放。 堆heapnew / malloc出来的内存手动申请手动释放。逐项对比管理方式栈编译器自动管理。函数进入自动分配函数退出栈内存自动回收不用手动释放。 堆程序员手动申请new/malloc、手动释放delete/free忘记释放造成内存泄漏重复释放产生未定义行为。内存大小栈空间很小一般几 MB。栈空间固定申请太大变量直接栈溢出 (stack overflow)。 堆空间大可以到 G 级别受限于进程可用虚拟内存。分配方式栈连续内存向下增长从高地址往低地址分配就是移动栈指针速度极快。 堆内存不保证连续分配需要内存管理器查找空闲块会产生内存碎片分配速度比栈慢。分配效率栈极快只移动栈指针无系统调用。 堆慢需要调用系统 API 找空闲内存有内核开销。生命周期栈随作用域。出作用域直接销毁。 堆从 new 开始直到调用 delete 才释放脱离作用域不自动回收。存储内容栈局部变量函数参数返回地址保存寄存器。 堆程序员主动分配的对象。补充全局 / 静态变量在数据段既不在堆也不在栈面试容易混。精简背诵版口述栈内存由编译器自动管理函数调用时分配函数退出自动回收栈容量小地址从高向低增长分配速度快没有内存碎片存储局部变量、函数参数、返回地址超出容量会栈溢出。堆内存由程序员通过 new/malloc 手动申请delete/free 手动释放堆空间大生命周期由程序员控制分配时查找空闲块速度比栈慢容易产生内存碎片。堆和栈都属于进程的虚拟地址空间。注意全局静态变量存放在数据段不属于堆也不属于栈。43. C/C 中堆内存如何申请和释放44. 动态内存频繁申请和释放会产生哪些问题频繁申请释放动态内存首先会产生外部碎片空闲内存总和充足但都是分散小块缺少连续大块大内存分配会失败。其次会产生内部碎片因为 chunk 头和对齐带来块内空间浪费降低内存利用率。 其次性能下降堆管理器查找空闲块、分割、合并 chunk 都需要开销碎片越多开销越大严重时频繁触发系统调用。 同时更容易引入各类内存 bug内存泄漏、野指针、重复释放、堆越界破坏堆结构造成程序崩溃。还可能引发内存抖动影响系统整体性能。45. 什么是内存碎片内存碎片分为内部碎片和外部碎片。 内部碎片堆分配时因为内存对齐和 chunk 头分配给你的块比你申请的更大多余的空间在块内部无法被其他请求复用。 外部碎片多次分配释放之后空闲内存被已使用的块切分成零散小块。空闲内存总和足够但是没有连续的大块无法满足大内存申请。C/C 默认堆管理器不能压缩堆内存因为裸指针无法自动更新地址。频繁申请释放小块内存很容易产生外部碎片。46. 内存池为什么能够减少内存碎片内存池预先调用 malloc 一次性申请一块连续的大堆内存由用户代码自己切分、管理小块。业务申请释放内存时直接从池内获取或者归还减少频繁调用 malloc 和 free。这样就避免了系统堆管理器反复分割、合并 chunk减少全局堆上的外部碎片。固定大小内存池效果最好不会产生小块残片但会带来内部碎片。内存池不是彻底消灭碎片碎片被限制在池内部当内存池整体销毁时可以一次性释放整块内存。代价是内存池占用的内存不会自动归还操作系统可能存在内存驻留同时增加实现复杂度。47. C/C 程序编译链接后通常包含哪些段.text 代码段存放程序机器指令只读可执行.rodata 只读数据段存放字符串常量、全局 const 只读数据只读.data 数据段存放已经初始化的全局、静态变量可读可写.bss 段存放未初始化全局和静态变量。在 ELF 文件不占用存储空间加载到内存时操作系统清零。48. .text 段通常存放什么内容.text 段是代码段存放编译之后的可执行机器指令也就是各个函数对应的二进制指令。属性是只读、可执行防止运行过程修改代码。注意区分只读常量字符串放在.rodata 段不是.text。.data 存放已初始化全局静态变量.bss 存放未初始化全局静态变量。49. .data 段通常存放什么内容.data 段存放已经初始化、且初始值非 0 的全局变量和静态变量。50. .bss 段通常存放什么内容全局 /static未初始化 或者初始化为 0ELF 文件不存数据只记录大小加载时内核自动清零节省文件体积。51. 全局 const 变量通常存放在哪里全局 const 变量一般放在.rodata只读数据段。52. .bss 段为什么能够减小可执行文件或固件镜像的占用.bss 只记录需要的内存总大小不在 ELF 文件中保存一堆 0所有清零工作交给内核在程序加载的时候做所以可执行文件不用存储大量 0 字节减小镜像体积。53. 静态链接和动态链接的区别静态链接链接阶段把库代码拷贝进可执行文件动态链接运行时才加载共享库程序文件只保留库的符号信息不拷贝库代码。54. 影石蓝牙图传项目主要做了什么55. 蓝牙图传出现延迟、卡顿是如何定位问题的蓝牙图传卡顿延迟采用分层定位。首先本地测试发送端图像采集编码判断瓶颈在图像侧还是传输链路。 如果本地画面流畅问题就在蓝牙链路查看 RSSI 信号强度检查距离遮挡抓取 HCI 日志查看丢包与重传判断 2.4G 频段干扰检查蓝牙 MTU、连接间隔、流控参数看是否带宽不足或参数不合理。 如果发送端本地就掉帧瓶颈在摄像头采集、编码 CPU 负载过高。接收端还要排查解码 CPU 负载、缓冲区溢出、渲染耗时。 固定延迟多来自编码、传输、解码固有耗时抖动卡顿多是无线丢包重传延迟持续累积一般是接收处理速度跟不上发送速度。56. MTU 对蓝牙图传性能和卡顿问题有什么影响BLE 的 MTU 定义 ATT 层一次最大数据包长度。MTU 过小一帧图像数据要拆成大量小包协议开销变大、吞吐下降更容易出现卡顿MTU 足够大可以减少分包提升吞吐量降低延迟但 MTU 不是越大越好无线链路差时大包更容易丢包丢一个大包代价更高。57. 图传中的“码流速率”具体指什么码流速率码率bitrate指编码之后每秒钟输出的压缩图像比特数量单位通常是 bps、kbps、Mbps。58. ESP32 FreeRTOS 项目的整体设计59. FreeRTOS 项目中为什么这样设计任务优先级60. FreeRTOS 项目中为什么进行任务绑核多核芯片上任务绑核就是把指定任务固定在某一个 CPU 核心上运行。目的是减少跨核开销、提升实时性、规避跨核资源竞争61. ADC 任务与 LVGL 任务之间如何通信ADC 任务负责采样把采集到的数据传递给 LVGL 任务优先使用队列也可以用信号量 全局变量必须做好互斥保护避免数据撕裂。62. 为什么使用覆盖队列普通队列是 FIFO队列满时发送会阻塞 / 发送失败覆盖队列的设计目标是只保留最新的 1 份有效数据旧数据直接被新数据覆盖。适合 UI 类场景消费者只关心最新值历史旧数据没有意义。63. 项目中还使用了哪些任务间同步方式64. 实际项目中是否使用过智能指针65. C 和 C 在实际项目中哪个使用得更多66. 项目使用的是哪个 C 标准67. 目前是否已经离职