第002篇 大疆创新·C++开发工程师面试——运算符重载与表达式求值顺序 📅 发布时间:2026/9/16 0:07:55 👁 浏览次数: 大疆创新·C开发工程师面试——运算符重载与表达式求值顺序有朋友去面大疆的 C 岗回来跟我吐槽问的全是看着简单、一深挖就露馅的题。其中就有运算符——谁不会用 - 啊可面试官问的是这个表达式到底先算哪一步重载 operator 和内置 有什么本质区别当场就把人问住了。今天聊聊大疆 C 面试里运算符和表达式求值这条线。大疆的代码库规模大、性能敏感面试官喜欢从这里考察你写代码时对表达式到底怎么执行的敏感度。我把真实遇到的题、追问和参考答案整理出来。第一组问答i 和 i底层到底差在哪这是最常见的开局题但大疆的面试官不会满足于i 先返回后自增这种答案。面试真题i 和 i 在底层实现上有什么区别在循环里用哪个更好参考答案思路对内置类型两者都是单条指令inc没有性能差异编译器还会自动优化。真正的差别在类类型上后置 operator 必须保存旧值再自增再返回意味着一次拷贝构造前置 operator 直接自增返回引用零拷贝。标准库迭代器就是这么实现的所以 for (auto it v.begin(); it ! v.end(); it) 用前置是行业惯例。面到内置类型两者一样、类类型后置多一次拷贝这一层基本就过关了。高频追问那自定义类型重载这两个运算符函数签名有什么区别参考答案思路前置是无参成员函数 T operator()后置是带一个哑元 int 参数 T operator(int)。这个哑元纯粹为了区分重载。后置返回旧值的拷贝所以一般实现成先拷贝一份、再调前置、返回拷贝T operator(int) { T tmp *this; // 保存旧值 (*this); // 复用前置 return tmp; // 返回旧值 }这个用前置实现后置的惯用法面试写出来很加分。高频追问为什么后置不能返回引用参考答案思路后置返回的是自增前的旧值它是一个临时对象。返回引用就会返回一个即将销毁的临时对象的引用——悬垂引用调用方拿到的是一块已失效的内存。所以后置必须按值返回。这是 C 语义决定的不是约定俗成。第二组问答重载 operator到底在重载什么面试真题给一个自定义的向量类重载 operator普通成员函数、友元、全局函数三种写法有什么区别参考答案思路三种写法都能实现 a b区别在参数隐式转换上。成员函数形式 Vec operator(const Vec rhs) 要求左操作数必须是 Vec 类型而友元/全局函数形式 Vec operator(const Vec lhs, const Vec rhs) 允许左操作数发生隐式转换。举例Vec 有单参构造函数 Vec(double)那么 2.0 v 用友元版本能编译2.0 隐式转成 Vec成员版本不行2.0 不是 Vec 的成员。所以二元运算符优先用友元或全局函数保证左右操作数对称。大疆的数学库四元数、旋转矩阵这类全是这套写法。高频追问那什么时候用成员函数重载运算符参考答案思路两种情况。一是一元运算符、--、-、*、!天然适合成员二是复合赋值运算符、-因为它们会修改左操作数自身语义上属于对象对自己的操作。而且 C 规定 a b 会自动展开成 a.operator(b)成员版本是唯一正确选择。 和 的实现还可以互相复用 用 实现避免逻辑重复。高频追问重载运算符要注意什么边界参考答案思路核心是不改变运算语义——重载 就不能做减法的事否则读者和调用方都会懵。另外四个点一是不能重载 ::、.、?:、sizeof 等二是重载不改变运算符优先级和结合性a b * c 永远是先乘后加三是返回类型要合理—— 返回新对象、 返回 bool、 返回 ostream 支持链式四是保持对称性重载了 一般也要重载 !C20 可以靠 自动生成 !。大疆代码规范里对运算符重载查得很严因为数学类型多重载滥用会让代码难读。第三组问答求值顺序表达式里的未定义行为面试真题i i i; 这个表达式结果是多少参考答案思路不知道而且永远不该写。这是典型的未定义行为UB。原因C 规定在同一个表达式中如果某个对象被多次修改且这些修改之间没有序列点C11 后叫 sequenced-before约束行为就是未定义的。i 和 i 都修改 i编译器可以按任意顺序求值结果可能是 0、1、2、3 或者任何值取决于编译器优化。大疆面试官考这个看的不是你会不会算而是会不会识别并规避 UB——嵌入式代码里一个 UB 在 Release 和 Debug 下表现不同是最难排查的 bug。高频追问那 f(i, i) 呢函数参数的求值顺序有保证吗参考答案思路没有保证。C 标准规定函数参数的求值顺序是未指定的unspecified编译器可以按任意顺序求值参数。虽然 C17 之后逗号表达式的求值顺序是确定的从左到右但函数调用参数列表里的逗号不是逗号运算符不受这个保证保护。所以 f(i, i) 也是未定义行为——两个参数都修改 i顺序不定。正确做法分开写f(i, i) 改成先算再调。高频追问C17 对求值顺序有什么新保证参考答案思路C17 引入了几个重要的 sequencing 保证一是函数参数之间仍是未指定顺序但后置表达式如 a[i] 的 []先于函数参数求值二是 a.b、a-b、a[b]、a[b] c 这类里左操作数在右操作数之前求值三是运算符右侧的求值如 a b 中 b 的求值发生在左侧之后……严格说 C17 只保证右操作数在赋值/复合赋值发生时已求值。总之现代 C 的姿势是一个表达式里不要对一个对象做多次有副作用的修改宁可多写一行。这是面试官真正想听的结论。第四组问答短路求值与运算符重载的陷阱面试真题 和 || 的短路求值重载后还成立吗参考答案思路不成立这是 C 一个著名的坑内置的 /|| 有短路求值左操作数为 false 就不求值右操作数但重载的 operator/operator|| 是普通函数调用两个参数都会先求值短路特性丢失。所以永远不要重载 、||、,——重载会让代码行为与内置语义不一致读者以为有短路实际没有可能引入严重的性能问题甚至 bug比如右操作数里有解引用空指针的代码短路时不会执行重载后就会崩。C 标准也明确建议不要重载这三个运算符。高频追问短路求值在实际代码里怎么利用参考答案思路经典用法是条件保护if (ptr ptr-valid())ptr 为空就不解引用if (len 0 buf[len-1] \n)len 为 0 就不访问越界。这类代码在机器人状态判断里到处都是。另一个点是短路求值的顺序是固定的 先左后右所以把代价低的判断放前面、代价高的判断放后面能省计算。比如先判断标志位再去做耗时的数组扫描。高频追问那 ?: 三元运算符呢重载不了吧参考答案思路对?: 不能重载。它只求值选中的那个分支另一个分支不求值而且两个分支的结果类型要能统一。面试时提到?: 只求值一个分支、和 if-else 的求值语义等价但表达式形式更紧凑能体现你对求值语义的把握。第五组问答重载 与输出流为什么必须用友元面试真题给自定义类型重载输出流 operator为什么必须用友元或全局函数参考答案思路因为左操作数是 std::ostream不是你的类。cout myObj 会被解析成 operator(cout, myObj)——如果定义为成员函数就要求左操作数是你自己的类myObj cout完全反了。所以必须用全局函数或友元把 ostream 放第一个参数。友元的好处是能访问私有成员。标准库就是靠这套机制支持 cout string 的——operator 是定义在 std 里的全局函数。高频追问重载 返回类型为什么必须是 ostream点赞、在看、转发三连是对我最大的支持「机器人大厂面经·从入门到精通」连载系列上一篇[第1篇 宇树科技·C开发工程师面试——变量与数据类型的底层存储](link)下一篇预告[第3篇 智元机器人·C开发工程师面试——条件分支的性能陷阱](link)评论区聊聊你面试遇到过最刁钻的运算符题是哪道