ARTICLE DETAIL

资讯详情

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

爱奇艺C++开发笔试题全解析:从基础语法到并发核心考点

爱奇艺C++开发笔试题全解析:从基础语法到并发核心考点 我在整理旧资料的时候翻到了一套2017年秋招的笔试题其中爱奇艺的C开发工程师笔试卷让我印象特别深。为什么单独说它因为这套卷子几乎就是当年互联网视频类公司后端C岗位笔试的标本题型分布完整、难度梯度合理、知识点既不冷门也不偏怪但基本功不扎实的人真的容易在它身上吃亏。无论你是在备战秋招还是工作几年想回头补补C基础把这种卷子彻底吃透收益都远大于闷头刷几十道LeetCode。这张卷子背后涉及的考点翻来覆去其实就集中在C基础语法、新特性、操作系统、数据结构与算法、并发与设计模式这几个方向上。下面我按这套笔试卷的常见考察路径把每一类核心考点、解题思路、易错点和实际工程中的关联一层层剥开来讲。内容偏长但都是能直接用的东西。1. 试卷结构拆解一场笔试为什么要这样设计1.1 题型分布与考察维度我拿到这套卷子最大的感受是爱奇艺的出题人非常清楚自己需要什么样的人整套题不是为了把你考倒而是为了在有限时间内识别出哪些人具备扎实的C功底、哪些人只是背过八股文。我把常见的试卷结构整理成了下面这个表题量和分值是综合当年众多面经后的合理估计具体数值可能有浮动但考察维度基本一致题型参考题量参考分值覆盖维度建议用时选择题15-20题40-50分C语法、STL、操作系统、网络、数据库40分钟简答题3-5题15-20分代码分析、并发、设计思路30分钟编程题2-3题30-40分算法与数据结构、边界处理、代码规范60分钟选择题看着轻松其实是用来筛人的第一道关。它们的覆盖面很广经常混进一两道关于C11/14新特性的题比如constexpr、auto、智能指针。这类题不背真的不会但光背不理解也容易掉进出题人挖的坑。简答题更偏向于让你分析一段代码的输出或指出隐患考的是读代码的能力。最后两道编程题才是拉开差距的关键一般是一道偏数据结构链表、二叉树、栈一道偏算法思维快速幂、单调栈、动态规划。我当年做这类卷子时的策略是选择题尽量控制在40分钟内遇到拿不准的果断先标记跳过简答题写关键点不写废话编程题至少留足60分钟先写暴力解保底再优化。这个节奏很关键很多人不是在难题上丢分而是在基础题上磨蹭太久最后编程题写不完。1.2 从爱奇艺的业务属性反推考点这不是一句空话。爱奇艺是做流媒体视频的后端C开发工程师的工作会直接接触到播放链路、CDN调度、弹幕评论、用户量、推荐策略这些业务。笔试虽然不会直接考业务但考点的偏向会明显向高并发、内存效率、系统底层靠拢。所以你会发现这类卷子里关于进程与线程区别、死锁条件、TCP和UDP差异、HTTP状态码、内存对齐、RAII这类问题出现频率很高。而C岗位和Java岗位最大的区别就是考点中会大量出现指针、内存管理、编译链接、多线程同步这些偏底层的东西。如果你只是会写业务代码没理解过汇编层面发生了什么选择题很容易在这几个点翻车。我自己在带新人的时候也发现能把这套试卷里底层问题讲清楚的人写出来的代码在稳定性和性能上通常都明显更好。这就是笔试考察底层逻辑的意义所在。2. C语言基础选择题里藏着的送分题和送命题2.1 字符串数组初始化细节决定成败字符串这个知识点在C笔试卷里出现频率极高而且是最容易被轻视的送命题。很多人在刷LeetCode时习惯用std::string但笔试选择题偏偏爱考C风格字符串和字符数组的区别。看这段代码char s1[] abc; char s2[] {a, b, c}; std::cout sizeof(s1) std::endl; // 输出4 std::cout sizeof(s2) std::endl; // 输出3 std::cout strlen(s1) std::endl; // 输出3sizeof(s1)是4因为s1结尾隐式包含一个\0而s2没有显式加\0长度就是3。如果你把s2直接传给strlen(s2)函数会一直往后读内存直到遇到一个0字节这在笔试改错题里是一个非常经典的越界场景。除了这个基础点还有一个高频题是“字符串转数组”“C字符串转数组”经常出现在搜索热词里说明确实很多人卡在这。你要知道std::string和C字符串之间转换的几种方式std::string str hello; // string 转 char* const char* p str.c_str(); // string 转 char 数组 char buf[16] {0}; str.copy(buf, str.size(), 0); // char* / char 数组 转 string std::string str2 buf;这里有个很容易被忽略的细节c_str()返回的是一个const char*如果你试图通过它修改字符内容编译期就会报错。而且c_str()指向的内容在字符串对象被修改或销毁后就会失效绝不能长期持有。笔试的代码分析题里特别喜欢在这个位置埋坑。如果你在做编程题时觉得C的输入输出太慢可以在main函数开头加这两行std::ios::sync_with_stdio(false); std::cin.tie(nullptr);这两行的作用是取消C标准流和C标准流之间的同步并且让cin不再每次输出都强制刷新缓冲区能明显提高大数据量输入输出的速度。但要记住一旦取消同步后cin和scanf就不能混着用了否则可能读取顺序错乱。这也是一个常见的笔试考点。2.2 结构体与链表基本功不能丢“C结构体链表基本语法”能在热搜词里出现说明很多应届生在这块的基础其实不牢。笔试里链表题通常不长但特别能检验一个人的基本功是否扎实。我印象里这套卷子的简答题部分或者编程题的第一小问很容易出现“反转链表”这种题目。很多同学能写出思路但一到手写代码就出问题不是忘了处理空链表就是指针搞乱。我建议把迭代和递归两种写法都背熟struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; // 迭代反转 ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* cur head; while (cur) { ListNode* nextNode cur-next; cur-next prev; prev cur; cur nextNode; } return prev; } // 递归反转 ListNode* reverseListRecursive(ListNode* head) { if (!head || !head-next) return head; ListNode* newHead reverseListRecursive(head-next); head-next-next head; head-next nullptr; return newHead; }迭代版本的重点是nextNode这个临时变量必须一开始就保存否则你把cur-next改掉之后后面的节点就找不到了。这是大多数人第一次手写链表反转最容易翻车的地方。递归版本的思考方式不太一样它假设函数已经帮你反转好了后面所有节点你只需要把当前节点接到最后一个节点后面。这种思路在二叉树题目里也会反复用到理解它比背代码更有价值。在这类题目里涉及内存管理的时候要养成一个思维习惯new出来的节点一定要考虑什么时候delete。笔试可能不会要求你手动释放内存但面试官经常会追问“这个链表怎么释放”如果你回答不上来前面的代码写得再好也会扣印象分。3. C新特性与底层原理从constexpr到智能指针3.1 constexpr从C11开始引入的编译期计算如果你搜“constexpr哪个c版本引入的”答案很明确C11。这是当年C标准化进程等待多年后的一次重大更新C11把一堆现代语言里早就有的能力带进了Cconstexpr就是其中标志性的一个。很多初学者会把const和constexpr搞混其实它们的核心区别是求值时机和语义定位。const只表示“这个变量在本作用域内不能被修改”它的值既可以来自编译期常量也可以来自运行时数据而constexpr是“强制在编译期求值”的意思用来告诉编译器“这个表达式我相信你一定能在编译阶段就算出来”。举个例子const int a rand(); // 合法a在运行时被初始化 constexpr int b rand(); // 不合法rand() 不是常量表达式 constexpr int square(int x) { return x * x; } int arr[square(5)]; // 编译期常量数组大小可以由此确定在C11标准里constexpr函数体内能用的语句限制很严基本只能有一条return语句。到了C14放宽了这个限制允许在函数内有局部变量、循环、条件分支。所以如果你在网上看到一段C11下的constexpr函数写得像压缩饼干一样难看不要惊讶那是因为当年的编译器只认识那种写法。笔试里比较常见的一个点是用constexpr实现编译期阶乘或者斐波那契数列然后问你编译后生成的代码里是否还包含运行时的计算逻辑。答案是只要参数是常量表达式编译器就会在编译期把结果算出来直接替换成常量不会生成运行时代码。这也是assert和模板元编程中大量使用constexpr的原因。理解这一点后你还会发现constexpr和static_assert是绝配。笔试写代码时可以用static_assert在编译期检查一些常量约束比如static_assert(sizeof(int) 4, int must be at least 32 bits);这种代码在工程里能帮你提前发现平台差异问题在笔试中写出来也算一个小加分项。3.2 智能指针与移动语义现代C代码风格的标配2017年的时候C11已经发布六年了C14也已经落地所以笔试卷子里的C新特性题占了不小比例。最常见的是auto_ptr、unique_ptr、shared_ptr、weak_ptr之间的区别以及移动语义的判断。这里我先说一个很多人踩过的坑auto_ptr已经在C11中被明确废弃原因是它的拷贝行为是“所有权转移”一个指针同时只允许一个auto_ptr拥有它每次拷贝都会偷偷把原指针置空这会导致很多隐蔽的运行时崩溃。替代它的是unique_ptr它直接把拷贝构造函数删除从语法层面禁止复制但允许移动std::unique_ptrint p1 std::make_uniqueint(42); // std::unique_ptrint p2 p1; // 编译错误拷贝被禁止 std::unique_ptrint p3 std::move(p1); // 合法所有权转移这个“拷贝被禁止、移动被允许”的设计完美体现了C11引入右值引用和移动语义的初衷把“值语义”的对象在传递过程中变成“资源转移”避免不必要的一次深拷贝。shared_ptr则是基于引用计数的智能指针它允许多个指针共享同一份资源最后一个引用消亡时自动释放资源。但使用它时有一个著名的坑循环引用。struct Node { std::shared_ptrNode next; std::shared_ptrNode prev; }; // 两个节点互相引用就会导致引用计数永远不为0内存泄漏解决方式是把其中一个方向改成weak_ptr。weak_ptr不增加引用计数只提供一个“安全观察”资源的渠道。笔试选择题里经常给你一组代码让你判断是否发生内存泄漏或者问weak_ptr.lock()返回的是什么。这里的答案就是lock()返回一个shared_ptr如果资源已经被释放返回空指针。笔试简答题还可能让你写一个shared_ptr简化版本。我建议你至少掌握引用计数的基本思想一个shared_ptr对象内部除了保存原始指针外还会保存一个指向控制块的指针控制块里存着引用计数。每次拷贝时计数1析构时计数-1减到0就释放资源和控制块。能把这个讲清楚面试官就知道你是真懂智能指针而不是背了几个类名。4. 算法题高发区编程题的核心方法论4.1 快速幂从一个看似简单的题目说起“快速幂算法c”是C搜索引擎里常青的搜索热词因为它确实是笔试算法题的“开胃菜”级别题目但多数人第一次做都会卡住。题目通常是这样给你三个整数a、b、m计算a^b mod m的结果。如果直接暴力循环时间复杂度是O(b)当b是10^9级别时完全不可行。快速幂的核心思路是二进制分解。把b拆成二进制之后a^b可以表示成若干个a^(2^i)的乘积。比如b13二进制是1101那么a^13 a^8 * a^4 * a^1。而我们只需要不断对底数做平方就能依次得到a^1, a^2, a^4, a^8再把对应二进制位为1的项乘起来long long fastPow(long long a, long long b, long long mod) { long long res 1 % mod; a % mod; while (b 0) { if (b 1) { res res * a % mod; } a a * a % mod; b 1; } return res; }这个代码有两个容易出错的地方。第一个是res的初始值如果mod1任何数对1取模都是0所以初始化为1 % mod是最稳妥的写法。第二个是乘法溢出问题res * a和a * a在a和res都接近10^9时乘积会超过32位整数的范围。所以要么用long long要么用__int128这是在笔试环境下特别容易被忽略的细节。扩展一步快速幂不止能算整数幂还能扩展到矩阵快速幂用来求解斐波那契数列的第n项。斐波那契的转移矩阵是[[1,1],[1,0]]求它自乘n次后的第一个元素就能得到Fib(n)时间复杂度O(log n)。这类题在2017年秋招里属于“进阶题”但理解快速幂之后矩阵版本就是套壳。4.2 排序算法冒泡、选择与快排的取舍“冒泡排序算法c”和“选择排序c”这两个搜索词热度一直很高说明排序算法是笔试绕不开的基础盘。但笔试不会只考你排序的实现更爱考不同排序算法之间的对比。我把这几个算法整理成了一张对比表笔试和面试都可以直接用算法平均时间复杂度最坏时间复杂度空间复杂度稳定性特点冒泡排序O(n²)O(n²)O(1)稳定实现简单接近有序时效率尚可选择排序O(n²)O(n²)O(1)不稳定交换次数少但比较次数多快速排序O(n log n)O(n²)O(log n)不稳定实际效率高STL sort的基础之一归并排序O(n log n)O(n log n)O(n)稳定外部排序、链表排序常用为什么选择排序不稳定因为它会跳过中间元素直接和很远位置的元素交换可能把相同值的相对顺序打乱。这是选择题里很经典的考点。至于快排很多同学会问“最坏情况是O(n²)为什么还快”答案是只要选取划分点的方式合理快排在随机数据上的期望复杂度就是O(n log n)而且它的常数因子比归并排序小得多所以在内存数组中性能非常好。你可以看到C标准库的std::sort并不是纯快排而是采用了introsort——一种混合排序策略。它先做快速排序当递归深度超过一定阈值时改用堆排序保证最坏情况时间复杂度当区间长度小于16时改用插入排序利用局部性优势。这个设计思路在笔试简答题里偶尔会出现真正理解了它你对排序的理解就不是停留在背代码层面了。4.3 单调栈一类问题的通用思考方式“单调栈算法c”是一个能区分“刷过题”和“没刷过题”的考点因为它在教科书里讲得不多但在笔试和面试逻辑题里太常见了。单调栈的思想其实很简单维护一个栈让栈内元素保持单调递增或递减。入栈时如果当前元素“破坏”了单调性就把栈顶元素出栈直到恢复单调性。别看规则简单它能高效解决一类问题“找数组中每个元素左边或右边第一个比它大/小的元素”时间复杂度能做到O(n)。最经典的一道题是“每日温度”给定未来几天的温度列表你需要输出一个数组每个位置表示要等多少天才能等到更高温度没有就填0。vectorint dailyTemperatures(vectorint temps) { int n temps.size(); vectorint ans(n, 0); stackint st; // 存下标栈内温度保持递减 for (int i 0; i n; i) { while (!st.empty() temps[i] temps[st.top()]) { int idx st.top(); st.pop(); ans[idx] i - idx; } st.push(i); } return ans; }这个代码的精髓在于栈里保存的每个元素下标都意味着“我还在等待一个更大的温度”一旦遇到比它大的当前温度就结算答案。每一个下标最多入栈和出栈各一次所以总体复杂度是O(n)。能把单调栈理解到这个程度再去做“柱状图中最大的矩形”“接雨水”这些问题思路就会通很多。它们本质上都是“找左右边界”的问题只是边界定义不同。笔试编程题的重点不是让你纯背模板而是看你能不能把问题转化成单调栈模型。4.4 最小公倍数与最大公约数数学与代码的桥“n个整数的最小公倍数怎么求c”这个搜索词出现频率高是因为它经常作为一道编程题的第一问或者作为综合题里的小工具函数。单独出难题有难度但作为一个基础考点它的坑不少。核心还是最大公约数用辗转相除法欧几里得算法int gcd(int a, int b) { return b 0 ? a : gcd(b, a % b); }理解了gcd后两个数的最小公倍数公式是long long lcm(int a, int b) { return 1LL * a / gcd(a, b) * b; }注意这里必须先用a / gcd(a, b)再做乘法因为如果先做a * b在a和b都接近10^9时乘积会溢出32位整数而long long虽然一般够用但严格来说还是建议先除后乘把中间结果控制在更小的范围内。如果是求n个数的最小公倍数方法是逐个递推先用前两个数求lcm再用这个结果和第三个数求lcm以此类推。这个思路比“一次性求所有数乘积再除”更稳。还有一个容易忽略的点n个数里如果出现0结果本身就没有定义所以笔试时记得处理或者和面试官确认输入约定。5. 并发与设计C开发工程师的进阶考点5.1 ABA问题CAS背后的经典陷阱“aba问题c”这个关键词能上榜说明它在C面试和笔试中确实是个被人反复问到的考点。我第一次听到这个词是在看无锁队列代码的时候第一反应是“这也能叫问题”后来在真实生产环境踩了一次坑才明白它有多隐蔽。先解释CASCompare-And-Swap。它是一条原子指令逻辑是比较某个内存地址当前的值是否等于一个预期值如果是就把新值写入否则不做任何操作。C11的std::atomic提供compare_exchange_strong和compare_exchange_weak就是干这个的。ABA问题的场景是这样的线程T1读取内存地址中的值为A然后线程T2介入把这个值改成B然后又改回A。此时T1继续执行CAS发现内存值还是A与它之前读取的预期值相等于是CAS成功。但问题在于值虽然变回A但内存背后可能发生了一连串变更比如指针指向的对象已经被释放、又有一块新对象恰好分配到同一地址。T1以为它操作的是原来的数据实际上那个数据可能已经换掉了。这句话用生活类比来解释就是你把车停在停车场用遥控钥匙锁车记录钥匙状态A。一个代客泊车的工作人员把车开走状态B办完事又把车开回来状态A。你回来按遥控钥匙车能解锁你肯定以为车没动过。但如果车里被换了东西、里程变了你根本不知道。CAS看到的永远是“表面值相同”判断不了“中间是否发生过变化”。解决ABA问题的通用方法是给变量加版本号或标记。每次修改时不仅改变量本身还要让版本号递增。CAS比较时同时比较值和版本号这样即使值变回A版本号也不可能变回去。在C里可以自己封装一个带版本号的结构体来存放数据和计数。struct Msg { int value; int version; }; std::atomicMsg atomicMsg; Msg expected atomicMsg.load(); Msg desired {newValue, expected.version 1}; while (!atomicMsg.compare_exchange_weak(expected, desired)) { desired.version expected.version 1; // 失败时 expected 会被更新为当前值继续重试 }笔试如果你能答出“使用带版本号的原子变量”或者“使用双重引用计数”已经能证明你对这个问题的理解不是停留在概念层面了。5.2 回调函数与设计模式C业务代码怎么落地“c回调函数例子”和“c 设计模式”这两个搜索词也经常被放在一起。笔试卷子里经常会出一道简答题让你说说观察者模式或者策略模式在C里怎么实现。很多同学会背Java版本的代码但C的写法和Java差别其实不小。传统C语言风格的回调函数是用函数指针实现的。C11之后标准库提供了std::function它像是一个“可调用对象的容器”既能放函数指针也能放lambda表达式还能放函数对象。这样写回调就灵活太多了。#include functional #include vector #include iostream class EventEmitter { public: using Handler std::functionvoid(int); void addHandler(Handler h) { handlers.push_back(std::move(h)); } void notify(int value) { for (auto h : handlers) { h(value); } } private: std::vectorHandler handlers; }; int main() { EventEmitter emitter; emitter.addHandler([](int x) { std::cout handler1 receive: x std::endl; }); emitter.addHandler([](int x) { std::cout handler2 receive: x * 2 std::endl; }); emitter.notify(10); return 0; }这段代码其实就是观察者模式的一个剪影。事件源不关心谁订阅了它只负责在事件发生时挨个通知订阅者也不关心事件源内部逻辑只负责对事件做出反应。在视频网站业务里比如播放器状态变化、弹幕消息分发、缓存淘汰策略都可以用这种模式解耦。笔试复盘的时候有一个点容易被忽略std::function产生的开销比裸函数指针要大因为它可能通过类型擦除保存任意可调用对象。在性能敏感的环境里比如涉及视频编解码的链路能不用std::function就不用。笔试如果你在答案里主动提到这一层会让面试官觉得你有工程素养。这其实揭示了设计模式的一个本质模式是为了维护可维护性付出的代价是运行时开销和抽象复杂度。具体场景里值不值得用要看业务量级。6. 笔试常见问题与调试技巧实录6.1 编译与运行环境常见问题速查表不管题写得多好真要让你在本地环境里复现代码环境配置往往会浪费大量时间。很多同学搜索“vscode配置c/c环境”“visual c redistributable”这类关键词其实都是在这上面卡过壳。我把笔试实操中常见的编译运行问题整理成了一份速查表遇到直接对照报错或现象最常见原因解决建议编译不通过提示error: ‘to_string’ is not a member of ‘std’未启用C11标准编译加-stdc11或更高版本运行时提示VCRUNTIME140.dll 缺失缺少Visual C运行库安装对应架构的Visual C RedistributableVS中链接报LNK2019 无法解析的外部符号只声明未实现或未正确链接库检查函数是否定义了实体头文件是否对应实现运行后提示访问冲突0xC0000005空指针或野指针操作检查指针初始化先判空再解引用栈空间耗尽程序闪退递归无终止条件或递归太深检查基线条件必要时改为迭代输出答案和期望不一致多输出了空格或换行严格按题目输出格式末尾不加多余空白另一个高频问题是不知道如何正确编译多文件项目。笔试一般要求一个.cpp文件搞定但如果工程里分模块你得知道g main.cpp utils.cpp -stdc11 -O2 -o main-O2优化选项在笔试时建议保留它能让你的暴力解法在时间限制边缘多一分生存空间。同时建议开启-Wall编译器给出的警告能帮你发现很多问题比如未使用的变量、有符号和无符号比较等。如果你用的是VS Code我建议提前把C/C插件装好并且确认能正常调试。很多同学平时在IDE里写代码很顺一到笔试题环境就卡在“不知道错在哪里”。提前掌握一个工具链是值得的。6.2 做题时最容易翻车的几个细节点编程题得分率低很多时候不是因为算法不会而是因为细节处理不到位。我把这些年做笔试题最常见的翻车点总结在这里第一个是边界条件。几乎所有算法题都要考虑n0、n1、输入为空、输入只有一个元素这些特殊情况。快速幂里b0返回什么链表反转里headnullptr能不能直接返回这些分支不处理轻则答案错误重则直接段错误。我建议每道题写完代码后第一件事就是用最小用例、空用例、极端用例各跑一遍。第二个是数组越界。很多人写for (int i 0; i n; i)去遍历长度为n的数组最后一个索引访问到了不存在的元素。C不会像Java那样抛异常它直接读取相邻内存的垃圾数据导致各种玄学Bug。这种问题在笔试环境下特别难排查因为本地偶尔能通过提交系统里数据一大就挂。第三个是整数溢出。笔试题里涉及乘法、加法的地方要时刻警惕int溢出。判断两个数相乘是否超过INT_MAX时不要直接用a * b limit因为a * b本身可能已经溢出了。应该改成a limit / b这种等价的除法判断方式。第四个是忘记初始化。局部变量、数组、结构体成员必须显式初始化。笔试题里经常有int* p; p-next ...这种没有new就直接解引用的代码改错题看到这种基本就是考点。第五个是递归路径里的资源释放。递归函数里如果new了对象必须保证在return之前释放否则每递归一次就泄漏一块内存。写递归函数时把“释放资源”和“返回结果”一起考虑可以避免很多麻烦。6.3 备考时值得注意的路线建议再说几句题外话很多人在准备C笔试时容易走偏天天去搜“c好玩的小游戏”“c爱心代码”这类看起来有趣、实际对考试帮助有限的内容。不是说这些不能学而是要注意优先级。这些小项目能帮你锻炼语言熟练度但笔试的选择题和编程题更看重系统性的知识体系。我个人建议的复习顺序是先把C语法基础过一遍变量、指针、引用、数组、字符串、结构体、函数然后是面向对象类、继承、多态、虚函数表接着是C11/14新特性智能指针、移动语义、lambda、constexpr再然后是STL的底层原理vector扩容机制、map和unordered_map的区别、sort的实现思路最后才是去刷题。操作系统、网络、数据库这些虽然不是C语言本身但在笔试中的占比几乎和语言基础平起平坐。进程和线程的区别、虚拟内存、堆和栈的区别、TCP四次挥手的过程、HTTP和HTTPS的区别这些常见问题不提前整理好到考场上很容易大脑空白。我自己在准备这类笔试时每做完一套模拟题就会做一次“错题回看”不是单纯把答案背下来而是把每道题涉及的知识点回溯到书本或文档里的原始定义再用自己的话重新解释一遍。这个习惯帮我避开了很多“感觉会做但换个问法就懵”的情况。如果你能把这篇文章里提到的那些考点都做到这个程度那么无论考场上遇到的是变化题还是原题心里都会稳很多。
返回列表