爱奇艺2018秋季校招的C++工程师笔试,我印象里第三场是十月中旬的那批。当时在线笔试系统用的还是牛客网那套,一共三道编程题加一堆选择填空,时间卡得比较紧。现在回头看,那场笔试题的风格很有代表性:不堆偏题怪题,但非常看重基本功的扎实程度,尤其是对C++内存模型和STL底层行为的理解。我身边几个一起参加的朋友,有人刷了大量LeetCode却在选择题上翻车,也有人编程题AC了但基础题答得稀烂最后没进面试。这篇文章我想还原一下第三场笔试的整体情况,把考点、思路和我在实战中踩过的坑都整理出来,给正在准备C/C++方向校招的同学做个参考。
1. 整体考情复盘与命题思路拆解
1.1 第三场笔试试卷结构与时间分配
爱奇艺的校招笔试和绝大多数互联网公司一样,用的是在线OJ平台,第三场C++工程师的试卷结构大致如下:单选多选混合的基础知识题差不多20道,占总分四成左右,覆盖C++语法、数据结构、操作系统、网络基础;剩下的全是编程题,通常是三道,难度梯度很明显,第一道是字符串处理或者简单模拟,第二道是排序或链表操作,第三道则是状态搜索或复杂DP。总分100分,编程题权重很大,基本是能不能进面试的分水岭。
当时我拿到试卷后的第一反应是时间不够用,因为选择题里藏了不少需要仔细推敲的C++细节题。比如有一道考的是vector扩容时迭代器失效的问题,有一道考的是虚函数表的内存布局,还有一道考的是std::string的COW(Copy-on-Write)在不同编译器下的表现差异。这些题如果是靠记忆去猜,很容易翻车。我的策略是先把选择题快速过一遍,拿不准的先标记,然后直接去做编程题,因为编程题一题的分值顶得上五六道选择题,性价比更高。编程题全部AC之后,再回头啃那些选择题。
1.2 校招笔试命题的几个潜规则
经历过几场校招笔试后,我发现爱奇艺这类公司出题有几个固定套路。第一,非常喜欢考"看起来简单但边界条件极多"的题,比如字符串逆序这类题,看起来谁都写得出来,但真正能一次通过的并不多,因为空格、标点、多字节字符这些边界情况会被拿来测试。第二,STL容器相关的问题从来都是重点,因为实际工程里C++工程师天天和容器打交道,如果一个候选人连map和unordered_map的区别都说不清楚,面试官会怀疑你能不能胜任业务开发。第三,操作系统和网络相关的题一般不会出得太深,但一定会出,比如进程和线程的区别、TCP三次握手的状态变化、堆和栈的区别,这些是计算机基础的核心,属于必考范畴。
另外一个容易被忽略的点是:笔试时间通常是和同期其他公司冲突的。我记得爱奇艺第三场那天,网易游戏和美团点评的笔试也在同一时间段。所以决定参加哪一场之前,一定先确认好时间。我当时的选择是优先爱奇艺,因为内容偏底层和我平时准备的方向更契合,事实证明这个选择是对的。
2. 编程题核心实战:从思路到AC代码
2.1 字符串逆序输出:最简单的题也最考验细节
第三场第一道编程题是字符串逆序输出,题目描述大概是这样:输入一行字符串,将字符串逆序输出,要求空间复杂度为O(1)。这道题看着简单,实际上一半的人会挂在边界条件上。标准做法是双指针从两端交换字符:
#include <iostream> #include <string> #include <algorithm> int main() { std::string s; std::getline(std::cin, s); // 注意用getline而不是cin,因为字符串里可能有空格 int left = 0, right = (int)s.size() - 1; while (left < right) { std::swap(s[left], s[right]); left++; right--; } std::cout << s << std::endl; return 0; }这里有一个关键的输入坑:题目说"一行字符串",但没明确说包不包含空格。如果你用std::cin >> s读入,遇到空格就断了,导致"hello world"只会逆序输出"world",直接Wrong Answer。正确做法是用std::getline整行读入。我当时第一版代码就栽在这个地方,好在提交前测了一下带空格的用例才改过来。另外,如果题目要求原地逆序且不允许用额外数组,那双指针从头尾向中间逼近是最合适的,时间复杂度O(n),空间复杂度O(1)。
这道题还有一个容易忽略的细节是字符类型。题目只说了字符串,没说一定是ASCII字符。如果输入包含中文等多字节字符,直接用std::swap去交换char就会把UTF-8编码的多个字节拆散,导致乱码。第三场没有考到这个深度,但面试环节可能会追问,提前想清楚更好。
2.2 冒泡排序的优化:从手写排序到理解排序本质
第二道编程题我记得和冒泡排序有关。原题是给一个数组,要求用冒泡排序将其升序排列,并输出每一轮排序后的结果。这题一看就是考基本功,但要求输出每轮结果这一点非常关键。如果你在循环里漏了某一轮,或者边界范围算错了,输出的中间过程对不上,照样不得分。
我当时写的版本:
#include <iostream> #include <vector> void bubbleSort(std::vector<int>& arr) { int n = (int)arr.size(); for (int i = 0; i < n - 1; i++) { bool swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); swapped = true; } } // 输出本轮结果 for (int k = 0; k < n; k++) { if (k) std::cout << " "; std::cout << arr[k]; } std::cout << std::endl; if (!swapped) break; // 提前终止 } }冒泡排序本身没什么难度,但这道题考了一个容易被忽视的点:当数组已经有序时,冒泡排序应该提前终止。加了一个swapped标记之后,时间复杂度最好就是O(n)而不是O(n^2)。在校招笔试里,时间复杂度的分析会体现在后面的大题或者面试追问里。比如你写了冒泡排序,面试官可能会问"它的最好情况时间复杂度是多少?怎么优化?"如果你当时答不出swapped标记这个优化,印象分会打折扣。
另外一个细节是输出格式。题目要求输出的数字之间用空格分隔,末尾换行。这看起来是小事,但真的有人因为多输出了一个空格而Wrong Answer。在线OJ对格式非常敏感,末尾空格、空行都算错误,写代码的时候就要刻意避免。
2.3 链表反转:这道题我推荐用三指针迭代法
第三场最后一道编程题是链表反转。题目给一个单向链表,要求返回反转后的头节点。这道题属于链表操作里最经典的题型,和"字符串逆序"并列,是各大厂校招笔试的常客。难点不在于实现本身,而在于边界情况的处理:空链表、只有一个节点的链表、多个节点的链表,三种情况都要能正确返回。
我推荐用迭代三指针法,因为空间复杂度是O(1),而且不容易弄混指针指向。核心思路是:用prev记录上一个节点,curr记录当前节点,next提前保存下一个节点,防止链断掉。
struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr != nullptr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; }这道题在笔试里占的分值很高,而且还会考察你是否能识别出边界条件。空链表直接返回nullptr,单节点链表实际上循环一次就直接返回了原节点。整个算法的复杂度是O(n)时间、O(1)空间。
面试时经常会有追问:用递归怎么写?如果链表中存在环还能反转吗?递归版本代码量少,但空间复杂度是O(n),在深度较大的链表上可能导致栈溢出。是否存在环这个问题,一般先判断环再决定是否反转,实际工程中一定要考虑。
3. C++语言基础考点盘点:选择题中的陷阱
3.1 指针与内存管理:C/C++工程师必须越过的那道坎
爱奇艺第三场笔试的选择题里,指针和动态内存管理的占比特别高。我印象最深的一道题是:定义一个二维数组int a[3][4],问a+1、&a+1、*(a+1)、*(a+1)+1分别代表什么。这道题看似简单,实际上把数组指针、指针数组、数组名退化这些概念全部考了一遍。
正确答案是:a+1指向第二行的首地址,即a[1]的地址;&a+1指向整个数组之后的位置(移动了3*4个int的大小);*(a+1)等价于a[1],是一个一维数组名,退化为指向其首元素的指针;*(a+1)+1指向a[1][1]。这些都是《C和指针》这本书里的经典内容,没深入理解数组和指针关系的同学很容易在这里翻车。
内存管理另一个必考点是new/delete与malloc/free的区别,以及内存泄漏的产生场景。笔试喜欢给一段代码,里面new了数组但用delete释放,问会发生什么。正确答案是这是未定义行为,可能导致程序崩溃或内存泄漏。正确写法是new[]配delete[],new配delete,malloc配free。我在第三场碰到的版本是用malloc分配内存后用delete释放,这同样是未定义行为,因为delete会调用析构函数而free不会,两个释放函数的管理机制也不同。
关于内存管理,我想多说一句:笔试很多时候不只是考你会不会写代码,还考你有没有工程习惯。比如new之后判断返回指针是否为空,在现在的C++里其实不太必要,因为new失败会抛出std::bad_alloc异常,但在嵌入式环境里,异常机制可能被禁用,这就要靠返回值判断了。我在笔试里遇到的一道题就是关于这个,问的是在没有异常机制的环境里如何安全地分配内存。
3.2 while与do-while的区别:选择题必背但容易混淆
热词里有一个"c语言while和do-while区别",这确实是笔试选择题的常客。核心区别一句话就能说清:while先判断条件后执行循环体,条件为假时循环体一次都不执行;do-while先执行一次循环体再判断条件,至少执行一次。
但笔试不会只问定义,它会给代码片段让你判断输出。比如:
int i = 0; while (i < 0) { i++; } printf("%d\n", i); // 输出0 int j = 0; do { j++; } while (j < 0); printf("%d\n", j); // 输出1这里要特别注意do-while后面的分号不能漏,漏了编译直接报错。实际工程里do-while还有一个非常经典的用法——宏定义多语句安全包裹。比如:
#define DO_SOMETHING(a, b) do { \ int tmp = (a); \ (a) = (b); \ (b) = tmp; \ } while(0)这个宏加上do-while(0)之后,无论外层怎么套if都能安全展开,而不会因为分号匹配问题编译报错或者改变逻辑。这个点在笔试里可能会以"宏定义相关"的形式出现,面试中也常被问到。
3.3 字符串函数的隐秘边界:strcpy、strcat、memcpy的异同
C语言字符串处理函数是选择题的常客,热词里也出现了"c语言字符串函数"、"字符串逆序c语言pta"这些搜索词。第三场笔试中有一道题是问strcpy、strncpy、memcpy三者有什么区别。这题看似简单,但能完全答对的人不多。
strcpy按\0判断拷贝结束,不拷贝\0之后的任何内容,也不指定拷贝长度,存在缓冲区溢出风险。strncpy多了一个长度参数,但它并不会在源字符串长度不足时自动补\0,而且如果拷贝长度超过源字符串长度,它会用\0填充剩余空间,这个行为很多初学者不知道。memcpy按字节数拷贝,不关心字符串语义,适合拷贝任意内存块,但要求dest和src不能重叠,重叠时应该用memmove。
笔试里还喜欢考strlen和sizeof的区别。strlen是运行时计算字符串长度,不包含结尾的\0;sizeof是编译期计算,对数组返回整个数组占用的字节数,对指针返回指针本身的大小(64位系统下为8)。所以对char buf[] = "hello"执行sizeof(buf)返回6,执行strlen(buf)返回5。这个考点在选择题里出现频率非常高,几乎每场笔试都有。
4. 机试实战障碍:开发环境与调试经验复盘
4.1 在线笔试环境下的编译器和代码规范问题
爱奇艺第三场用的在线编程系统,编译器版本是GCC 7.3,默认使用C++14标准。这个信息很关键,因为如果你用了C++17才支持的语法特性,比如std::optional、结构化绑定、if constexpr,编译就会直接报错。我当时在写第二题的时候,顺手写了一个auto [a, b] = std::make_pair(1, 2),编译直接挂了,愣了一下才想起来这是C++17的语法,赶紧改成传统写法。
还有一个细节是scanf_s和scanf的区别。在线OJ系统大多跑在Linux上,使用的是glibc,只支持标准C的scanf,不支持Visual Studio的scanf_s。但许多同学平时在Windows上用VS写代码习惯了,一上来就写scanf_s,在OJ上直接编译失败。我的建议是校招复习阶段就全部用标准C/C++写,不要依赖Windows特定的函数或头文件。
另外,头文件包含顺序和命名空间使用也要注意。虽然这些不影响编译通过,但有些OJ平台会使用-Wall -Werror参数,把警告当作错误处理,比如未使用的变量声明就直接编译失败。我的习惯是写代码时顺手把不用的变量删掉,不放过任何一个警告。
4.2 本地开发环境配置:VSCode跑通C++比你想的更省心
热词里有好几个是关于VSCode配置C/C++环境的,这说明很多同学对本地开发环境搭建不太熟悉。尤其是校招季,大家都在宿舍或者图书馆用笔记本刷题,如果不能快速搭好本地环境,刷题效率会大打折扣。我个人的主力编辑器就是VSCode,配置C/C++环境其实很简单,装两个扩展就行:C/C++(微软官方出的那个)和Code Runner。前者提供语法高亮、代码补全和调试功能,后者可以一键编译运行单个文件。
编译这块我推荐直接用g++命令,而不是依赖IDE的构建系统。写一个小脚本或者直接用终端跑:
g++ -std=c++14 -Wall -O2 main.cpp -o main && ./main参数解释一下:-std=c++14指定C++标准,-Wall开启所有常见警告,-O2开启优化。笔试机试一般不会开-O2,但平时刷题开着有助于暴露潜在问题。我经常用-Wall来抓那些被忽略的类型转换和未初始化变量问题,这些在笔试的隐藏测试用例里很可能就是扣分点。
VSCode里调试配置也不复杂,按F5先选"C++ (GDB/LLDB)",系统会自动生成launch.json和tasks.json,把program改成你的可执行文件路径就行。调试时设置断点、查看变量值,在线下复现问题的时候非常有用。笔试现场虽然不能用这些调试工具,但平时刷题用熟了之后,代码的容错性会明显提升。
4.3 从"C盘爆红"到开发环境清理:一次机房电脑的求生实录
热词里有一堆"c盘满了怎么清理"、"c盘爆红了可以删除哪些文件"、"磨针c盘清理"之类的搜索词。很多人觉得这跟校招笔试没什么关系,但正是在第三场笔试的准备阶段,我借了一台机房电脑来模拟在线笔试环境,结果C盘直接爆红了,系统都提示"磁盘空间不足,无法创建新文件"。当时的应对方案我记了下来,因为这些都是开发过程中非常实用的经验。
机房电脑经常中招的点是用户目录下的AppData\Local\Temp缓存文件,以及C:\Users\Administrator\AppData\Local下各种软件的缓存文件。我当时的处理思路是:先删除Temp目录下能删的文件(删除被占用文件时注意提示),然后是各种软件在AppData里生成的缓存,最后清理系统休眠文件hiberfil.sys。这里要注意:直接删C:\Windows下的系统文件是危险操作,千万不要碰。
如果你遇到的是C盘空间不足导致编译失败,更稳妥的做法是把VSCode的扩展和编译缓存都挪到其他盘,或者用DiskGenius从其他盘扩容。这些操作虽然看起来和笔试无关,但真遇到设备不给力的时候,能帮你节省宝贵的调试时间。我当时清理完之后,本来已经跑不动的npm install和编译任务都能正常完成了。笔试现场最怕的就是环境问题,提前把系统盘空间清理好,等于给自己少埋了一个雷。
5. 经典C++笔试题型拓展与备战复盘
5.1 文件读写操作:笔试和面试都容易忽略的工程基础
热词里出现了"c语言文件读写操作代码",这其实也是校招笔试的重要考点。爱奇艺第三场虽然没直接考文件读写的大题,但选择题里有一道是和文件操作相关的C++代码题,问的是读取文件时如何判断文件是否成功打开。
C语言风格的文件操作核心是FILE*指针加fopen/fclose。正确写法是:
FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { // 处理打开失败的情况 perror("fopen"); return -1; } char buffer[256]; while (fgets(buffer, sizeof(buffer), fp) != NULL) { // 按行处理 } fclose(fp);C++风格则是用ifstream/ofstream:
#include <fstream> #include <iostream> #include <string> int main() { std::ifstream in("data.txt"); if (!in.is_open()) { std::cerr << "Failed to open file" << std::endl; return 1; } std::string line; while (std::getline(in, line)) { // 按行处理 } return 0; }文件操作的坑在于缓冲区和资源释放。fscanf读字符串时忘记指定最大读入长度,会造成缓冲区溢出;fclose忘记调用,系统会在程序结束时回收,但如果你在一个长时间运行的服务里反复打开文件不关闭,文件描述符会耗尽导致后续打开失败。这些都是工程上非常经典的问题,笔试虽然只考一两个代码片段,但背后考察的是工程安全意识。
5.2 Dijkstra算法的笔试姿势:不要只背模板
热词里有"迪杰斯特拉c",这个也是校招笔试的高频考点。爱奇艺第三场第三道编程题其实就和图的最短路径沾边。这里我要分享一下亲身体会:大部分同学都能背出经典Dijkstra算法的模板,但笔试题目里大多数时候不会直接给你一个"标准最短路"让你套,而是把它包装成一个业务问题,比如"从起点到终点的最小花费""服务器之间的最小延迟"等等。识别出这是最短路问题才是解题的关键。
经典Dijkstra实现(优先队列优化)大概是这样的:
#include <vector> #include <queue> #include <limits> using namespace std; void dijkstra(int start, const vector<vector<pair<int, int>>>& graph, vector<int>& dist) { int n = (int)graph.size(); dist.assign(n, numeric_limits<int>::max()); dist[start] = 0; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; pq.push({0, start}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d > dist[u]) continue; for (auto& [v, w] : graph[u]) { if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } }注意里面有一个if (d > dist[u]) continue;的剪枝操作。如果不加这个判断,优先队列里可能积累大量过期的冗余节点,导致复杂度爆炸。这个细节也是面试官很喜欢追问的:为什么Dijkstra算法需要贪心选择当前距离最小的节点?如果图里有负权边会怎样?如果用的是邻接矩阵而不是邻接表会怎样?这些追问的来源都是笔试里那道基础题。
5.3 从笔试到面试:那些"额外"的能力其实更重要
爱奇艺第三场笔试虽然只是一场考试,但它的分数构成和面试评价体系是高度一致的。你会发现,编程题AC得漂亮的人,往往在选择题部分也不会太差,因为两者考察的是同一种能力:对C++语言和数据结构有没有深入的理解,能不能在短时间内写出没有明显bug的代码。
我个人的建议是:算法刷题可以继续,但别盲目。前40道题用分类刷法,搞清楚每一类题的通用解法,比如链表类题基本都是双指针或者哨兵节点,树上题基本都是递归或者栈模拟,区间类题基本就是排序加遍历,图的最短路类题基本都是Dijkstra。40道之后开始刷套题,模拟真实笔试场景,限定时间做完整套试卷,不是为了做对,而是训练自己在时间压力下快速审题、快速定位考点、快速写出第一版可运行的代码。说实话,现在回头看,刷200道题的意义远不如认真复盘20道题来得大。
另外,我强烈建议在正式参加笔试之前,去心仪公司的笔试系统里做一两套模拟题看看环境。很多平台在线编译器的错误提示和本地编译器不一样,比如输入输出格式上多了一个\r都可能导致错误。这件事提前做一次,到了真实笔试的时候心态会稳很多。
6. 写在最后:一次笔试给我的三个收获
我后来顺利拿到了爱奇艺的面试邀请,虽然最终因为个人原因没有接offer,但第三场笔试的备考过程对我的C++功底提升确实是实打实的。我觉得这次经历最大的收获有三个:第一,编程题没有捷径,只有亲手写过一遍,把边界条件都考虑进去,才算真正掌握一道题;第二,C++基础概念必须体系化地去理解,靠死记硬背应付不了那些"看起来简单但细想全是坑"的选择题;第三,环境准备和基本功同样重要,无论是本地开发环境还是C盘空间这种看似不相干的小事,都有可能在关键时刻成为你翻车或者不翻车的分野。
如果你也在准备类似公司的校招笔试,我的经验是:别急着刷难题偏题,先把基础概念吃透。字符串、内存、指针、排序、链表、最短路,这些核心考点全部掌握,笔试的胜算就已经非常大了。剩下的细节,交给时间和题量去磨。祝顺利。