ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

声网2020校招C++笔试题解析:多线程、内存管理与设计模式核心考点

声网2020校招C++笔试题解析:多线程、内存管理与设计模式核心考点 校招季又来了后台不少同学私信问我“声网2020校招-通用C笔试题”的情况。说实话虽然过去几年了但这份笔试题在当年算是一份很有代表性的题目放到今天也依然能打。声网做的是实时音视频SDK底层大量使用C所以它的笔试题不光是考语法更多是考你在高并发、低延迟场景下能不能写出靠谱的代码。今天我把这份题目的考察思路、核心考点和答题策略拆开揉碎了讲一遍不管你是准备声网还是准备其他RTC厂商的校招这份内容都值得静下心看完。1. 声网笔试的底层逻辑为什么RTC行业格外看重C功底1.1 实时音视频对C工程师的硬性要求声网的主业是实时音视频通信也就是我们常说的RTC。你可能用过它的SDK做直播、在线教育、视频会议甚至是陪玩软件里的语音聊天。这类业务有一个非常鲜明的特点延迟必须以毫秒级衡量且不能有卡顿。想象一下你在视频会议里说话对方两秒后才听到这会议根本开不下去。为了做到低延迟、高流畅SDK的底层模块——音频采集、降噪、编码、网络传输、丢包重传、视频渲染——几乎全部是用C写的。正因为如此声网对C工程师的要求跟做业务后台、做客户端应用的团队完全不是一个维度。你写的每一行代码都可能在数万甚至数十万路音视频流里跑着。一个锁用错了可能造成全局抖动一次内存拷贝没优化可能在弱网环境下直接拖垮通话质量。所以在校招笔试环节声网最想看到的不是你背了多少API而是你有没有能力在资源受限、并发激烈的环境里写出可预测、可控、可优化的代码。这里有个通俗类比普通C开发像是做家常菜慢一点没关系熟了能吃就行RTC的C开发像是做刺身刀工、火候、食材新鲜度、上菜速度每一环都不能掉链子。笔试题就是考你“刀工”——你对C这门语言和底层运行机制的理解深度。1.2 通用C岗位的考察范围与题量分布声网的校招通用C笔试题整体上分两个大块客观题和编程题。客观题以选择题、填空题为主覆盖C语言基础、内存管理、多线程、设计模式、操作系统等内容编程题一般是两道左右一道偏算法一道偏系统设计或者说是偏编码实现难度介于LeetCode中等和困难之间偶尔会有场景题。从2020年那套题来看高频考察点集中在几个方向上语法细节拷贝构造、赋值运算、移动语义、constexpr、模板特化、类型推导等内存与生命周期智能指针、RAII、堆栈区分、内存泄漏场景并发编程锁的使用、原子操作、ABA问题、死锁的产生与避免设计模式单例、观察者、工厂等常见模式在工程中的具体应用算法与数据结构链表、二叉树、字符串处理、排序、动态规划这跟我在网上看到的一些“C八股文”题库有所重叠但声网的题目更偏向“工程后果”而非单纯“语法结论”。比如同样考智能指针普通笔试题会问“shared_ptr的引用计数是怎么维护的”声网可能会问“在多线程环境里shared_ptr的拷贝和析构是否线程安全为什么”。后者显然需要你把语法知识放进真实的并发场景里思考。2. 高频笔试考点拆解一多线程与并发控制2.1 从ABA问题聊到无锁编程ABA问题几乎是声网这类强调高并发性能的公司必考的考点。热搜词里也出现了“aba问题c”说明不少人都在搜但很多人对它的理解停留在“CAS操作中值被改回原值”这个层面能讲清楚影响和解决方案的并不多。先简单说下ABA问题是什么。假设我们有一个无锁栈栈顶是A节点。线程T1读取栈顶A准备执行CAS操作把栈顶换成B。在线程T1挂起期间线程T2把A弹出又压入了一个新的A节点节点地址相同但内容可能已经变了然后栈顶又变成了A。此时线程T1恢复执行CAS发现栈顶还是A于是成功替换成B。看起来操作成功了但实际上栈的结构已经和T1看到的不一样了这就可能引发内存管理上的严重问题。为什么这个题在RTC领域特别值得考因为RTC的音频包、视频包都是高频小对象回调线程、采集线程、渲染线程之间经常需要传递数据。如果每个包都用锁保护锁的开销甚至比业务逻辑本身还大。所以很多高性能队列会采用无锁设计而CAS是无锁数据结构的基石。一旦ABA问题没有处理好轻则数据错乱重则直接崩溃。笔试里如果考到ABA问题常见问法有两种。一种是概念题让你解释什么是ABA问题另一种是代码题给你一段用CAS实现的入栈出栈代码让你指出潜在Bug。你至少要能答出解决方案使用带版本号的原子指针比如用AtomicStampedReferenceJava里比较常见或者自己封装一个带tag的结构体。每做一次修改版本号加一CAS时就同时比较指针值和版本号这样即使地址没变版本号也会暴露中间发生过修改的事实。我在实际项目里的做法是用一个64位的结构体高32位存指针低32位存版本。因为主流平台都支持16字节的原子比较交换操作起来并不复杂。笔试答题时你不用写这么底层能讲清楚“引入版本号”这个核心思路就够了。2.2 锁竞争、原子操作与死锁的工程后果除了ABA问题声网笔试题中并发考察第二多的就是锁竞争和死锁。有一道印象深刻的题目给你一个多线程音频处理程序A线程采集音频数据B线程编码C线程网络发送三者通过一个带锁的队列传递数据。请问在什么情况下这个程序会出现音频卡顿如何优化很多同学第一反应是“队列满了会丢包”这只能算答对了一半。真正深入的问题是三个线程共享一把大锁导致采集线程在等锁的时候麦克风数据没有及时读取造成采集缓冲区溢出。在RTC的实时链路里采集是绝对不能阻塞的一旦阻塞这段音频就废了。所以工程上通常不会让采集线程去争一把大锁而是用无锁环形缓冲区RingBuffer或者让采集线程只负责往缓冲区写数据不负责处理锁。笔试题里遇到这类并发设计题答题的核心是体现出“锁的粒度与线程的实时性要求”之间的关系。你需要说明哪些线程对延迟敏感采集、渲染哪些线程允许偶尔抖动后台任务然后围绕这个做锁的拆解或者用原子操作替代。这也是声网笔试跟普通C笔试最大的差异点——它不考死记硬背考的是你在实时系统里做的工程决策。另外死锁是必考概念。四个必要条件互斥、持有并等待、不可剥夺、循环等待要能默写并且要会分析一段代码的死锁可能。笔试中常见的坑是多个锁的加锁顺序不一致。比如线程1先锁A再锁B线程2先锁B再锁A这就埋了死锁的雷。解决办法也简单所有地方都按固定的全局顺序加锁或者用std::lock一次锁多个避免顺序问题。3. 高频笔试考点拆解二内存管理与RAII3.1 智能指针家族的笔试陷阱声网的C笔试里智能指针是绝对的重点。从热搜词“c八股文”、“c面试题”里能看出来很多同学已经在背了但背概念和真正理解工程语义之间差距还是很大的。先列几道声网风格的智能指针题你可以自测一下shared_ptr的引用计数本身是线程安全的但shared_ptr对象本身不是。这句话怎么理解多线程环境下两个线程同时拷贝同一个shared_ptr引用计数会正确增加吗两个线程同时对一个shared_ptr赋值会发生什么weak_ptr如何解决循环引用问题在什么场景下必须用weak_ptr而不是shared_ptr用shared_ptr管理数组时析构函数是否会被正确调用最后一道题是经典的坑。shared_ptr的默认删除器是delete不是delete[]。如果你用shared_ptr p(new int[10])析构时只delete第一个元素其余9个内存泄漏。正确做法是指定删除器shared_ptr p(new int[10], std::default_deleteint[]())或者用shared_ptrint[]这在C17之后是支持数组特化版本的。再说回那个线程安全的问题。shared_ptr的实现里控制块control block的引用计数是原子变量所以多个线程同时拷贝同一个shared_ptr时引用计数的递增是安全的。但shared_ptr本身的两个成员指针和控制块指针并不是原子修改的。如果两个线程同时对同一个shared_ptr变量赋值比如p make_shared (1)就会发生数据竞争可能导致双重删除或者控制块指针损坏。这个知识点在声网的SDK里非常重要。你想想一个音频帧在采集线程被封装成shared_ptr然后要传给多个处理模块降噪、增益、编码。如果设计不当多个线程同时修改同一个shared_ptrSDK就崩了。工程上通常约定shared_ptr可以自由拷贝传递但绝不能多个线程同时写同一个shared_ptr变量。这句话建议你记下来笔试问答和面试都直接用得上。3.2 移动语义与右值引用性能优化的核心武器RTC场景对性能极其敏感移动语义几乎是每套笔试题里都会出现的考察点。2020年那套题里有一道很典型的题目给出一段代码让你分析调用了哪些构造函数输出什么。代码大概长这样#include iostream #include string std::string createString() { std::string s hello; return s; // 这里会发生什么 } int main() { std::string a world; std::string b a; // 第1处 std::string c std::move(a); // 第2处 std::string d createString(); // 第3处 return 0; }第1处调用拷贝构造因为a是左值。第2处调用移动构造因为std::move把a转换成了右值引用此时a的资源被转移到ca变为空字符串理论上具体取决于string实现但一般是这样。第3处在C11之前会先构造临时对象再拷贝但在C11之后因为返回值优化RVO/NRVO的存在甚至可能连移动构造都不调用直接在d的内存位置上构造s。这个知识点为什么对RTC重要因为音频和视频帧动辄是几十KB到几MB的数据。如果你在传递过程中频繁进行深拷贝带宽和CPU消耗会成倍增加。我在做网络传输模块的时候UDP收包线程收到一个包后要经过解包、解密、解码、渲染多个阶段。每一层如果都用拷贝传递多一次拷贝就多几十微秒的开销在720P、30帧的视频流里这就是灾难。笔试中如果让你写一个移动构造函数你要注意哪些细节首先必须把源对象的指针置空否则源对象析构时会把资源释放掉导致目标对象变成悬垂。其次如果类里有自定义析构函数、拷贝赋值函数、移动赋值函数中的任何一个编译器可能不会生成默认的移动构造需要自己显式声明。第三移动构造函数应该标记为noexcept这样在vector扩容时才能安全地移动元素而不是拷贝。如果移动构造函数可能抛出异常vector扩容时会退化为拷贝性能就掉下去了。关于constexpr热搜词里也有“constexpr哪个c版本引入的”。答案是C11引入的C14放宽了函数限制C17又加了inline constexpr变量等能力。声网的题目倾向于考察constexpr变量与宏定义的区别、constexpr函数在编译期求值的条件等。这个知识点本质上是在考你对“编译期优化”的理解——在RTC这种对运行时性能极度敏感的领域能在编译期算完的东西绝不留到运行期。4. 高频笔试考点拆解三设计模式与代码组织4.1 单例模式的线程安全写法从笔试到工程落地单例模式是C面试题里的经典声网笔试也没放过这个点。但让我比较意外的是2020年那套题里的单例题并没有直接问“如何实现一个单例”而是给了一段存在问题的单例代码让考生分析有哪些线程安全问题、如何修复。那段代码大概是这样的凭记忆还原核心逻辑一样class Singleton { public: static Singleton* getInstance() { if (instance nullptr) { instance new Singleton(); } return instance; } private: Singleton() {} static Singleton* instance; };这段代码问题非常大。两个线程同时调用getInstance()都判断instance为nullptr然后都执行new结果就是创建了两个对象且其中一个被泄漏还有可能因为double-free导致程序崩溃。解决这个问题的方案有几种笔试中你最好都能写出来并且说清楚各自的优劣。方案一是加锁用std::mutex包裹new操作这样保证只有一个线程执行创建。但问题在于每次调用getInstance()都要加锁性能有损耗。方案二是双重检查锁DCLP先无锁判断一次再加锁判断一次。这个方案看起来很完美但在C11之前存在内存重排序的问题。好在C11之后如果instance是atomic类型的或者直接使用函数局部静态变量这个问题就解决了。最好的写法是俗称Meyers Singleton的写法class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } private: Singleton() {} };C11标准保证函数局部静态变量的初始化是线程安全的。编译器会在首次执行到声明处时生成一个隐藏的守护变量锁来保证只有一个线程执行初始化。这种写法既简洁又安全而且因为静态变量在程序结束时自动析构也就不需要手动释放内存。笔试答题时建议优先写出这种实现然后解释为什么它比加锁版更好。4.2 观察者模式与回调机制在RTC中的实际应用观察者模式在声网的笔试里经常以回调的形式出现。你看热搜词里也有“c回调函数例子”说明大家在准备这类题。但多数人理解的回调函数还停留在函数指针调用层面。声网想考察的是在C的工程架构里回调如何设计才能既灵活又安全。声网SDK的API大量使用观察者模式。C开发者需要注册一个回调接口比如本地用户加入频道、远端用户发布视频流、网络质量变化SDK内部在特定事件发生时调用这些回调通知上层应用。底层的事件分发线程可能是音频采集线程、网络线程或者主线程而上层应用收到回调后不一定会立刻处理。这里就涉及一个十分经典的问题回调在什么线程执行如何处理回调中对象的生命周期笔试中如果让你设计一个观察者模式你需要考虑以下几个点观察者注册与注销业务模块在初始化时注册回调在销毁时必须取消注册。如果观察者已经销毁而主题仍然持有它的裸指针主题触发通知时就会调用悬垂指针导致崩溃。解决方案是使用weak_ptr管理观察者通知时先尝试lock到shared_ptr如果成功则调用失败则说明观察者已析构跳过。回调的执行线程如果回调在音频线程里执行而回调内部做了耗时操作比如写文件、网络请求就会阻塞音频链路。所以工程上通常只在回调里做“接收状态”和“投递消息”的操作耗时逻辑扔到独立的工作线程里。回调的顺序多个观察者之间是同步调用还是异步投递是否需要保证顺序在RTC中音频数据包的处理顺序非常敏感乱序或者重入都可能导致声音异常。这些点结合起来就是一道很典型的“C设计模式在实时系统中的应用”考察题。答得好不好直接反映你是否有大型C项目的工程经验而不是只会在小demo里跑跑回调函数。5. 笔试实战策略与常见问题排查实录5.1 编程题的踩坑与提速技巧声网的笔试编程题我用血泪经验告诉你几个重点。第一输入输出一定要处理稳当。校招笔试通常用标准输入输出有些题目给的输入有比较烦人的格式比如一行输入多个整数中间有多个空格末尾还可能有换行。你可以用cin x来读它能自动跳过空白符比scanf安全。但如果你要读一整行字符串再解析建议使用getline搭配istringstream避免缓冲区残留问题。第二注意数据范围和算法复杂度。声网有一道印象比较深的题目是快速幂算法变种要求计算a的b次方对p取模。如果你真的用循环连乘遇到b是10^18的数据范围直接超时。这时候必须用快速幂时间复杂度是O(log b)。核心思路是把b拆成二进制每次把底数平方遇到二进制位为1就乘到结果里。笔试的时候如果对数据范围没把握先把int改成long long避免溢出。C里int一般是32位最大能表示约21亿稍微一乘就超了。第三合理使用STL但要知道STL的坑。比如vector 并不是真正的bool数组它是位压缩存储的取地址操作会失败。再比如std::sort不是稳定排序如果你需要保持相等元素的相对顺序要用std::stable_sort。还有map和unordered_map的选择数据量小、遍历有序时用map数据量大、只查不排序时用unordered_map。但unordered_map的哈希函数可能被精心构造的数据卡到O(n^2)笔试中遇到极端数据过不去时可以换成map保平安。5.2 遇到“看不懂题”时的拆题思路这个问题是很多校招生面试后跟我反馈最多的题目读第一遍完全不知道在说什么。我当年也经历过这个阶段后来总结了一个拆题方法分享给你笔试时特别管用。第一步把题目里涉及的所有名词列出来拆解成“已知条件”和“求解目标”。比如题目说“有一个长度为n的数组每次操作可以选一个子数组将该子数组内的所有元素加一求使得所有元素相等的最小操作次数”。读完不要慌先画出来数组、子数组、加一、相等、最小次数。画着画着你就会发现这其实是在求差分数组的某个性质。第二步拿最简单的样例手动跑一遍。笔试的题面一般会给示例输入输出。你哪怕是暴力穷举也要把示例弄明白理解样例的每一步变化。有时候题目的真实含义和你的第一直觉完全不同跑一遍样例能帮你矫正理解。第三步如果题目明确说了“数据规模”先根据规模猜测解法。n 1000大概率是O(n^2)的动态规划n 10^5大概率是O(n log n)的排序、二分n 10^9大概率是数学规律题或者矩阵快速幂。这个思路不能保证100%正确但能帮你快速锁定尝试方向。还有一点如果一道题卡了20分钟还没有任何思路果断跳到下一题别死磕。声网的笔试时间是固定的编程题不可能每一道都满分先把能拿的分全拿到再回头啃硬骨头。这里有个小技巧如果你能写出一个暴力解即使它超时也先提交上去。有些OJ是按特殊样例给分的暴力解至少能帮你拿一部分测试点的分总比空着强。5.3 笔试常见问题速查表我把声网C笔试题里最容易出现的坑整理成了一个速查表这是我自己刷题和跟学员复盘时总结出来的方便你考前快速过一遍。考察点常见陷阱正确的工程做法拷贝构造函数浅拷贝导致double free自定义深拷贝或默认删除拷贝并只允许显式克隆移动语义移动后源对象未置空移动后将源对象的指针成员设为nullptrshared_ptr多个线程同时写同一个shared_ptr约定只读不写或使用atomic_load/storeunique_ptr作为函数参数传递所有权明确引用传递或std::move转移所有权vector扩容元素移动拷贝耗时移动构造标记noexcept字符串c_str()返回的指针失效调用后立即使用不缓存指针多线程死锁加锁顺序不一致固定全局加锁顺序或std::lock批量加锁单例双重检查锁的重排序问题使用函数局部静态变量这里多说一句速查表是“查漏补缺”用的不是让你背的。你要在理解每个知识点背后的“为什么”之后再去复习效果才好。比如shared_ptr的多线程问题理解了控制块和对象本身分离的设计你自然就明白为什么“引用计数安全对象不安全”了。结束语声网2020校招这套通用C笔试题放到今天来看依然很有参考价值。它的难度不在于题目本身有多深而在于它处处在考察“你会不会用工程思维写C”。如果你能把多线程、内存管理、设计模式这些知识放在实时音视频的场景里去思考那么不管是声网还是其他任何RTC厂商的笔试面试你都具备了足够的底层竞争力。最后再分享一个我自己备考时的习惯每做一道题不只是改了答案就完事还会在评论区写下“这道题如果放到声网的真实业务里会出现在哪个模块”——这个习惯帮我打通了从做题到做工程的最后一公里。希望这份复盘对你也有帮助。
返回列表