C++代码调试技巧:面试官问“你的程序崩了怎么排查“,别只说printf
上篇聊了CMake,把项目的编译构建搞明白了。今天说编译之后的事——调试。
先讲个面试场景。
面试官问:"你的机器人程序跑着跑着突然segfault了,你怎么排查?"
我当时脱口而出:"加printf看哪里挂了。"
面试官没说话,就看着我。
我接着补充:"也可以用日志……"
场面一度很尴尬。
后来才知道,面试官想听的不是这个。他想听的是系统性的排查思路——core dump分析、GDB定位崩溃点、ASAN检测内存问题、日志系统辅助定位。这几个工具的组合使用,才是一个成熟的C++开发者应该具备的调试能力。
GDB:C++调试的瑞士军刀
GDB是Linux下最强大的调试器。虽然用起来不如IDE的调试器直观,但在服务器环境和机器人本体上,你通常只能通过命令行调试。
基本操作大家都知道:break设断点,run运行,next单步,continue继续,print打印变量。这些是入门级的。
真正有用的是几个进阶用法。
第一个是core dump分析。程序崩溃后,系统会生成一个core文件,记录了崩溃时的内存状态。用GDB加载它:
gdb ./my_robot_node core
进去之后输入bt(backtrace),GDB会告诉你崩溃时的函数调用栈。比如:
#0 0x00005555 in LidarProcessor::processScan (this=0x0, scan=...)
#1 0x00005678 in PerceptionModule::update (this=0x55555...)
#2 0x00005890 in main ()
一眼就能看到:LidarProcessor的this指针是0x0,空指针解引用。问题定位到了。
要注意,生成core dump需要系统配置。默认情况下Ubuntu的core dump大小限制是0。需要执行ulimit -c unlimited,或者配置systemd的coredump handler。面试的时候如果能提到这个细节,加分。
第二个是条件断点。在循环里某个特定条件才触发的问题,不可能手动一步步走到那个条件。用条件断点:
break sensor.cpp:42 if distance < 0.01
只有当distance小于0.01时才会停下来。这在调试数值计算问题时特别有用。
第三个是watchpoint。你想观察某个变量什么时候被修改了:
watch robot_state
每次robot_state被写入,程序都会暂停。找那种"不知道谁改了全局变量"的bug时,watchpoint是神器。
ASAN:内存问题的照妖镜
GDB擅长定位崩溃现场,但有些内存问题不会立刻崩溃——它们像定时炸弹,不知道什么时候炸。比如:
- 堆缓冲区溢出:写数据时越界了一个字节
- use-after-free:对象被delete了还在用
- 内存泄漏:new了没delete
- 栈溢出:递归太深或者局部变量太大
这些问题,GDB不一定能抓到。但AddressSanitizer(ASAN)可以。
ASAN是编译器内置的内存检测工具。用法非常简单,编译时加两个flag:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
然后正常运行程序。一旦发生内存问题,ASAN会立刻终止程序并输出详细的错误报告,精确到文件、行号,告诉你那块内存是什么时候分配、什么时候释放的。
ASAN的性能开销大概是2倍左右。所以一般只在Debug模式下开,Release发布时关掉。在机器人项目里,我建议在CI里加一个ASAN的构建配置,每次提交自动跑一遍测试。
除了AddressSanitizer,还有ThreadSanitizer(检测数据竞争)、MemorySanitizer(检测未初始化内存)、LeakSanitizer(检测内存泄漏)。用法都一样,换个flag就行。面试的时候如果能提一嘴这些工具的区别和适用场景,说明你确实有实际调试经验。
日志系统:生产环境的调试手段
ASAN和GDB都适合开发阶段。但程序部署到机器人上跑的时候,你不可能挂着GDB。这时候日志就是最重要的调试手段。
很多新手用std::cout打日志。这在本地demo里够用,但在真实项目里问题很多:没有级别控制、没有文件输出、多线程下输出会交错。
工业级的做法是用日志库。C++常用的有spdlog和glog。spdlog速度快、API友好,是目前最流行的选择:
#include <spdlog/spdlog.h>
int main() {
spdlog::info("Robot node started");
spdlog::error("Sensor processing failed: {}", e.what());
}
日志的级别很重要。DEBUG级别的日志在发布时关掉,INFO记录关键流程,WARN记录异常但可恢复的情况,ERROR记录严重错误。通过配置文件或者运行时参数调整日志级别,不需要重新编译。
在ROS2里,每个节点自带日志系统rclcpp::get_logger()。用法类似:
RCLCPP_INFO(this->get_logger(), "Lidar scan received, %zu points", scan.points.size());
RCLCPP_ERROR(this->get_logger(), "Failed to connect to lidar");
日志输出会自动加上节点名、时间戳和日志级别。在多节点的机器人系统里,这个前缀信息对于区分日志来源非常关键。
还有一个实用技巧:在关键路径上加带上下文的日志。比如处理传感器数据之前记录数据的大小和时间戳,处理之后记录结果的摘要。出了问题翻日志就能定位到是哪一步出的问题。
几个实战排查思路
面试里还有一个经常被追问的细节:内存泄漏怎么排查?除了ASAN,Valgrind是经典工具。valgrind --leak-check=full ./your_program能告诉你每一处未释放的内存分配点和调用栈。缺点是运行速度会慢几十倍,不适合实时性要求高的程序。实际项目中,我一般先用ASAN快速筛查,确认有泄漏后再用Valgrind定位具体位置。
还有一个实用技巧:GDB的watch命令可以监控变量变化。比如你怀疑某个指针被意外修改,设置watch ptr,当ptr的值发生变化时程序会自动暂停,然后你用bt查看调用栈,就能看到是谁修改了这个指针。这在排查多线程环境下的野指针问题时特别有用。
面试的时候,面试官问"程序崩了怎么排查",他期望的是一个完整的思路,而不是零散的工具名。
第一步,看core dump。用GDB加载,bt看调用栈,info locals看局部变量,定位崩溃点。
第二步,如果崩溃点和内存有关(segfault、double free),开ASAN重跑,让ASAN告诉你具体的内存错误类型。
第三步,如果问题不是崩溃而是逻辑错误(输出不对、行为异常),加日志缩小范围。在关键函数的入口和出口打日志,二分法缩小问题区间。
第四步,如果是多线程问题(偶发崩溃、数据不一致),用ThreadSanitizer检测数据竞争,或者用GDB的thread apply all bt看所有线程的调用栈。
第五步,如果问题只在特定环境出现,对比两个环境的差异——库版本、编译选项、硬件配置。
这个排查流程,有条理地讲出来,比单纯说"我用GDB调"强太多了。
给正在准备面试的你
调试能力这个东西,很难通过看书学会。你得真的遇到bug,真的去调,才能积累经验。
建议你在做项目的时候,故意给自己制造一些bug——写个越界访问、写个空指针、写个多线程数据竞争——然后用上面这些工具去定位。经历过几次之后,面试的时候讲起来就有底气了。
下篇聊C++性能优化实战——profile驱动优化的完整流程。代码能跑了,还得跑得快。怎么找到性能瓶颈、怎么优化,这是面试中区分初级和高级开发者的话题。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第63篇 CMake编译系统详解——从入门到能写复杂项目 下一篇预告:第65篇 C++性能优化实战——profile驱动优化的完整流程
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)