声网通用C++笔试题解析:从constexpr到ABA问题

发布时间:2026/8/31 15:47:32
声网通用C++笔试题解析:从constexpr到ABA问题 声网2020校招的“通用C笔试题”光看“通用”这两个字估计很多人会先松一口气——不考音视频专有协议不考WebRTC源代码听起来门槛不高对吧但真等坐到电脑前开始做题才知道这个“通用”到底有多大的含水量。我当年刷完这套题的感觉是它根本不考你背了多少八股而是用一批朴素的C题目把你写代码的习惯、对内存和并发的敏感度、边界条件的处理方式全给照了出来。如果你正准备投声网这类做实时音视频底层的公司或者单纯想检验一下自己的C底子这篇文章会把热搜词里那些高频点constexpr、ABA问题、快速幂、回调函数、字符串数组初始化……串起来还原一份接近真实的笔试题拆解和备考思路。1. 声网这类实时音视频公司笔试题为什么叫“通用”1.1 RTC业务对C工程师的真实要求先把“通用”这两个字背后的业务逻辑说清楚。声网做的是RTCReal-Time Communication也就是实时音视频通信。它的Agora SDK要跑在Android、iOS、Windows、macOS、Linux甚至各种嵌入式平台上核心引擎基本是C写的。你想想一段音频从采集到播放中间要经过降噪、回声消除、编码、网络传输、抖动缓冲、解码、渲染任何一环多出几毫秒延迟、多占几个字节内存用户端就能感知到卡顿和杂音。所以声网的C工程师日常要面对的不是“写一个能运行的算法”而是“在受限的CPU和内存下写出稳定、低延迟、可跨平台的底层代码”。这套能力要求基本就决定了笔试的考察方向语言功底要扎实不能只停留在“能编译通过”对现代C特性要熟练因为大量底层模块都在用C11/14/17对并发和性能要有直觉因为音视频引擎里线程池、锁、原子操作到处都是。那问题来了这种业务能力怎么在校招笔试里考察直接考WebRTC源码解析显然不现实应届生里也没几个真啃过。于是就有了“通用C笔试题”这个折中方案不考具体业务但考的每一道题都能映射到真实工作里会用到的能力。1.2 “通用”背后的三层筛选逻辑我研究过这套题之后发现它的“通用”不是降低难度而是把考察范围限定在“不依赖特定业务背景的C核心能力”上。具体来说它想通过一份笔试题完成三层筛选。第一层筛掉语法基础不牢的人。比如constexpr是哪个C版本引入的、字符串数组初始化有哪几种写法、结构体链表怎么处理更安全这些题目没有半点炫技成分纯靠平时的积累。我不会说这些题“简单”——因为在实际笔试压力下能一次性写对的人真的不多。第二层筛掉只会刷OJ、不懂工程的人。很多LeetCode玩家能把快速幂算法背得滚瓜烂熟但你要他写一个线程安全的队列他可能连锁该加在哪里都搞不清楚。声网的笔试里算法题会考但通常不会出那种你见过就秒解、没见过就废的偏题怪题。它更看重的是把算法题写“干净”的能力。第三层筛掉没有性能意识的人。实时音视频是典型的性能敏感场景所以笔试题里关于多线程、内存管理、回调函数的题目往往藏着一个隐藏考察点你写的代码是不是高效、安全、可维护的。这一层筛的不是“会不会”而是“在真实工程里敢不敢用”。1.3 和音视频专项题的区别既然标题叫“通用”那就说明这类笔试大概率不会出现“请解释WebRTC的JitterBuffer工作原理”这种题。哪怕声网的核心业务和音视频强相关校招笔试阶段也会刻意把业务知识剥离掉。这是有道理的校招候选人还没入职要求他懂音视频是不现实的但一个学了两三年C的人如果连new/delete、shared_ptr、mutex都说不清楚那招进来也没法干活。所以“通用”是一道分水岭——它先确认你是一个合格的C工程师再谈后面的音视频业务培养。摸清这层逻辑你就知道备考的重点应该放在哪里了。2. 从热搜词反推考点的三个层次既然关键词是空的我结合声网这套笔试题的风格和网上最常被检索的C热点反推了一下考点分布。你会发现这些热搜词并不是彼此孤立的它们天然聚成了三个层次语言特性层、算法与数据结构层、并发与设计层。这三个层次也正好对应一份笔试卷子从易到难的梯度。2.1 语言特性层语法细节决定你能不能拿保底分这一层是热搜词里占比最大的也是很多人在笔试中最容易翻车的地方。随便列几个高频词c字符串数组初始化、c string库、c结构体链表基本语法、constexpr哪个c版本引入的、visual c redistributable、vscode配置c/c环境。先说constexpr。这是C11引入的关键字用来声明可以在编译期求值的常量表达式。声网这种对性能敏感的公司很多计算希望在编译期完成避免运行时开销。笔试题里很可能给你一段代码问你“下面哪句constexpr写法是合法的”“C14对constexpr做了哪些放宽”。这题考的不是背标准而是你在实际项目里用没用过constexpr。如果你平时写代码都是const替代一切这一分八成拿不到。再说字符串数组初始化。这是一个看着基础、其实细节非常多的话题。char str[] hello、const char* str hello、char str[6] {h,e,l,l,o,\0}这三者在内存布局、可修改性、sizeof结果上都有区别。笔试里经常把这几个混在一起出成判断正误题。我的建议是考前花半小时把指针、数组、字符串字面量之间的关系彻底搞透比盲目刷十道题都管用。2.2 算法与数据结构层不考难题但考你写不写得对热搜词里的算法相关词汇也很有代表性冒泡排序算法c、选择排序c、快速幂算法c、单调栈算法c、n个整数的最小公倍数怎么求c。声网的算法题难度上限大概在中等偏下。快速幂、最小公倍数、单调栈这些属于“你应该会但不见得写得对”的类型。什么叫写得对边界条件处理完整、代码可读、没有多余的复杂度。比如快速幂正常人都会写递归版本但你会不会写迭代版本会不会处理指数为负的情况会不会在取模运算时注意溢出这些才是笔试真正想看的东西。说句实在话我见过太多人把快速幂背得滚瓜烂熟但一让写最小公倍数就翻车。最小公倍数本质上要借助最大公约数而gcd的写法本身就藏着很多细节——辗转相除法的迭代写法是不是真的理解而不是背下来的。所以备考算法这层与其追求刷难题不如把常见题型的边界写扎实。2.3 并发与设计层拉开差距的核心战场如果我们把热搜词里c多线程、aba问题c、c回调函数例子、c设计模式、c八股文、c面试题这些词挑出来你会发现它们全都指向同一个主题并发、生命周期和设计能力。这一层才是声网笔试题真正拉开差距的地方。它通常会以这样几种形态出现让你分析一个多线程程序是否存在数据竞争给你一个回调函数的使用场景考察你在异步逻辑里如何管理对象生命周期或者问你ABA问题在无锁数据结构中如何解决。ABA问题是并发编程里的经典问题了。简单说就是一个线程读到A另一个线程把它改成B又改回A第一个线程再次比较时发现值还是A于是认为这段时间没有变化但实际上中间已经发生了两次修改。在实时音视频这种高频读写的场景里ABA问题极其容易引发隐蔽的bug。笔试里如果考到无锁队列、无锁栈大概率会追问ABA问题以及怎么处理常见方案是使用带版本号的原子指针比如std::atomic std::shared_ptr 或者用标签指针。但我想强调一句声网笔试里对并发和设计的考察并不要求你写出多么惊天动地的无锁结构。它更在意的是你知不知道锁的粒度该多大、能不能避开死锁、unique_lock和lock_guard有什么区别、回调为什么容易造成悬空引用。这些才是真正的工程素养。下面的表格是我根据热搜词整理出来的“关键词→潜在考点→出题形态”映射可以帮你快速对齐备考方向热搜关键词潜在考点可能出题形态constexpr哪个c版本引入的C11/14标准差异、编译期计算选择题/改错题c字符串数组初始化指针与数组、内存布局、sizeof判断正误/阅读代码快速幂算法c分治、迭代实现、溢出处理手写代码n个整数的最小公倍数最大公约数、数论基础手写代码/代码补全单调栈算法c栈的单调性、问题抽象代码填空/算法设计c多线程锁、原子操作、数据竞争找错/改错aba问题cCAS、无锁数据结构、版本号概念题/方案设计c回调函数例子函数指针、std::function、生命周期代码阅读/补全冒泡排序/选择排序排序稳定性、复杂度手写代码/代码优化c设计模式RAII、观察者、单例设计题/代码改错c八股文/c面试题综合基础能力综合笔试题3. 模拟试卷与逐题解析贴近真实风格的C题目拆解吃透考点之后我们直接进入实战环节。下面这份模拟卷是我根据声网这套“通用C笔试题”的总体风格构造出来的题型、难度、考察点都尽量贴近真实。每道题我都会给出参考思路和踩坑提醒尤其是那些容易被忽略的细节。3.1 选择题constexpr的版本与限制题目关于constexpr描述正确的是A. C11引入constexprC14放宽了constexpr函数的限制。 B. C11的constexpr函数体内可以包含循环语句。 C. constexpr变量必须用编译期常量初始化否则编译报错。 D. constexpr可以用在虚函数上。正确答案是A。这道题的迷惑性在于很多人只知道C11有constexpr不知道C14对它做了很大的放宽。C11时代的constexpr函数体非常受限只能包含一条return语句到了C14才允许在constexpr函数里写循环、局部变量、if分支这让很多原本只能在运行时计算的逻辑可以搬到编译期。选项C看起来正确但有一个例外constexpr变量也可以被函数参数等非编译期常量初始化C20之后甚至允许constexpr的虚函数和动态分配。所以如果你停留在“constexpr 常量表达式”这个粗糙理解上这道题很容易选错。我实际笔试时对这类题的态度是不仅要选对还要能在旁边写出依据。比如A选项我会顺手标注C11的constexpr函数体限制和C14的放宽细节。这不会体现在分数上但能逼自己把标准吃透。3.2 代码题字符串数组初始化的陷阱题目给定以下声明写出各自的sizeof结果。char s1[] hello; const char* s2 hello; char s3[5] {h, e, l, l, o};如果你是靠“字符串有个结尾的\0”这个记忆来答题那这题就到陷阱里去了。真正的答案是sizeof(s1) 6因为包含末尾的\0。sizeof(s2) 864位系统指针大小它只是一个指向字符串字面量的指针和字符串长度无关。sizeof(s3) 5因为显式指定了5个char没有位置放\0它根本不是一个C风格字符串。很多人在写字符串处理相关的代码时习惯性认为char数组就一定以\0结尾于是用strlen去计算长度结果在s3这种场景下直接把缓冲区读到越界。声网这种做网络协议的公司对字符串边界、缓冲区溢出的敏感度要求极高这类题其实是在提醒你不要对数组内容做任何隐含假设。3.3 手写题快速幂算法的迭代实现题目实现一个函数计算a的n次方并对MOD取模。要求时间复杂度O(log n)n可以为负数。快速幂的递归版本我相信大部分人能写出来但迭代版本才是考察工程能力的重点。long long fastPow(long long a, long long n, long long mod) { if (mod 1) return 0; long long res 1; a % mod; while (n 0) { if (n 1) res res * a % mod; a a * a % mod; n 1; } return res; }这里有几个细节必须注意。第一long long的使用。a的平方很可能溢出int范围必须用long long承接中间结果。第二n为负数的情况。如果要求支持负数次幂可以转化为fastPow(a, -n, mod)的逆元来处理前提是模数为质数、a和mod互质。但很多OJ题默认n为非负所以有两种做法要么在函数入口判断n0要么在题目描述里明确不考察负数。笔试时一定要先搞清楚题设。第三mod 1的边界。任何数对1取模都是0如果不加这个判断res初始化为1就会直接返回错误结果。这种边界条件恰恰是“刷题选手”和“工程选手”的分水岭——面试官一眼就能看出你有没有处理过真实边界。3.4 求最小公倍数的完整解法题目给定n个整数求它们的最小公倍数。求两个数的最小公倍数很简单lcm(a, b) a / gcd(a, b) * b。但n个数的版本有很多细节值得展开。第一为什么要求顺序两两合并而不是直接乘起来再除两个原因一是防止溢出先除后乘能把中间结果控制在合理范围二是如果先乘后除在数值比较大时直接就爆了long long。第二gcd的实现用迭代更稳妥long long gcd(long long a, long long b) { while (b) { long long t a % b; a b; b t; } return a; }第三求n个数的时候初始值应该设为1还是第一个元素从可读性和边界安全性出发我建议初始化为第一个元素然后循环i从1开始两两合并。这样如果n1直接返回a[0]不需要额外分支。这类题的考察意图非常明显它根本不指望你发明什么新算法就看你能不能把已知的数学原理用代码正确地表达出来。而“正确”这一条恰恰是最多人做不到的。3.5 并发场景下的回调与生命周期题目有一个异步回调接口当某个异步任务完成时会调用传入的回调函数。请指出下面代码的问题class Task { public: void setCallback(std::functionvoid() cb) { callback_ cb; } void run() { // 异步执行 std::thread([this]() { // do something callback_(); }).detach(); } private: std::functionvoid() callback_; };这段代码的问题非常典型如果Task对象在异步线程执行callback_()之前被析构了callback_已经成为一个悬空引用调用它就会造成未定义行为。这就是实时音视频引擎里非常常见的“回调生命周期陷阱”。正确的做法有很多种核心思路是确保回调执行期间对象仍然存活。可以使用std::shared_ptr管理Task生命周期在lambda里用智能指针持有对象或者把任务提交到线程池并保证线程池的回收顺序晚于对象的析构。class Task { public: void setCallback(std::functionvoid() cb) { callback_ std::move(cb); } void run() { auto self shared_from_this(); std::thread([self]() { // do something if (self-callback_) self-callback_(); }).detach(); } private: std::functionvoid() callback_; };这里用shared_from_this()的前提是Task继承std::enable_shared_from_this 并且对象必须由shared_ptr管理。笔试时如果时间紧张写出这个方案就能拿到大部分分数。关键在于你要能说清楚为什么要这么做而不是单纯背一个“shared_from_this”的用法。3.6 ABA问题的解释与应对题目请你解释什么是ABA问题以及在C无锁编程中如何解决它。ABA问题的本质是CAS操作无法区分“值没变过”和“值变过又变回来了”。在多线程场景下如果线程T1读取到共享变量值为AT2把A改成B再改回AT1继续CAS时发现当前值还是A于是CAS成功但这期间的状态实际上已经发生了两次修改数据可能已经被破坏。解决思路主要有三种。第一种使用带标签的指针例如将值和版本号打包成一个原子变量。C里可以用std::atomicuint64_t高32位存值低32位存版本号每次修改都让版本号递增。第二种使用std::atomicstd::shared_ptr 既保证指针的原子操作又让对象生命周期得到管理。这是C20标准库提供的方案但在C11/14下要自己封装。第三种避免使用无锁结构改用互斥锁。很多人觉得“无锁”比“有锁”高级实际上在实时音视频场景里锁竞争不激烈的前提下mutex的开销完全可接受。能写出稳定的有锁代码远比写一个隐患丛生的无锁结构更受面试官认可。4. 代码能力之外的隐性考察点如果你以为这种“通用C笔试题”只考代码那就大错特错了。我做完之后复盘发现有相当一部分分差来自“代码能不能被维护”和“边界条件处不处理得干净”。这些点不会出现在题目描述里但会渗透在阅卷人的评价标准中。4.1 边界条件的敏感度你写一个函数正常路径跑通很容易难得是把它考倒。比如前面提到的快速幂对mod1的处理最小公倍数的n1场景字符串数组初始化里\0的有无这些都是最典型的边界。还有一个高频考察是数组下标越界。不要以为这种低级错误不会出现在笔试里。我亲眼见过有人在笔试代码里写for (int i 0; i n; i)而且用的还是i n。当n恰好等于数组长度时这一下就越界了。很多真实系统里的崩溃就是这个“差一错误”的具象化。所以如果你在笔试里遇到填空题或者代码修改题第一反应应该是检查循环边界和数组长度而不是急着看业务逻辑。4.2 内存与生命周期管理的自觉C和其他语言最大的不同就是资源需要你显式管理。声网的笔试题里只要是涉及指针、智能指针、回调的题都可能藏着“对象什么时候释放”这个灵魂拷问。我建议大家在做题时每写完一个new就强迫自己问一句这个对象在哪里delete如果答不出来说明设计有问题。写回调时问一句异步执行时调用方的对象还活着吗如果存活性不能保证就得考虑用shared_ptr或weak_ptr。更高级一点的考察是RAII思想。很多工程素养好的候选人会在设计题里主动用unique_ptr管理互斥锁、用scope_guard处理异常路径上的清理工作。这些不会明确出现在题目要求里但一旦你用上了阅卷人立刻就知道你是一个有真实项目经验的人。4.3 调试与排查能力的体现有些笔试的第二轮或附加题会给你一小段有bug的代码比如一个多线程程序或者一个函数参数传递有问题的实现让你找出问题并修复。这种题想考察的不是你能不能编译通过而是你怎么定位问题。实用的定位思路是先看数据结构和算法有没有问题再看资源管理有没有问题——有没有动态分配没有释放有没有智能指针循环引用最后再看并发——有没有共享变量没有加锁有没有锁的粒度过大导致性能瓶颈。这就是我们平时排查问题的顺序笔试里的找错题只不过是把这个过程浓缩在一段代码里而已。5. 基于声网业务场景的拓展题通用之外的门槛聊完通用题本身我再补充一类很容易出现在声网这类公司后续面试或者加试中的题目。它们名义上还是“通用C”但已经能看出音视频业务的影子了。5.1 线程安全环形缓冲区音频数据的处理是流式的一边采集线程往里写一边播放线程往外读。用环形缓冲区Ring Buffer来解耦这两个线程是经典做法。考题通常会让你实现一个无锁或加锁的环形缓冲区要求支持并发读写。它的核心难点在于生产者和消费者的读写指针如何同步缓冲区满和空如何判断以及如何避免数据竞争。简单的实现是用mutex保护读写操作更进阶的用原子变量配合内存序memory order做无锁方案。声网这种低延迟场景每一帧音频数据都晚一刻到达都会造成音画不同步所以这种题完全符合业务气质。5.2 高性能日志组件日志模块几乎是所有公司的C笔试“附加题”最爱。为什么因为它麻雀虽小五脏俱全需要线程安全多线程同时写日志、需要性能写日志不能阻塞业务逻辑、需要格式化各种类型拼接、需要文件管理按天/按大小切割。要拿到高分关键在于你会不会用双缓冲异步写日志会不会用全局单例来管理日志器能不能处理好格式化时的异常安全。这些都是真实项目中反复出现的工程问题。回到声网的场景RTC SDK跑在嵌入式设备上日志既不能影响通话质量又要能帮助排查故障这个平衡本身就是一张考卷。5.3 定时器与事件循环音视频引擎里有大量定时任务超时重传、丢包统计、缓冲区水位检测。所以一个定时器模块也经常被当成设计题。它要支持注册回调、取消任务、定时触发并且所有操作要考虑到线程安全。你不需要实现一个像libevent那样复杂的定时器管理库但至少应该知道用优先队列维护定时任务、用时间轮做高效超时管理这些基本思路。这类题没有标准答案面试官看的是你能否从需求出发设计出一个结构清晰、可扩展的方案。从我实际和其他参加过这类笔试的朋友交流的情况来看声网这套“通用C笔试题”真正的难点不在于某道题有多刁钻而在于它考察的是一个人长期养成的编码习惯短时间突击很难大幅提升。所以与其焦虑地背八股不如把功夫花在平时每写一段代码都多想想边界和资源管理每次多线程调试都多问一句竞争条件每看到一个回调函数都习惯性地检查对象的生命周期。如果你能从现在开始有意识地用这套标准要求自己那不管遇到的是声网还是其他做底层C的公司笔试这关都不会是问题。