尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

爱奇艺C++笔试第三场复盘:考点地图与备考指南

爱奇艺C++笔试第三场复盘:考点地图与备考指南 这个标题我太有感触了。2018年这个时候我也坐在考场里面对爱奇艺这套C试卷。当时只觉得“第三场”只是时间上的区别后来复盘才知道第三场的卷子经常是两个批次里题型最全、考点最典型的一套。更关键的是爱奇艺的C笔试基本代表了流媒体平台后端方向的核心考察逻辑既考语言底蕴又考算法实战还穿插工程细节。今天就把这套题背后的考点地图、原理细节和备考方法完整拆一遍给正在准备C校招的同学一份可以照着做的复盘清单。1. 为什么“第三场”这份卷子值得反复琢磨很多人看到“补录批次”就直接把这套题当成“备胎卷”这是一个很容易踩的误区。爱奇艺的2018秋季校招C工程师笔试分多场进行第三场在时间上确实靠后但考察内容的含金量一点也不低。从职位本身倒推爱奇艺的C岗位集中在播放内核、CDN调度、服务端中间件、推荐和搜索的底层引擎、流媒体传输协议栈这些方向。这类业务有一个共同点对性能极其敏感对资源控制的要求极高运行时的每一毫秒延迟、每一个字节的内存占用都可能影响线上的播放体验指标。所以它的C笔试不会只满足于“你懂语法”这层水平而是要通过一系列题目判断你能不能在生产级的C系统里写出稳定、高效、可维护的代码。第三场相比前几场还有一个特殊性题目在保持主干一致的同时细节层面经常做微调。同样一道概念题前一场考的是结论第三场可能就改成考边界情况前一场直接让你写排序第三场会加一个“要求稳定排序”的前置条件。这其实是很多同学觉得“第三场好像更难”的真实原因——不是难度抬高了多少而是它用更细的题面去筛选那些真正能吃透知识的人。所以我给这套笔试的定位是它不是一套可以草草浏览的旧题而是一张“从C语言到系统落地”的完整考察地图。认真把这张地图啃透基本就能覆盖绝大多数视频平台/中间件类公司C岗位的笔试考点。2. 考点地图从热搜词反推这场笔试的考察半径我先快速建一个“考点-意图”映射表这个表格也可以直接当作后续复习的自查清单。它是我从相关热搜词和历年同类卷子的命题规律里反推出来的不是官方题解但覆盖度很全。热搜/高频词背后考点爱奇艺这类公司考察意图constexpr 版本、C设计模式、回调函数语言特性演进、运行期/编译期语义、代码组织判断你写的是“现代C”还是“C with Class”ABA问题、C多线程并发原语、内存模型、无锁数据结构流媒体后端有大量并发读写线程安全是基本功字符串数组初始化、结构体链表基本语法类型系统、存储类别、指针与数组退化基础不牢的筛选题最容易丢隐含分冒泡排序、选择排序、快速幂、最小公倍数经典算法、算法复杂度、数论考察基础算法基本功和数学转化能力单调栈、消息传递NOIP模拟、指定顺序输出数据结构选型、图论/树形DP、多线程顺序控制拉分题考察建模能力VSCode 配置C/C、CMake 构建、导出so库工程化能力、构建系统、调试能力上机/面试轮会有大量编译调试环节Visual C Redistributable运行时依赖、Windows分发属于分发与部署常识笔试未必直接考但面试可能问从这个表能看出来这套笔试的核心逻辑是三层递进语言层筛掉基础不牢的人算法层筛掉不会工程建模的人工程层筛掉只会写题目不会写系统的人。所以我的建议是不要只刷“爱奇艺2018第三场原题”而是要借这套题的考点结构去建立属于自己的完整知识树。下面重点拆几个我复盘时收获最大的考点。3. C语言核心那些“都会背但一写就错”的题目3.1 编译期与运行期的边界constexpr 的版本演进constexpr 是热搜里的高频词也是笔试常客。它有很强的年代感考察方式通常不是“会不会用”而是“这在哪个C版本引入的、能力边界是什么”。C11 引入了 constexpr。它被用来声明“可以在编译期求值”的函数和变量。但那个版本的约束非常严格函数体往往只能有一条 return 语句循环、局部变量、if 这些通通不支持。C14 大幅放宽了限制允许在 constexpr 函数体内使用局部变量、循环、分支等。大部分“能在编译期算清楚”的逻辑在 C14 之后都可以写。C17 进一步把它扩展到了 lambda 表达式默认 constexpr以及 if constexpr 这个编译期分支机制。C20 又拉开了新一轮扩展虚函数、try/catch、dynamic_cast 在某些条件下也可以 constexpr。笔试爱上“哪个版本引入”的原因很简单这能看出你平时是只看教程的人还是真正在维护多版本项目的人。你在 CMakeLists 里如果写的是CXX_STANDARD 11那你的 constexpr 函数就得按 C11 的规则约束项目里如果有老模块编译不过大概率就是被这些边界卡住了。实操中我建议这样记忆如果你只需要写出一个简单常量表达式C11 够了一旦函数体内有三行以上逻辑你的最低要求是 C14如果团队已经切到 17 或 20就放心把 constexpr 当成“强制内联编译期计算”的加强版普通函数来用。3.2 ABA问题并发题里最隐蔽的坑ABA 问题在笔试题里出现的频率极高尤其是当一个公司招聘的岗位涉及多线程编程的时候。它的本质来自 CASCompare-And-Swap操作CAS 在修改前会比较目标当前值是否等于预期值如果相等就更新。问题是它只能感知“值当前是多少”感知不到“值中间变过几次”。一个经典的 ABA 场景是这样的线程 T1 读取共享变量 A准备做 CAS。线程 T2 抢先把 A 改成 B。线程 T3 又把值改回 A。T1 的 CAS 此时发现值还是 A于是认为“没人动过”提交更新。这种“值没变但状态已变”的错觉在没有锁保护的数据结构里会造成超级隐蔽的逻辑错误。我记得有个非常典型的例子是栈的无锁实现T1 读取栈顶地址后T2/T3 对栈做了一次 pop 再 push栈顶地址又变回原来的值T1 基于旧地址的修改就会覆盖掉中间发生的操作。C 层面我们一般用带版本号的方案解决 ABA。你可以在自己的类里定义一个计数器CAS 的时候同时比较“指针 版本号”这个组合体这样 T2/T3 即使把值改回 A版本号也已经变了。#include atomic #include cstdint struct Node { void* ptr; uint64_t stamp; }; std::atomicNode g_atomic{Node{nullptr, 0}}; void safe_update(void* expected, void* desired) { Node old_val g_atomic.load(); Node new_val{desired, old_val.stamp 1}; // compare_exchange_weak 在 ABA 场景下必须把版本号一起带入比较 while (!g_atomic.compare_exchange_weak(old_val, new_val)) { new_val.ptr desired; new_val.stamp old_val.stamp 1; } }注意old_val必须用循环里的最新值重新读取否则会陷入死循环或者误判。这个细节笔试里没有明说但现场写代码时极容易忽略我当时就在这个点上吃过亏。对于笔试来说关键是能在回答里说出三件事第一ABA 是什么第二它只发生在 CAS 这类“比较-交换”操作里第三主流解法是版本号/标记位。能做到这三点这道题基本满分。3.3 数组、字符串与链表基础题才是失分重灾区热搜里有一串词c字符串数组初始化、c结构体链表基本语法、c字符串转数组。这些词看起来简单但这种题在笔试里恰恰是失分最严重的地方。原因很诡异大家普遍觉得太简单所以不复习。字符串数组初始化最常考的区分是这几种写法char a[] hello; // 可修改的字符数组大小6含\0 const char* p hello; // 指向只读字符串字面量C11后类型是 const char[6] char* q hello; // C11起编译错误字符串字面量不能隐式转为 char* std::string s hello; // 现代C最推荐如果题目问“数组 a 和指针 p 有什么区别”本质在考三点sizeof(a)是6而sizeof(p)是指针大小a 的元素可以通过下标修改而 p 指向的内容不允许a 会在作用域内分配栈内存p 指向的是静态存储区。结构体链表的基本语法则重点考察初始化、内存分配和遍历。笔试里最经典的三个坑是节点声明时忘记struct关键字C语言风格老写法、malloc 之后没检查返回值、删除节点时没有保存 next 指针。#include iostream struct ListNode { int val; ListNode* next; explicit ListNode(int v) : val(v), next(nullptr) {} }; ListNode* reverse(ListNode* head) { ListNode* pre nullptr; ListNode* cur head; while (cur) { ListNode* nxt cur-next; // 先存后继否则改 next 后会丢链 cur-next pre; pre cur; cur nxt; } return pre; }这类题的正确姿势不是背代码而是把“指针指向哪里、地址是否有效、生命周期是否安全”这三件事当成默认检查项每写一步都过一遍。面试官看到你写链表之前先画清楚前后指针关系印象分会高很多。4. 算法与数据结构笔试里真正拉分的部分4.1 排序算法不止背模板稳定性和复杂度同样要会说爱奇艺这类公司笔试里排序题几乎必出但出法往往很恶心不是让你直接做动图演示而是给一个业务场景让你选排序算法。比如“视频播放量排行榜播放量相同的视频希望按ID从小到大排你会选什么排序算法”。这个问题的关键就是“稳定性”。冒泡排序和插入排序是稳定的选择排序不稳定快速排序不稳定归并排序稳定。这里的“稳定”指如果两个元素比较值相等排序后它们的相对位置不变。冒泡排序最容易被忽略的优化是“一趟没有发生交换就提前终止”加上这个优化之后最好情况复杂度能从 O(n²) 降到 O(n)。选择排序虽然时间复杂度稳定在 O(n²)但它的交换次数只有 O(n)这在某些写入代价昂贵的场景反而有优势。笔试如果只让背模板很容易丢掉这个“工程选型”的分。说实话排序最好能手写一遍并在注释里标清楚每一行的用途别再只背sort一行代码。4.2 快速幂与最小公倍数数学题的正确打开方式快速幂是笔试里非常高频的板子题它的本质是二分幂把指数拆成二进制每一位代表要不要乘上当前的底数幂。要求a^b mod p经典写法如下long long quick_pow(long long a, long long b, long long p) { long long res 1 % p; while (b) { if (b 1) res res * a % p; a a * a % p; b 1; } return res; }几个容易被问到的细节res 1 % p是为了处理 p1 的场景否则 b0 时会错误返回1a a * a % p的取模不能省否则 a 很容易溢出b 的类型如果可能是超长整数要改成先取模再循环。最小公倍数则是“n个整数的最小公倍数”的经典考法。基本结论是lcm(a, b) a / gcd(a, b) * b注意这里要先除后乘否则 a * b 可能溢出。多个数的最小公倍数两两合并即可。int gcd(int a, int b) { return b 0 ? a : gcd(b, a % b); } long long lcm(long long a, long long b) { return a / gcd(a, b) * b; }如果题目让你求 n 个整数的最小公倍数不要天真地每两个直接乘而是用一个变量累积 lcm每一步都保持不溢出。这类题在笔试里是稳拿分题但很多人因为“一看就会”就没有实际手写结果在取模和溢出细节上翻了车。4.3 单调栈与消息传递从题目场景反推数据结构选型单调栈是让很多人头疼的考点。它本身并不难核心特征特别明显找“下一个更大元素”“上一个更小元素”“最大矩形面积”的时候基本都能用单调栈把 O(n²) 暴力降到 O(n)。单调栈之所以叫“单调”是因为你维护了一个栈内元素单调递增或递减的结构。我自己的理解方式是栈里存的是“刚才还没找到答案的备选元素”一旦新元素比栈顶更优栈顶就可以出栈并结算答案。vectorint nextGreater(vectorint nums) { int n nums.size(); vectorint ans(n, -1); stackint st; // 存下标从栈底到栈顶递减 for (int i 0; i n; i) { while (!st.empty() nums[st.top()] nums[i]) { ans[st.top()] nums[i]; st.pop(); } st.push(i); } return ans; }这段代码最需要理解的一行是while (!st.empty() nums[st.top()] nums[i])。它保证当元素被弹出时新的元素就是它右边第一个比它大的值。消息传递(news)这道题虽然是 NOIP 模拟题但它的数据结构选型思路完全适用于校招笔试。一个典型的“消息传递”问题会涉及“哪些节点需要经过谁转发消息”的依赖建模这类题第一步永远是判断数据范围再看用 Dijkstra、树形 DP 还是并查集。校招笔试题给的 n 通常是一万到十万级别O(n²) 必挂必须能从题目场景中提取出“求最短路径/求父子依赖/求连通分量”这三类模型之一。4.4 用 C 小游戏练代码手感热搜里挂着c小游戏、c好玩的代码、c爱心代码这个思路其实非常适合备考。笔试一天不写就会手生但天天刷题又太干。做一个简单的控制台小游戏比如猜数字、贪吃蛇、推箱子是特别好用的手感保持方式。理由有三点第一小游戏天然包含输入输出、循环、状态转移、数组/链表操作正好覆盖笔试基础语法第二它需要你调试调着调着就把断点、单步、观察变量这些都练熟了这对上机考试帮助极大第三面试时如果你能拿出一个自己实现的 C 小游戏项目这比自我介绍里写“熟悉C”有说服力得多。我当时为了备考写了一个两百行左右的贪吃蛇用链表存蛇身每吃一个食物就在头部插入新节点尾部删一个节点这本质上就是链表的头插法加尾删法。笔试真遇到链表题时我脑子里直接就是那段代码的运行画面。5. 工程能力题编译、调试与运行效率的隐藏分5.1 VSCode 配置C/C环境高频坑位逐个看很多人笔试前临时用 VSCode 配环境结果卡在 tasks.json 和 launch.json 上。这类问题在笔试中不会直接考但如果你要复现算法题、做本地调试环境配不好会浪费大量时间。VSCode 写 C 最少需要三个文件tasks.json负责编译launch.json负责调试c_cpp_properties.json负责 IntelliSense代码提示和纠错。最常踩的坑是 IntelliSense 的配置和实际编译器不一致。比如你 tasks.json 里用的是 MinGW 的 g但是c_cpp_properties.json里 includePath 指向了 MSVC 的 Windows SDK结果代码上方飘着一堆红色波浪线实际上编译却没问题。这个问题的解法很简单检查compilerPath是否和tasks.json里的编译器一致并让intelliSenseMode匹配当前编译器类型。{ configurations: [ { name: Win64, includePath: [${workspaceFolder}/**], compilerPath: C:/msys64/mingw64/bin/g.exe, cStandard: c17, cppStandard: c17, intelliSenseMode: windows-gcc-x64 } ], version: 4 }另一个常见坑是调试时点了“启动调试”但没先编译。解决方案是在launch.json的preLaunchTask字段里填上 tasks.json 里编译任务的 label让 VSCode 在启动调试前自动先构建。5.2 C/C 构建与运行时从 CMake 说到 Visual C Redistributable笔试很少直接考 CMake但如果你顺利进入面试面试官很可能会打开你的项目问“这个 so 库是怎么导出来的”。热搜里的c/c 构建、cursor 新建c项目怎么配置、c编译so导出库都指向同一个点代码能不能从单个 .cpp 变成可分发、可链接的产物。最基本的 CMakeLists.txt 如下cmake_minimum_required(VERSION 3.16) project(demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(mylib SHARED src/mylib.cpp) target_include_directories(mylib PUBLIC include) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE mylib)这里值得记住的知识点是add_library(mylib SHARED ...)代表生成动态库。如果你要在 Windows 上导出 C 类给 C 代码或者其他语言调用记得在导出头文件里加extern C以及导出宏否则会面临名字修饰name mangling的问题。#ifdef _WIN32 #define DLL_EXPORT __declspec(dllexport) #else #define DLL_EXPORT __attribute__((visibility(default))) #endif extern C { DLL_EXPORT int add(int a, int b) { return a b; } }Visual C Redistributable是 Windows 上跑 C/C 程序的常见依赖。很多用户在自己电脑上编译一切正常换一台机器就提示“找不到 VCRUNTIME140.dll”或者直接报0xc000007b就是因为目标机器缺少对应版本的 VC 运行库。C 标准库的实现细节比如std::string在 MSVC 下的小字符串优化实现也常是面试追问的方向不过笔试阶段更多是对“是否知道自己写的东西需要什么运行依赖”的隐式考察。5.3 减少运行时间从输入优化到常数优化热搜里有一条很直白的话c怎么只能加代码的情况下减少运行时间。这其实问的是“在不允许更换算法/换语言的前提下怎么通过代码层面的优化压时间”笔试里非常实用。第一级优化是输入输出。cin/cout默认会和 C 标准 I/O 同步这导致它很慢。你可以在 main 函数开头加两行std::ios::sync_with_stdio(false); std::cin.tie(nullptr);这两行能快不少但要明确一个代价用了之后不能混用cin和scanf否则顺序会乱。如果题目数据量极大比如超过一百万行输入更稳妥的写法是自己实现快速读入inline int read() { int x 0, f 1; char c getchar(); while (c 0 || c 9) { if (c -) f -1; c getchar(); } while (c 0 c 9) { x x * 10 (c - 0); c getchar(); } return x * f; }第二级优化是容器选择。能用std::vector不要用std::list能用unordered_map做查询时注意自定义哈希以减少冲突攻击能用std::string_view传参就避免字符串拷贝。第三级优化是拷贝。写函数参数时用const T而不是T避免大对象的无意义拷贝。C11 之后移动语义已经让返回临时对象变得不那么昂贵但传参的拷贝还是实打实的开销。这些优化在笔试里未必能立竿见影地让一道 O(n²) 题过掉但如果一个算法题你压线超时这些常数优化往往就是“卡线下和上线”的区别。很多人刷题时只重复杂度、不重常数这是笔试经常爆冷的一个隐藏原因。5.4 回调函数把函数当参数的工程思路回调函数是热搜中的高频词在爱奇艺这类以事件驱动为主的业务场景里尤其重要。播放器要等视频信息加载完才能渲染网络请求完成后要异步更新 UI这些本质上都是回调。C 里回调的实现方式按年代演进C 风格函数指针C98/03函数指针 仿函数functorC11std::function lambda 表达式笔试中更常考的是 lambda 的捕获列表。一个常见题目是让你输出 1 到 10 之间的偶数用 lambda 方式实现类似回调的能力#include iostream #include functional void traverse(int n, const std::functionvoid(int) cb) { for (int i 1; i n; i) { cb(i); } } int main() { traverse(10, [](int x) { if (x % 2 0) std::cout x ; }); return 0; }这里的关键点是std::function可以接收 lambda、函数指针、仿函数但会引入一点点额外开销如果你只接收 lambda其实可以改成模板参数减轻实例化压力。笔试一般不深究面试可能会追问。另外回调的闭包capture是另一个常考知识点。[]表示按值捕获所有外部变量[]表示按引用捕获。按值捕获的变量在 lambda 内部是 const 的想修改需要加mutable。这些都容易在写算法题时被忽略。5.5 指定顺序输出多线程编程的经典入门题“指定顺序输出”是一个很经典的多线程场景启动三个线程分别输出 A、B、C要求最终输出的顺序是 A - B - C。它看起来简单实则考察的是线程同步的三个基本工具mutex、condition_variable、atomic_flag。最常用的写法是 mutex 加 condition_variable#include iostream #include thread #include mutex #include condition_variable std::mutex mtx; std::condition_variable cv; int turn 0; void print_char(int id, char ch) { std::unique_lockstd::mutex lock(mtx); while (turn ! id) { cv.wait(lock); } std::cout ch; turn (turn 1) % 3; cv.notify_all(); } int main() { std::thread t1(print_char, 0, A); std::thread t2(print_char, 1, B); std::thread t3(print_char, 2, C); t1.join(); t2.join(); t3.join(); return 0; }这里有个笔试高频追问为什么用while (turn ! id)而不是if (turn ! id)答案是因为 notify_all 可能发出“虚假唤醒”spurious wakeup被唤醒的线程应该重新检查条件是否真的满足。用 if 的话一旦被虚假唤醒就可能跳过条件判断直接输出错误的字符。这个细节是很多候选人翻车的点。如果你的代码用了atomic_flag自旋等待要注意它可能会导致忙等待占满 CPU 资源笔试中一般优先推荐条件变量方案。在考场上遇到这类题最稳的做法是先说明你会用 condition_variable 做同步再说用 while 而不是 if 的原因最后补充虚假唤醒这个概念。这三点一出基本就是满分答案。6. 备考节奏与考场策略从这套笔试里提炼的实战建议6.1 三个月冲刺期的阶段划分爱奇艺2018这套题覆盖的知识面太宽如果你现在还比较茫然建议把备考周期分成三个阶段。第一个月是“语言地基期”重点解决三个问题一是把 C11/14/17 的核心特性全部过一遍包括智能指针、移动语义、lambda、右值引用二是手写常见容器比如 vector、list 的简单实现理解扩容机制和迭代器失效三是用 C 把经典排序手写一遍标注复杂度、稳定性、适用场景。第二个月是“算法强化期”按专题刷题。数据结构这边优先把单调栈、单调队列、Trie、并查集弄明白算法这边二分、双指针、回溯、动态规划、图论最短路都要有专题训练。这个阶段的节奏是每天至少两道编程题题目做完之后必须写复盘笔记把时间复杂度和空间复杂度写在代码后面。第三个月是“真题模拟期”严格限时做整套试卷。这时不是做一题查一题答案而是按考试规格来90分钟快速过选择题填空题编程题留足时间。做完不是对完答案就结束而是要把每一道错题的知识点整理进错题本并找到同类题再练。6.2 一套题的三刷法很多同学拿着一套题做完一遍就丢到一边这非常可惜。我的习惯是“三刷”。第一刷在备考前期不限时把每道题涉及的考点标注出来不会的题允许查资料但这个阶段的目的是摸清题目范围。第二刷在备考中期开始限时按考场要求做同时对每一道选择题都要求能解释“为什么选这个不选那个”。第三刷放在考前一周只看错题和薄弱点用一刷时标注的考点清单快速扫一遍确保没有盲区。这样一套题等于被吃透了三次。比起刷十套题但每套只做一遍三刷的收益在笔试考场上会体现得非常明显因为很多考点在一套题里是重复出现的第一遍理解、第二遍熟练、第三遍形成肌肉记忆。6.3 考场上的时间分配与心态校准笔试的时间总量有限合理分配特别重要。以爱奇艺这类校招笔试为例通常选择题/填空题和编程题各占一块。我自己的策略是这样的先把“判断题”“概念题”控制在十分钟以内因为这些分是白送的不会就直接过不要恋战。试卷如果包含多道编程题先花五分钟扫一遍题目优先做“思路最清晰”那道把稳拿的分数落袋再回头啃难题。单道编程题如果想了二十分钟还没有明确思路先停在草稿上写个暴力解法把部分分拿到手然后有时间再优化。另外特别提醒一点审题时要把“数据范围”当成题面的一部分来读。看到n 10^9说明这道题基本不可能用 O(n) 遍历看到n 1000可能就是 O(n²) 甚至 O(n³) 的动态规划。数据范围是出题人留给你的提示不是随便写的。这里也顺带说下遇到填空题让你写某个 STL 容器的时间复杂度时不要只写“O(1)”而要说清楚是“平均O(1)最坏可能退化为O(n)”并给出触发最坏场景的条件。这种在答案里多写半句的习惯经常能帮你从其他同水平候选人里区分出来。结束语写给自己也写给正在刷题的你们复盘完这套爱奇艺2018秋季校招C工程师第三场的笔试图谱我最想强调的其实不是具体的某道题而是做题时形成的那种“知识怎么落到代码上”的敏感性。准备久了你会发现面试官和出题人真正想筛选的不是“谁背的多”而是“谁在限时、高压环境下依然能写出又快又稳的代码”。笔试只是一个单向门过了它还会面对面试轮里更细的追问、更贴近业务的系统设计。把这些基础问题啃透不仅是过这一关更是为后面整个职业生涯铺路。最后送大家一个小建议手边永远放一份纸质的 C 常见考点清单考前看一遍比临时翻文档有用十倍。
返回列表