Qt / C++ 工业软件岗位模拟面试复习手册

面试手册

Qt / C++ 工业软件岗位

模拟面试复习手册

基于你的上位机、Modbus TCP、Pistache 与 PostgreSQL 项目经历整理

**目标:**把前面已经问过的问题收拢成可复述答案。不要逐字背诵,优先记住每题的结构和关键词,再换成自己的表达。

使用方法

**第一遍:**只看每题的”记忆关键词”,先建立知识框架。

**第二遍:**遮住推荐回答,每题控制在 60 到 90 秒,用自己的话说一遍。

**第三遍:**重点复习红色”易错点”和最后的附件代码核对,避免夸大实现。

**面试现场:**先给结论,再讲实际做法、异常处理和结果;不会的底层细节可以说明使用过成熟库,但要讲清原理。

你的项目主线

项目面试时要形成的主线
工业电解槽数据采集与生产追溯平台Pistache 接口接收 → 元数据校验 → 有界队列 → 多 Worker 批量入库 → PostgreSQL 事务与失败降级 → LISTEN/NOTIFY + SSE 推送
基于 Modbus TCP 的电解制氢上位机7 台 PLC 独立通信 → 寄存器合并读取 → 数据解析与报警 → SQLite 离线缓存 → RESTful API 上传 → 曲线、查询与导出

**谨慎表述:**附件代码中的应用层 WAL/本地持久化仍是 TODO;队列满时当前实现会丢弃并计数。面试时应描述真实现状,再说明计划如何完善。

第一部分 开场、Qt 多线程与数据可靠性

本部分共 10 题。回答顺序建议:结论 → 设计/实现 → 异常处理 → 项目实际边界。

01 请做一个与岗位匹配的自我介绍。

**推荐回答:**您好,我之前主要负责 Qt/C++ 上位机和 C++ 后端开发。上位机项目主要面向工业设备数据采集、工艺流程展示、数据保存与下发,并与 MES 或云端系统通过 HTTPS/RESTful API 对接。我使用过 Modbus TCP、RS232 和 RS485,其中对 Modbus TCP 更熟悉。项目中我负责过上位机和后端关键模块,包括多 PLC 采集、报警、曲线、离线缓存、数据上传,以及 Pistache、PostgreSQL、JWT 和 SSE 相关开发。我希望继续从事工业自动化软件开发,并把通信稳定性、数据可靠性和系统可维护性做好。

**记忆关键词:**Qt/C++、设备通信、数据可靠性、MES/云端对接、个人职责与成果

**易错点:**补上实际工作年限、设备类型和可量化成果。不要把团队完成的内容全部说成个人独立完成,也不要在简历写 Python 却完全不准备。

02 Qt 上位机如何拆分线程,并保证采集、显示、保存和上传的一致性?

**推荐回答:**我会把设备通信、数据处理、本地存储和 HTTPS 上传分别封装为 Worker,并通过 QObject::moveToThread() 放入工作线程。UI 线程只负责交互和展示,线程之间使用队列信号槽传递带设备编号、时间戳和序列号的数据对象。采集数据先可靠落盘,再进入上传队列;数据库写入使用事务,服务端以唯一业务 ID 做幂等。UI 只显示数据快照,不直接跨线程访问 Worker 内部状态。下发指令使用串行命令队列,并设置超时、应答和重试。

**记忆关键词:**Worker、QueuedConnection、本地先落盘、事务、唯一 ID、命令队列

**易错点:**不要只说”耗时操作放线程”。还要说明数据所有权、失败处理、落盘时机、幂等和安全退出。

03 Modbus TCP 设备突然断开,如何检测、重连和告警?

**推荐回答:**每台设备的 Socket 由对应通信 Worker 统一管理,避免心跳和正常读写同时操作同一个 Socket。模块维护未连接、连接中、已连接和重连中的状态。连续心跳失败、Socket 断开或读写超时后进入重连流程,采用递增间隔,例如 1、2、5、10 秒。重连成功后先读取关键寄存器确认设备可用,再恢复采集。连续多次失败时,工作线程通过信号通知 UI 告警,同时后台继续低频重试。高风险控制指令断线后不能盲目自动重发。

**记忆关键词:**单线程管理 Socket、状态机、退避重连、恢复确认、UI 信号

**易错点:**子线程不能直接弹窗;固定无间隔循环重连会造成资源压力;心跳线程与业务线程并发操作同一 Socket 可能导致响应错乱。

04 Modbus TCP 如何匹配请求与响应,并处理半包、粘包和迟到响应?

**推荐回答:**Modbus TCP 的 MBAP 头包含 Transaction ID,客户端发送请求时分配事务号,设备响应时原样返回。简单场景下,同一设备同一时刻只保留一个未完成请求;允许并发时,则用 Transaction ID 维护待响应表。TCP 是字节流,readyRead 不等于一帧。我会把数据追加到接收缓冲区,至少收到 7 字节后读取 MBAP 的 Length 字段;数据不足就继续等待,包含多帧则循环拆帧。超时请求从待响应表移除,之后到达的旧响应按事务号丢弃,并校验协议标识、长度、单元地址和功能码。

**记忆关键词:**Transaction ID、MBAP Length、接收缓冲区、循环拆帧、迟到响应丢弃

**易错点:**Direct readAll() 后把结果当成完整报文不可靠。Length 表示从 Unit Identifier 开始的剩余长度,完整 ADU 长度通常按 6 + Length 计算。

05 Qt 的 DirectConnection、QueuedConnection 和 AutoConnection 有什么区别?

**推荐回答:**DirectConnection 是同步直接调用,槽函数在发出信号的当前线程执行,槽返回后发射位置才继续。QueuedConnection 把调用投递到接收对象所属线程的事件队列,属于异步调用,要求该线程有事件循环。AutoConnection 在发出信号时判断:当前执行线程与接收对象所属线程相同则直接调用,否则使用队列调用。工作线程向 UI 发送数据时,我会使用默认连接或明确使用 QueuedConnection,保证 UI 更新发生在主线程。

**记忆关键词:**Direct 同步、Queued 异步、接收对象线程、事件循环

**易错点:**DirectConnection 不是异步。AutoConnection 比较的是发信号时的当前线程与接收对象所属线程,不应简单理解成对象创建线程。

06 Worker 使用 moveToThread() 时能否有父对象?如何安全释放 Worker 和 QThread?

**推荐回答:**准备 moveToThread() 的 Worker 不能已有父对象,因为 QObject 子对象必须与父对象属于同一线程。通常创建无父对象的 Worker,移动到工作线程,并把 thread::started 连接到 Worker 的启动槽。停止时先通知 Worker 停止任务并关闭定时器、Socket 等资源,再由 Worker 发出 finished;finished 连接到 thread::quit 和 worker::deleteLater,thread::finished 连接到 thread::deleteLater。程序退出需要同步等待时,先请求停止,再 quit 和 wait,不能在线程仍运行时直接 delete QThread。

**记忆关键词:**无父对象、停止业务、deleteLater、quit、wait

**易错点:**不要在主线程直接 delete 仍属于工作线程的 Worker;Worker 内的定时器和 Socket 最好在进入目标线程后创建。

07 数据库连接能否在主线程创建后交给保存线程使用?你实际如何设计?

**推荐回答:**数据库连接具有线程使用边界,不能在主线程创建后让多个保存线程随意共享。若上位机本地使用 QSqlDatabase,应在数据库线程内部创建独立的命名连接,并在线程结束前关闭和移除。我的实际架构是上位机通过 HTTPS/RESTful API 把数据交给 C++ 后端,由后端统一完成鉴权、校验和 PostgreSQL 入库;上位机本地只维护 SQLite 离线队列。这样网页端和上位机可以复用同一套服务端接口。

**记忆关键词:**每线程独立连接、后端统一入库、本地 SQLite 缓存

**易错点:**理由不是”数据库连接耗时”,而是连接的线程归属、事务边界和并发安全。HTTPS 本身也不能阻塞 UI。

08 HTTPS 上传时网络断开,怎样保证数据不丢失且不重复?

**推荐回答:**采集数据先保存到本地 SQLite,记录设备编号、持久化序列号、采集时间、业务内容和上传状态。只有本地事务提交成功后,才认为数据已可靠接收。上传线程读取待上传记录,服务端明确返回成功后才更新为已上传;超时或断网则保留并按退避策略重试。每条数据使用设备编号加持久化序列号生成唯一业务 ID,服务端建立唯一约束或使用 ON CONFLICT,处理”服务端已入库但客户端没收到响应”造成的重复上传。

**记忆关键词:**本地先落盘、状态字段、ACK 后更新、幂等 ID、退避重试

**易错点:**时间戳不能单独作为唯一标识;序列号必须持久化,不能每次重启从零开始。

09 突然断电时,为什么选择 SQLite 而不是普通文件或 CSV?

**推荐回答:**SQLite 支持事务、结构化查询和状态管理,更适合实现待上传队列。采集写入与状态更新都在事务中完成,使用单写线程,必要时启用 WAL 模式和 busy_timeout。设备编号与持久化序列号组成唯一键,重启后根据上传状态继续处理。CSV 主要用于操作人员查看和导出,不能替代事务型存储,因为写入过程中断电仍可能留下半行或不完整文件。

**记忆关键词:**SQLite 事务、单写线程、WAL 模式、恢复未上传记录、CSV 仅导出

**易错点:**CSV 备份不等于断电保护;SQLite 的 WAL 是 SQLite 自身的日志模式,不要与应用层持久化队列混为一谈。

10 岗位要求 Python,但项目主要是 C++,如何回答?

**推荐回答:**我的生产项目主要使用 C++,Python 不是核心开发语言。目前我掌握 Python 基础语法和常见脚本能力,能够用于日志处理、数据格式转换、接口验证或自动化辅助。如果面试岗位需要 Python,我会重点补充 requests、pandas、pytest 以及串口和 Modbus 相关库。对于没有实际做过的生产功能,我会明确说明学习程度,不把练习项目包装成上线经验。

**记忆关键词:**坦诚边界、基础脚本、requests、pandas、pytest、快速迁移能力

**易错点:**只保留与你真实经历一致的内容。如果确实写过脚本,应补充脚本解决了什么问题和产生了什么结果。

第二部分 Pistache、PostgreSQL 与后端数据链路

本部分共 8 题。回答顺序建议:结论 → 设计/实现 → 异常处理 → 项目实际边界。

11 metadata.json 保存什么?如何生成 SQL 并防止注入?

**推荐回答:**metadata.json 保存业务字段到数据库字段的映射,以及类型、是否必填、是否允许新增或修改等约束。收到请求后,服务端逐个校验字段是否在元数据白名单中,并校验类型、长度、范围和必填项。SQL 的表名、列名和排序字段只能来自白名单;具体值使用 PostgreSQL 参数占位符和参数绑定,例如 $1、$2,不能直接拼接用户输入。查询、更新、删除也遵循相同原则,敏感字段还要做写权限限制。

**记忆关键词:**元数据白名单、类型校验、列名受控、值参数绑定、写权限

**易错点:**值可以参数化,表名和列名通常不能靠参数绑定,因此必须来自白名单。只校验字段”存在”还不够,还要校验是否允许写入。

12 内存队列、多 Worker 和批量入库如何设计?队列满或数据库失败怎么办?

**推荐回答:**HTTP 线程作为生产者,完成鉴权和字段校验后,把带业务 ID 的任务放入有容量限制的线程安全队列。Worker 使用条件变量等待,达到批量大小或刷新超时时间后取出一批,在持锁区域内只搬移数据,释放锁后执行数据库事务。队列满时不能无限占用内存:已经实现可靠落盘时可写入应用层持久化队列,否则应限流并返回 429 或 503。数据库失败时事务回滚,任务保留或进入重试/失败队列,使用幂等键避免重放产生重复数据。

**记忆关键词:**有界队列、批量大小或超时、短持锁、背压、持久化重试、幂等

**易错点:**PostgreSQL WAL 是数据库内部预写日志,不是应用层队列。你当前附件代码在队列满时直接 dropped++,本地落盘仍是 TODO,面试时不要说已经实现。

13 批量插入发生唯一键冲突或单条坏数据时如何处理?

**推荐回答:**入库前先做类型、必填、长度和范围校验。唯一键重复通常直接使用 PostgreSQL 的 ON CONFLICT DO NOTHING,不必因为重复记录让整批失败。若批量事务因约束错误或坏数据整体失败,我会先 rollback,再降级为小批次或逐条事务,隔离具体坏数据;正常数据继续入库,异常数据写入失败记录并保留原始内容与原因。如果业务要求整批原子成功,则必须整体回滚,不能部分写入。

**记忆关键词:**ON CONFLICT、事务回滚、逐条降级、失败队列、业务原子性

**易错点:**先区分”重复数据”和”非法数据”。当前代码的 insertRowsForTable 返回 rows.size(),即使 ON CONFLICT 忽略了部分数据,也可能把 processed 统计得过高。

14 PostgreSQL LISTEN/NOTIFY 如何通过 SSE 推送到浏览器?为什么不用 WebSocket?

**推荐回答:**PostgreSQL 发生报警变化后通过 NOTIFY 告知后端,后端的监听连接收到事件,再查询数据库获取完整报警内容,并通过 SSE 长连接推送给浏览器。SSE 是基于 HTTP 的服务端到客户端单向推送,浏览器用 EventSource 接收;客户端提交数据仍使用普通 HTTP。WebSocket 是全双工,更适合双方都需要频繁主动发消息的场景。我们的需求主要是服务端推送报警,因此 SSE 更简单。断线后通过事件 ID、Last-Event-ID、补查接口和心跳处理漏消息与重连。

**记忆关键词:**NOTIFY 触发、后端查询、SSE 单向、EventSource、Last-Event-ID

**易错点:**SSE 不是浏览器收到信号后必须再拉取;它本身可以携带完整事件。LISTEN/NOTIFY 适合作为触发信号,不应作为可靠消息存储。

15 JWT 中保存什么?如何校验并防止修改 access_level 提权?

**推荐回答:**登录时服务端使用密码哈希校验账号密码,成功后签发 JWT。Payload 保存用户 ID、账号、角色或权限等级、签发时间、过期时间,以及必要的 issuer、audience 和 jti,但不保存密码。客户端通过 Authorization: Bearer 发送 Token;服务端校验签名、有效期、签发方和受众后,再根据已验证的角色执行接口鉴权。客户端自行修改 access_level 会导致签名失效,服务端也不能信任请求体中传来的权限等级。高风险操作可根据用户 ID 再查数据库,并通过短有效期、Token 版本或黑名单实现撤销。

**记忆关键词:**密码哈希、签名、exp/iss/aud/jti、服务端权限判断、撤销机制

**易错点:**JWT 默认只是编码,不是加密,绝对不要放明文密码;Token 本身通常也不需要再放进 Payload。

16 Pistache 的 HTTP 线程、业务 Worker 和数据库 Worker 如何协作?接口何时返回?

**推荐回答:**HTTP 线程负责接收请求、鉴权、参数校验和生成请求 ID,然后把任务投递到有界队列。业务 Worker 完成数据转换、分组和批量组织,再由数据库 Worker 执行事务。异步模式下,任务可靠进入内存队列或本地持久化队列后,接口返回 202 Accepted 和请求 ID,含义是已接收、正在处理,而不是已经入库。最终结果通过状态查询或 SSE 获取。参数错误返回 400,队列满返回 429 或 503。如果业务要求入库成功后才能继续,则采用同步接口,但要设置明确超时。

**记忆关键词:**接收校验、请求 ID、有界队列、202 Accepted、最终状态

**易错点:**数据尚未入库时不要返回”保存成功”。若仅进入易失内存队列,也要评估进程崩溃导致的数据丢失。

17 PostgreSQL 连接池为什么必要?连接能否被多个 Worker 同时使用?

**推荐回答:**连接池复用已经建立的数据库连接,减少 TCP 建连和认证开销,同时支持多个后端 Worker 并发执行 SQL。一个连接同一时间只借给一个 Worker,避免事务边界和结果集错乱;使用完成后通过 RAII 自动归还。池耗尽时任务只能有界等待,并设置超时,不能无限挂起。归还前确认事务已提交或回滚,失效连接要销毁重建。池大小结合数据库最大连接数、后端实例数量和 SQL 耗时确定,并为运维任务预留连接。

**记忆关键词:**连接复用、每次独占、有界等待、RAII 归还、连接健康检查

**易错点:**多个上位机连接的是 HTTP 服务,后端 Worker 才从数据库连接池借连接。连接数不是越大越好。

18 一条 PostgreSQL 查询从 50ms 变成 3 秒,如何定位?

**推荐回答:**先通过请求 ID 和应用日志定位 SQL、参数和分段耗时,把连接池等待、网络耗时与数据库执行时间分开。再查看慢查询日志和 pg_stat_statements,判断是偶发还是持续变慢。对安全的查询使用 EXPLAIN (ANALYZE, BUFFERS) 查看全表扫描、索引未命中、估算偏差、排序落盘和 Join;通过 pg_stat_activity 和锁视图检查长事务或阻塞;同时观察 CPU、内存、磁盘 I/O、连接数、数据量和统计信息。处理后重新对比执行计划和耗时。

**记忆关键词:**分段耗时、慢日志、pg_stat_statements、执行计划、锁、系统指标

**易错点:**EXPLAIN ANALYZE 会真实执行 SQL,对 UPDATE、DELETE 或重型查询必须谨慎,生产环境先评估影响。

第三部分 多 PLC 采集、报警、曲线与配置

本部分共 8 题。回答顺序建议:结论 → 设计/实现 → 异常处理 → 项目实际边界。

19 一套上位机连接 7 台 PLC,线程、Socket 和采集周期如何设计?

**推荐回答:**我为每台 PLC 创建独立通信 Worker 和 Socket,每台设备在线程内维护自己的定时器与串行请求队列。某台 PLC 读超时只影响本设备,不阻塞其他设备。每轮数据带设备编号、采集时间和周期序号,并设置明确超时和周期截止时间。业务需要 7 台设备形成快照时,由上层按周期序号或时间窗口聚合:全部返回或达到截止时间后发布结果,超时设备标记缺失或沿用上一周期值,同时报警,而不是无限等待最慢设备。

**记忆关键词:**每设备 Worker/Socket、串行请求、故障隔离、周期序号、截止时间

易错点:“速度都差不多,所以不会变慢”不是容错设计。独立线程解决隔离问题,但线程数量、事件循环和资源释放仍需管理。

20 41 组寄存器如何减少请求次数,并处理数据类型和字节序?

**推荐回答:**我先按 PLC、功能码和地址排序,把地址连续或间隔较小的点合并成读取区间,单次功能码 03/04 读取不超过 125 个寄存器。地址跨度过大、功能码不同或设备不允许跨区读取时要拆分,不能为了凑满 125 个而读取非法区域。关键工艺数据高频采集,状态类数据可低频轮询。同一 PLC 内请求串行,7 台 PLC 之间并行。解析按配置处理 uint16、int32、float、字节序、寄存器顺序、比例系数和偏移量。

**记忆关键词:**地址排序、连续区间、125 寄存器、分级周期、类型与字节序配置

**易错点:**上限是 125 个 16 位寄存器,不是 125 字节;一个寄存器为 2 字节。合并读取必须满足同一功能码和合法地址范围。

21 两个寄存器解析出的 float 明显错误,如何排查?

**推荐回答:**先以十六进制记录请求地址、功能码、数量和原始响应,确认请求响应匹配,再核对 PLC 手册中的 0-based/1-based 地址差异。让 PLC 写入 1.0 或 100.0 等已知值,分别验证字节顺序和两个寄存器的先后顺序。原始值正确但浮点结果异常时检查 ABCD、BADC、CDAB、DCBA 等组合、符号位和数据类型;结果只是固定倍数偏差时再检查缩放系数与偏移。最后把地址、类型、字节序和缩放写入配置,并用已知值做解析单元测试。

**记忆关键词:**原始十六进制、地址基准、已知值、四种顺序、缩放、单元测试

**易错点:**QDebug 只是观察工具,不是排查方法。生产环境应使用可控级别的结构化日志,避免高频打印影响采集周期。

22 PLC 提供报警位时,如何实现边沿触发、去重和恢复?

**推荐回答:**我读取 PLC 报警位得到当前状态,并保存上一轮状态。0 变 1 是上升沿,只创建一次报警事件;持续为 1 时只更新持续状态,不重复写记录。1 变 0 是恢复沿,写入恢复时间并更新页面。为了避免通信抖动,可要求连续多次采样一致或持续超过阈值后再确认。报警记录包含设备编号、报警编码、等级、开始时间、恢复时间和确认状态。程序重启后从本地数据库恢复未结束报警,避免再次生成重复事件。

**记忆关键词:**上一状态、上升沿、恢复沿、去抖、未结束报警恢复

**易错点:**只判断当前 0/1 只能显示状态,不能形成可靠报警事件。报警日志和 CSV 写入应异步,不得阻塞通信线程。

23 300 多个点位和 QCustomPlot 实时曲线如何避免 UI 卡顿?

**推荐回答:**采集线程只解析数据并写入缓存,不直接操作 QCustomPlot。UI 线程用定时器按固定频率批量读取最新快照,例如每 50 到 100ms 刷新一次,而不是每个点到达就 replot。多条曲线更新完成后只调用一次 queued replot。每条曲线只保留固定时间窗口或固定点数,使用环形缓冲或删除旧数据,避免无限增长。历史高频数据按显示像素进行最大值、最小值或平均值降采样;未显示或未选中的曲线不持续重绘。

**记忆关键词:**采集绘图解耦、批量刷新、一次 replot、固定窗口、降采样

**易错点:**300 多个采集点不等于 300 多条同时绘制的曲线。应准确说明实际显示数量、采样频率和保留窗口。

24 QXlsx 或 CSV 大数据导出如何避免卡住界面?

**推荐回答:**导出由独立 Worker 执行,UI 只发起任务、显示进度并接收完成或失败信号。开始前获取稳定的数据快照,再按批次查询和写入。大数据量优先用 CSV 流式逐行写入,不一次性加载全部数据;正确转义逗号、引号和换行,并根据使用场景处理 UTF-8 BOM。先写临时文件,成功后再替换目标文件,避免留下不完整文件。必须生成带格式报表时,在后台使用 QXlsx,并限制单次导出量、支持进度和取消。

**记忆关键词:**后台 Worker、稳定快照、批量查询、CSV 流写、临时文件、进度取消

**易错点:**CSV 不能完全替代 Excel:简历既然写了 QXlsx,就要说明何时用 xlsx、何时用 CSV。线程切换后仍不能直接从导出线程操作 UI。

25 配置文件热更新如何保证采集线程不会混用新旧配置?

**推荐回答:**UI 修改地址、周期或阈值后,先校验格式、范围和业务关系,再组装一份带版本号的完整不可变配置,通过队列信号发送给对应 Worker。Worker 在自己的线程中接收配置,并在当前 Modbus 请求完成、下一轮开始前整体替换,保证一轮采集只使用一个版本。定时器也由 Worker 自己重设。配置持久化采用临时文件写入成功后原子替换;校验或应用失败则保留旧配置并通知 UI。多个线程确实共享时,使用不可变快照或互斥锁。

**记忆关键词:**整份配置、版本号、QueuedConnection、安全边界、原子替换、失败保留旧版

**易错点:**UI 线程直接修改 Worker 正在读取的普通变量会产生数据竞争;“数值一改立刻推送”没有说明安全生效边界。

26 Qt 项目使用 qmake,后端使用 CMake,如何说明构建经验?

**推荐回答:**Qt 上位机当时使用 qmake,通过 .pro 管理源码、Qt 模块、C++17、第三方库和平台链接参数。C++ 后端使用 CMake,顶层 CMakeLists 定义项目和 C++ 标准,再按 HTTP、数据库、认证和业务模块划分子目录。依赖通过 find_package、pkg-config 或导入目标接入,并使用 target_include_directories 和 target_link_libraries 作用于具体 target。Debug 保留符号、断言和详细日志,Release 开启优化,并保证第三方库与运行库配置一致、输出目录隔离。

**记忆关键词:**qmake .pro、target 级配置、依赖管理、Debug/Release、Qt 自动代码生成

**易错点:**不要把”会编写 CMake 文件”停留在一句话。Qt6 新项目可提 AUTOUIC、AUTOMOC、AUTORCC,但不要冒充已经完成过迁移。

第四部分 现代 C++ 与容器基础

本部分共 3 题。回答顺序建议:结论 → 设计/实现 → 异常处理 → 项目实际边界。

27 什么是 RAII?如何管理锁、Socket、文件和数据库连接?

**推荐回答:**RAII 是资源获取即初始化:把资源生命周期绑定到对象生命周期,构造时获取,析构时释放。栈对象离开作用域时会自动析构,因此即使提前 return 或抛异常,也能释放资源。互斥锁使用 std::lock_guard 或 std::unique_lock;独占动态资源优先 unique_ptr;文件使用 fstream;数据库连接可用 RAII 句柄在析构时自动归还连接池;Socket 封装类在析构时关闭。Qt QObject 还要遵守父子对象树和线程归属,通常由父对象释放或在所属线程调用 deleteLater。

**记忆关键词:**构造获取、析构释放、异常安全、lock_guard、RAII 连接句柄

**易错点:**RAII 不只是”资源初始化”,核心是所有权和自动释放。不要把所有 QObject 都交给普通 shared_ptr。

28 unique_ptr、shared_ptr 和 weak_ptr 分别适用于什么场景?

**推荐回答:**unique_ptr 表示唯一所有权,不能拷贝但可以移动,适合 Worker、Socket 或其他独占资源。shared_ptr 表示共享所有权,最后一个持有者释放时对象销毁,适合生命周期确实需要共同持有的对象。weak_ptr 不拥有对象,使用前通过 lock() 临时取得 shared_ptr,常用于打破循环引用或实现观察者。默认优先栈对象或 unique_ptr,只有无法明确单一所有者时才使用 shared_ptr,因为共享引用计数有运行和内存开销,也会模糊生命周期关系。

**记忆关键词:**唯一所有权、移动、共享所有权、引用计数、非拥有观察、循环引用

易错点:“多个指针指向同一地址”不一定需要 shared_ptr;关键是多个对象是否真正共同拥有生命周期。

29 如何选择 QVector、QList、QHash、std::vector 和 std::unordered_map?线程安全吗?

**推荐回答:**QVector 和 std::vector 都适合连续存储、动态扩容和随机访问,通常是顺序数据首选;已知规模时 reserve 可减少扩容。QList 不能按”是否确定大小”来选择,Qt 5 与 Qt 6 的实现差异还要结合项目版本。QHash 和 unordered_map 适合按设备 ID、报警编号或请求 ID 做平均 O(1) 查找;需要按键排序时使用 QMap 或 std::map。Qt 接口和信号槽数据常用 Qt 容器,通用后端模块倾向 STL。它们不支持无保护并发读写:同时只读且无人修改通常可以,有写操作时要加锁或采用单线程所有权和消息传递。

**记忆关键词:**连续存储、reserve、哈希查找、Qt 版本、并发读写需同步

**易错点:**哈希容器不是”值的一对一关系”,多个键可以对应相同值。解锁后不要继续使用可能因容器修改而失效的内部引用或指针。

第五部分 生产者消费者与附件代码

本部分共 2 题。回答顺序建议:结论 → 设计/实现 → 异常处理 → 项目实际边界。

30 生产者消费者队列中,mutex、condition_variable 和停止流程如何使用?

**推荐回答:**生产者 enqueue 时用 lock_guard 保护多个线程共享的队列;消费者使用 unique_lock 配合 condition_variable::wait_for。wait_for 等待时会原子地释放互斥锁,让生产者可以入队;唤醒后重新获得锁,并用”队列达到批量大小、刷新超时或系统停止”的谓词重新判断,以处理虚假唤醒。消费者持锁时只把共享队列的数据移动到线程局部 batch,随后解锁并执行数据库操作。退出时把原子 stopping 设为 true,notify_all 唤醒所有消费者,拒绝新任务,Worker 按既定策略排空或持久化剩余任务,最后 join。

**记忆关键词:**共享队列、unique_lock、谓词、防虚假唤醒、短持锁、notify_all、join

**易错点:**锁保护的是共享队列 q,不是 backgroundLoop 中每个线程自己的 batch。数据库操作不能在持有队列锁时执行。

31 为什么 condition_variable 使用 unique_lock,而不是 lock_guard?

**推荐回答:**condition_variable::wait 或 wait_for 需要在等待前自动解锁 mutex,并在返回前重新加锁,因此需要一个支持显式 unlock 和 lock 的 unique_lock。lock_guard 只提供作用域内固定持锁,不允许中途释放和重新获取,所以不能直接交给 condition_variable 等待。谓词版本相当于循环检查条件,可抵抗虚假唤醒;超时返回后也必须在锁保护下再次检查共享状态。

**记忆关键词:**unique_lock 可解锁/重锁、wait 原子释放、谓词循环、虚假唤醒

**易错点:**wait_for 的时间到达不等于一定有数据,通知到达也不等于条件必然成立,始终以谓词和共享状态为准。

附录 A 附件代码的面试前核对清单

这部分不是要求你现场主动暴露问题,而是帮助你在被追问代码时保持口径准确。推荐表达是:“当前版本这样实现,风险是……,我会这样完善……”。

A1. 共享队列退出判断

**核对:**附件中 while (!stopping.load() || !q.empty()) 在未持有 mtx 时读取 q.empty(),生产者可能同时修改 q,存在数据竞争。应把”停止且队列为空”的判断放进持锁的 pullBatch/等待逻辑,并让函数明确返回是否退出。

A2. 批量触发条件

**核对:**当前 wait_for 的谓词是 stopping || !q.empty(),只要来一条数据就会立即唤醒,不会真正等待凑满一批。若需求是”满批或超时”,需要记录批次等待起点,并以 q.size() >= MAX_BATCH、停止或超时作为触发条件。

A3. 队列满时的数据可靠性

**核对:**当前 enqueue 在 q.size() >= MAX_QUEUE 时增加 dropped 并直接 continue,未执行本地 WAL。processBatch 中的本地缓存也只是 TODO。因此当前真实行为是丢弃并计数,不能在面试中描述为已经可靠落盘。

A4. enqueue 返回值与停止状态

**核对:**enqueue 当前无论是否有数据成功入队都返回 true,而且 stopping 后仍可能接收新数据。建议返回已接收数量或明确状态,并在停止阶段拒绝新任务。

A5. 通知策略

**核对:**普通入队通常在解锁后调用 notify_one 即可,减少无效唤醒;shutdown 时使用 notify_all。持锁 notify_all 在语义上可行,但会让被唤醒线程先竞争一个尚未释放的锁。

A6. 批量插入统计

**核对:**SQL 使用 ON CONFLICT DO NOTHING,但 insertRowsForTable 直接返回 rows.size()。如果发生冲突,processed 可能高于真实插入数。应读取数据库返回的影响行数,或使用 RETURNING 统计。

附录 B 一分钟速记

**Qt 跨线程:**Worker 归属工作线程;UI 只在主线程更新;QueuedConnection;安全停止后 deleteLater。

**Modbus TCP:**Transaction ID 匹配;MBAP Length 拆帧;每设备请求串行;125 个寄存器,不是字节。

**数据可靠性:**先落本地事务;服务端 ACK 后改状态;唯一业务 ID;超时可重试但不能重复入库。

**后端队列:**有界队列;批量大小或超时;锁内只搬数据;队列满要背压或可靠落盘。

**PostgreSQL:**参数绑定;ON CONFLICT;事务回滚;连接独占;慢 SQL 看执行计划、锁和系统指标。

**报警与 UI:**上升沿只记录一次;恢复沿补恢复时间;UI 批量刷新;曲线固定窗口并降采样。

**现代 C++:**RAII 管生命周期;默认 unique_ptr;真正共享才 shared_ptr;并发容器读写要同步。

对比项静态链接库动态链接库
常见文件Windows .lib、Linux .aWindows .dll、Linux .so
链接时间编译程序时通常运行程序时加载
最终程序库代码被复制进 .exe.exe 依赖外部库文件
部署通常只需一个可执行文件需要同时部署 DLL/SO
更新库一般需要重新编译程序替换库文件即可,前提是接口兼容
程序体积通常更大主程序较小
内存多个程序可能各带一份代码多个程序可共享同一个动态库代码页
启动和运行少一次动态加载,行为更直接需要查找、加载和解析依赖
兼容性风险构建时问题较多,运行时依赖少可能遇到缺少 DLL、版本冲突、符号不兼容
适合场景独立工具、嵌入式、部署简单插件系统、公共组件、频繁更新的库

先讲真实做法,再讲改进方案;比背一个过度完整但经不起追问的答案更稳。

载入内容