异常处理try/catch:面试官问“机器人程序崩了怎么办“,我这样答直接加分

异常处理try/catch:面试官问“机器人程序崩了怎么办“,我这样答直接加分

上篇聊了模板类,C++基础语法部分到这里基本收尾了。今天讲一个工程味很浓的话题——异常处理。

异常处理在面试里不算高频考点,但一旦问到,区分度很大。因为大部分人只会写try/catch,但对异常的安全机制、性能影响、最佳实践说不清楚。

讲个真实场景。

机器人跑着跑着,激光雷达突然断连了。如果没有异常处理,程序可能直接崩溃——指针解引用一个空数据,段错误,core dump。整个机器人停在那里,一动不动。

有了异常处理,你可以优雅地降级:检测到传感器断连,抛异常,上层捕获,切换到备用传感器或者安全停车。

这就是异常处理在机器人开发里的意义——不是让程序不犯错,而是让程序犯错的时候不会死。

try/catch的基本用法

语法很简单:

try { // 可能出错的代码 double distance = readLidar(); if (distance < 0) { throw runtime_error("Lidar reading error: negative distance"); } processDistance(distance); } catch (const runtime_error& e) { // 处理runtime_error cerr << "Error: " << e.what() << endl; useFallbackSensor(); } catch (const exception& e) { // 处理其他标准异常 cerr << "Unexpected error: " << e.what() << endl; } catch (...) { // 兜底:捕获所有异常 cerr << "Unknown error!" << endl; emergencyStop(); }

几个要点:

catch的顺序很重要。子类异常要放在前面,父类异常放在后面。因为catch是按顺序匹配的,一旦匹配到就不再往下走。如果把exception放最前面,它会捕获所有标准异常,后面的catch就永远不会执行了。

catch(...)是兜底,捕获所有异常,包括非标准异常。但它的缺点是你拿不到任何异常信息。

异常类继承体系

C++标准库定义了一套异常类层次:

exception ├── logic_error │ ├── invalid_argument │ ├── out_of_range │ └── length_error └── runtime_error ├── range_error ├── overflow_error └── system_error

面试偶尔会让你区分logic_error和runtime_error。简单说:logic_error是逻辑错误,理论上可以通过代码检查避免(比如参数越界);runtime_error是运行时错误,只有在程序跑起来之后才能发现(比如硬件故障)。

在机器人开发里,自定义异常类通常继承自runtime_error:

class SensorException : public runtime_error { string sensor_name_; int error_code_; public: SensorException(const string& name, int code) : runtime_error("Sensor " + name + " error: " + to_string(code)), sensor_name_(name), error_code_(code) {} const string& sensorName() const { return sensor_name_; } int errorCode() const { return error_code_; } }; // 使用 try { throw SensorException("lidar_front", 1001); } catch (const SensorException& e) { cerr << e.what() << endl; cerr << "Sensor: " << e.sensorName() << endl; // 可以根据error_code做不同处理 }

这样做的优点是异常信息丰富——你知道是哪个传感器出了什么问题,方便定位和恢复。

异常安全:机器人开发里的关键问题

面试官如果深挖,会问"什么是异常安全"。

异常安全有三个等级。

基本保证:程序在抛异常后不会泄漏资源。比如new了一块内存,在异常路径上也能释放。

强保证:异常发生后,程序状态回滚到调用前的状态。要么完全成功,要么什么都不变。

不抛异常保证:函数保证不抛异常。用noexcept关键字标记。

void processData() noexcept { // 保证不抛异常 // 如果真抛了,程序直接terminate }

在机器人开发里,运动控制相关的函数通常标记为noexcept。因为机器人正在走的时候,你不能用异常来处理错误——你得用返回值或者状态码,确保控制回路不会被打断。

异常的争议:用还是不用

讲真,C++社区对异常的态度是分裂的。

Google的C++编码规范里明确禁止使用异常。理由是:异常的性能开销不可预测,而且大型项目里异常会让控制流变得很难追踪。

但很多机器人框架(包括ROS)是依赖异常的。比如PCL的IO操作、Eigen的部分运算,出错了就抛异常。你不用异常,就得在每层都检查返回值,代码写起来很啰嗦。

讲个实际例子。我之前做一个导航模块的时候,初始化阶段用了大量异常处理——加载地图文件可能失败、连接激光雷达可能超时、读取配置文件可能格式错误。这些都是一次性的操作,用异常处理很合适。但在主控制循环里,所有错误都用返回值处理,因为控制循环跑100Hz,你不能让它因为异常被中断。

我的建议是:在机器人开发里,异常适合处理"不太可能发生的错误"——比如硬件初始化失败、配置文件格式错误。对于"经常发生的错误"——比如传感器读数偶尔超时——用返回值更合适。

记住一个原则:异常处理的是"异常"情况,不是"正常"的错误流程。

面试中的异常考点

面试官考异常,几个经典问题。

"异常和错误码相比有什么优缺点?"优点是异常不能忽略(你不catch就往上抛,最终程序崩溃,逼着你处理);缺点是影响性能、让控制流不直观。

"析构函数能抛异常吗?"最好不要。如果析构函数在栈展开过程中抛异常,程序会直接terminate。这就是为什么析构函数通常标记为noexcept。

"catch的时候用值还是引用?"用const引用。用值会多一次拷贝,而且可能发生对象切片(slicing)——子类异常被截成父类。

还有一个面试深度追问:异常安全的三个保证级别你知道吗?第一层是基本保证——即使抛出异常,程序的状态也是合法的,没有资源泄漏,但数据可能变了。第二层是强保证——操作要么成功要么完全回滚,就像数据库的事务一样,数据状态不变。第三层是无异常保证——函数承诺绝不抛出异常,也就是noexcept。在机器人开发里,控制指令的发送函数一般要做到强保证,因为发了一半的指令比不发更危险。实现异常安全最靠谱的方法还是RAII——用智能指针和局部对象来管理资源,让析构函数自动清理,这样即使中途抛异常也不会泄漏。面试时能讲出异常安全的三个级别,再结合RAII给出解决方案,面试官基本就满意了。

给正在准备面试的你一点建议

异常处理在面试里不算高频,但属于"会了加分"的知识点。

记住三点就够了:try/catch的基本语法和catch顺序,自定义异常类的设计,异常安全的基本概念。

如果你能聊到"Google为什么禁用异常"以及"机器人开发里异常和返回值的选择",面试官会觉得你有工程思维,不只是会写代码。

再聊一个面试常考的延伸点:noexcept关键字。C++11引入了noexcept,让你可以标记一个函数不会抛出异常。比如移动构造函数通常标记为noexcept,因为STL容器在扩容时会优先选择noexcept的移动构造而不是可能抛异常的拷贝构造,这对性能影响很大。如果你能在面试中解释"为什么移动构造函数要标记noexcept",面试官会觉得你对异常安全和性能优化都有理解。

下篇讲命名空间——大型项目中避免冲突的规范。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列上一篇:第27篇 模板类——STL容器的底层设计思路 下一篇预告:第29篇 命名空间——大型项目中避免冲突的规范

有任何问题欢迎评论区留言,我会尽量回复。