面灵AI→

BIGO 音频 SDK 开发二面面经:select/poll/epoll、C++ 面向对象与手撕快排

轮次
二面
结果
凉经(大概率)
时间
2026-10
来源
牛客网

《面试题目》

面试官 1

  1. 请做一下自我介绍。
  2. 你是出于什么目的去做了这些项目?
  3. select、poll、epoll 之间有什么区别?
  4. 项目里提到「缓存控件可见性来解决背包输入被摇杆抢占」,这是什么意思?解决了什么问题?这个方案有什么缺点?
  5. 项目提到「建立创角数据落库的三道门控」,它解决了什么问题?问题的根因是什么?你只做了落库兜底,多节点玩家信息不同步这一点有没有解决?
  6. 手写快速排序。

面试官 2

  1. 和 C 对比,C++ 是怎么通过类实现面向对象特性的?
  2. C++11 及之后的版本,你认为还有哪些改进点?
  3. C++ 常用的设计模式有哪些?你在项目中用到了哪些设计模式?
  4. 对于任意一个无限循环小数,如何求它的分数形式?
  5. 在校成绩怎么样?

《参考解析》

select、poll、epoll 的区别

三者的差别集中在「内核怎么维护被监听的 fd、每次调用要搬多少数据」上。select 用位图传 fd 集合,有 1024 的 fd 上限,每次调用都要把整个集合从用户态拷进内核再由内核线性扫描一遍,返回后还要自己遍历找出就绪的 fd,复杂度是 O(n)。poll 把位图换成了 pollfd 数组,去掉了数量上限,但「整体拷贝 + 线性扫描」这两件事没变。

epoll 把注册和等待拆开:epoll_ctl 一次注册后,fd 挂在红黑树上,内核回调把就绪的 fd 放进就绪链表,epoll_wait 只返回就绪的那几个,复杂度与实际活跃连接数相关(O(1)~O(k)),也不存在每次全量拷贝。它还有水平触发(LT)和边缘触发(ET)两种模式:LT 只要缓冲区还有数据就会一直通知,写起来和 poll 类似;ET 只在状态变化时通知一次,必须一次把数据读干净(配非阻塞 fd 循环读),否则事件会丢。需要注意 epoll 的优势体现在「连接多但活跃少」的场景,连接数很少时三者差别不大。

控件可见性缓存与输入抢占

这个问题问的是 UI 输入的归属。摇杆和背包面板同时存在时,两者都会消费同一份触摸输入,如果背包打开后没有把摇杆的输入层挡掉,就会出现「点背包按钮,角色同时移动」这类穿透。用「缓存控件可见性」的解法是:在打开/关闭背包时同步刷新各输入控件的可交互状态(visible / interactable / 层级),让不可见的面板不参与命中测试,输入自然落到摇杆上。

这个方案的缺点在于状态是分散且手工同步的:每个面板的显隐都要记得改一次输入状态,组合一多(背包 + 技能轮盘 + 弹窗 + 引导遮罩叠加)就容易漏,出现要么穿透、要么反过来谁都不响应(输入被吃掉)。更稳的做法是把输入做成栈式管理:每次打开一个面板就往输入栈压一层,栈顶优先消费、下层挂起,关闭时出栈自动恢复,显隐与输入归属只在一处决定。

创角落库的三道门控与多节点不同步

「三道门控」通常指把创角请求的落库收敛成三道关:客户端侧的防重复提交、服务端的幂等校验(同一账号同一创角请求只落一次,用唯一键或请求 id 去重)、以及落库失败时的兜底与补偿。它要解决的根因一般是重复请求(玩家连点、断线重连后重发、超时重试)导致同一个角色建出多条数据,或者创角流程中途失败留下半套数据。

但只做落库兜底,确实没有解决「多节点玩家信息不同步」——不同玩家数据落在不同节点、跨节点缓存与内存态不一致,落后节点上仍会读到过期角色数据。这一层的常见做法是按 uid 做路由(同一账号固定落到同一节点处理)、用分布式锁或数据库唯一约束保证写入只有一个胜出者、写成功后用版本号或 binlog/MQ 广播让其它节点失效缓存,并让读路径在缓存未命中的情况下回源。回答时把「幂等 → 单点写 → 版本号/广播」这条链讲清楚,比只说「加了兜底」扎实。

手写快速排序

快排的核心是选定基准、划分、递归:每轮把小于基准的放左边、大于的放右边,基准落到最终位置,再对左右两段递归。要写出能过关的版本,得主动交代几个细节:基准用随机选取或三数取中,避免有序输入退化成 O(n²);大量重复元素时用三路划分(小于、等于、大于)把等于基准的元素一次归位;递归到小区间(如长度小于 16)时换成插入排序,减少函数调用开销;原地划分的写法注意 i、j 的移动顺序,避免死循环。平均时间复杂度 O(n log n),最坏 O(n²),空间是递归栈 O(log n)。如果面试官追问「怎么避免最坏情况」,还可以提内省排序(递归深度超过阈值换堆排序,C++ std::sort 就是这么做的)。

C++ 用类实现面向对象,与 C 的对比

C 里没有语言级的封装、继承、多态,只能靠「结构体 + 函数指针 + 命名前缀」手工模拟;C++ 把这三件事做成了语言机制:

  • 封装:class 通过 public / protected / private 控制成员的可见性,编译器直接禁止外部访问私有成员;C 只能靠约定(比如 _ 前缀)和文件作用域。
  • 继承:class Derived : public Base 复用基类的数据与行为,对象内存布局里基类子对象在最前面,派生类指针可以隐式转成基类指针。
  • 多态:基类里声明 virtual 函数后,类会多出一个虚函数表指针(vptr)指向该类的 vtable,调用时通过 vtable 间接寻址,从而在运行时决定执行哪个版本;纯虚函数把基类变成抽象接口。对象析构要走虚析构,否则通过基类指针 delete 派生对象时不会调用派生类析构函数。
  • C 里等价的东西是一个含函数指针的结构体(比如 Linux 内核的 file_operations),但函数指针要手动初始化、没有访问控制、也不能自动按对象类型派发。

C++11 之后的改进点

按标准分批说比较清楚:C++11 是分水岭,引入了 auto、范围 for、右值引用与移动语义、完美转发、nullptr、lambda、智能指针、std::thread 与内存模型、变参模板。C++14 补了泛型 lambda、返回值类型推导、变量模板。C++17 有结构化绑定、if constexpr、折叠表达式、std::optional/variant/any/string_view、并行算法、内联变量、类模板实参推导。C++20 是第二次大改:concepts 约束模板、ranges 管道式算法、协程、modules、std::span、三路比较 <=>。C++23 有 std::expected、std::mdspan、deducing this、import std。

要答出「改进点」而不是背特性清单,可以再补两句还没解决的:反射到现在仍未进标准、标准库缺网络库、module 生态和 ABI 兼容性仍是痛点、工具链对先进特性的支持参差不齐。这类题通常考的是你有没有跟着标准演进更新知识面,能说出「哪个版本解决了什么痛点」比列举名词得分高。

常用设计模式与项目中的使用

面试里最高频的是这几类:创建型——单例(全局管理器、配置中心,注意线程安全与生命周期)、工厂/抽象工厂(按类型创建对象,屏蔽构造细节)、对象池(子弹、特效这类高频创建销毁的对象)。结构型——适配器(对接不同平台的 SDK)、装饰器(给组件动态加能力)、组合(UI 树、技能效果树)。行为型——观察者/事件总线(UI 与逻辑解耦)、策略(同一接口换不同的算法或排序规则)、状态(角色状态机:待机/移动/受击/死亡,限制非法转移)、命令(把操作封装成对象,支持撤销重放)。

回答「你在项目里用了哪些」时,要挑得出具体场景和收益:比如音频 SDK 里用适配器把不同平台/编解码实现统一成一个接口,用单例管理音频设备与播放通道,用状态机管理播放生命周期(空闲/加载/播放/暂停/释放),用观察者把播放状态回调给上层业务。说清「不用它会怎样」比只报名字更有说服力。

无限循环小数转分数

方法是把循环部分「错位相减」消掉:

  1. 纯循环小数(循环节从小数点后第一位开始):设 x = 0.\overline{b},循环节长度为 k,则 10^k · x − x = (10^k − 1) x = b,所以 x = b / (10^k − 1)。例:0.\overline{3} = 3/9 = 1/3,0.\overline{142857} = 142857/999999 = 1/7。
  2. 混循环小数(前面有不循环的部分):设 x = 0.a\overline{b},其中不循环部分有 m 位、循环节 k 位。先乘 10^m 把不循环部分挪到整数位,再乘 10^k 做错位相减:x = (ab − a) / ((10^k − 1) · 10^m)。例:0.1\overline{6} = (16 − 1)/90 = 15/90 = 1/6。

最后约分即可。面试官若要求给「任意」一个小数的通用做法,把上面两步写成公式再说一句「分子分母同时除以最大公约数」就完整了。

面试官的反问反馈说明了什么

被问到「有哪些可以提升的地方」时,得到的答复是「C++ 细节了解得还可以,但一些业务和选型上的大方向还要再多理解学习」。这类反馈对客户端/音视频岗很典型:基础语法与语言细节只是入场券,真正拉开差距的是方案取舍——为什么选这套输入管理方案、为什么这样落库、多节点一致性打算怎么收口、瓶颈出现在哪一段、成本与收益怎么权衡。复盘时可以把项目里每个「我做了什么」都补上一层「为什么这么做、当时还考虑过什么、代价是什么」。