上篇聊了struct的内存对齐,有人后台说想看struct和class的区别详解。别急,后面会专门写。
今天聊两个面试里容易被忽视、但实际工程中特别有用的东西:联合体union和枚举enum。
为什么放一起讲?因为在机器人开发里,这俩经常搭档出现——特别是设计状态机的时候。
讲个面试故事。
去年面试一个候选人,简历上写着"设计过机器人状态机"。我问他:"你的状态机是怎么实现的?"
他说用了一堆if-else,状态用int表示,0是空闲,1是运行,2是错误……
我问他:"如果别人接手你的代码,看到if (state == 3),他知道3是什么意思吗?"
他愣了一下,说:"我写了注释的。"
我又问:"如果状态变了,比如加了一个新状态4,你怎么保证所有用到状态的地方都更新了?"
他没答上来。
讲真,这种问题我太常见了。不是不会写状态机,而是没有用对工具。如果他用的是enum,上面两个问题都不存在。
联合体union:几个变量共享同一块内存
先说union,因为这个面试考得比enum多。
union是什么?它把几个不同类型的变量放在同一块内存上,同一时刻只有一个变量是有效的。
和struct最大的区别:struct是每个字段各占各的空间,union是所有字段共用一块空间。
union Data { int i; float f; char c; };这个union占多少字节?答案是4字节——和最大的字段(int或float)一样大。
而如果是struct,加上内存对齐,至少要12字节。
union的内存模型是这样的:i、f、c三个变量的起始地址完全相同,都是这块4字节内存的开头。你往i写数据,f和c的内容就变了,因为它们读的是同一块内存。
Data d; d.i = 65; cout << d.c << endl; // 输出 'A',因为65是'A'的ASCII码这种特性在机器人开发里有什么用?
场景一:协议解析。机器人通信协议里,经常有一段原始数据需要按不同类型来解读。比如一个4字节的payload,有时候是int,有时候是float,用union最方便:
union Payload { int32_t int_val; float float_val; uint8_t bytes[4]; };场景二:节省内存。如果你有一个数组,每个元素可能是不同类型的数据,用union可以省空间。比如一个传感器读数,有时是温度(float),有时是状态码(int),用union只需要占最大类型的空间。
面试的时候,面试官问union,最爱考的就是"union占多少字节"和"union和struct的区别"。记住:union共享内存,大小等于最大字段;struct各占各的,大小等于所有字段加padding。
枚举enum:给状态起个有意义的名字
再说enum。
enum是什么?给一组相关的常量起有意义的名字。
没有enum之前,大家用int或#define表示状态:
#define STATE_IDLE 0 #define STATE_RUNNING 1 #define STATE_ERROR 2这种写法有两个问题:第一,#define没有作用域,容易命名冲突;第二,编译器不知道这些常量是一组的,没法做类型检查。
用enum就好了:
enum RobotState { STATE_IDLE, STATE_RUNNING, STATE_ERROR };现在STATE_IDLE、STATE_RUNNING、STATE_ERROR是RobotState类型的值。如果你写RobotState s = 5;,编译器会报警告——因为5不是一个有效的枚举值。
但传统的enum有个坑:它会把枚举值暴露到外层作用域。如果你有两个enum都定义了ERROR,就会冲突。
enum MotorState { IDLE, RUNNING, ERROR }; enum SensorState { IDLE, READY, ERROR }; // 编译错误!ERROR重复定义所以C++11引入了enum class(强类型枚举):
enum class MotorState { IDLE, RUNNING, ERROR }; enum class SensorState { IDLE, READY, ERROR }; // 没问题 MotorState ms = MotorState::ERROR; SensorState ss = SensorState::ERROR;现在每个枚举值都在自己的作用域里,不会冲突。而且enum class不能隐式转换成int,更安全。
面试的时候,如果你能主动提到enum class和传统enum的区别,面试官会觉得你对C++11有了解。
联合体+枚举:状态机的经典搭配
回到开头那个面试故事。怎么用union和enum设计一个清晰的状态机?
思路是这样的:用enum表示当前状态,用union存储不同状态下的数据。
enum class RobotState { IDLE, MOVING, SENSING, ERROR }; struct MoveData { double target_x, target_y; double current_speed; }; struct SenseData { float distance; int object_id; }; struct ErrorData { int error_code; char message[64]; }; union StateData { MoveData move; SenseData sense; ErrorData error; }; struct RobotFSM { RobotState state; StateData data; };这样设计的好处是什么?
第一,状态用enum表示,语义清晰。if (fsm.state == RobotState::MOVING)比if (fsm.state == 1)好读一百倍。
第二,不同状态的数据用union存储,节省内存。MOVING的时候不需要SENSING的数据,它们共用同一块内存。
第三,类型安全。如果用enum class,编译器会帮你检查状态值的合法性。
在机器人开发里,这种模式特别常见。ROS2里的很多消息类型,本质上就是"一个状态标识+不同状态对应的数据"。你用union和enum来理解它,一下就通了。
面试中的常见考法
总结一下,面试中关于union和enum,最爱考这几个点:
union相关:union占多少字节?union和struct的区别?union能不能有构造函数和成员函数?(C++11之后可以,但有限制。)
enum相关:enum和enum class的区别?enum的底层类型是什么?(默认是int,但可以指定。)enum class能不能隐式转成int?(不能。)
综合题:怎么设计一个状态机?这时候你就可以把union和enum的搭配讲出来。
我面试的时候,如果候选人能把状态机设计讲清楚——enum管状态、union管数据、switch-case管转移——我基本就认定这个人有工程思维。
给正在准备面试的你一点建议
union和enum,单独看都不难。难的是把它们组合起来解决实际问题。
我的建议是:
先把union的内存模型搞清楚,能手算union的大小,知道union和struct的区别。然后把enum class的用法练熟,特别是和传统enum的区别。最后,试着自己设计一个简单的状态机——不用复杂,机器人的运动状态就行:空闲、前进、转弯、停止,每个状态带不同的数据。
做完这些,你对union和enum的理解就不只是"课本上的语法"了,而是真正能用到工程设计里的工具。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列上一篇:第9篇 结构体struct——自定义数据类型的第一步 下一篇预告:第11篇 指针的本质——内存地址的"门牌号"比喻
有任何问题欢迎评论区留言,我会尽量回复。