ARTICLE DETAIL

资讯详情

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

爱奇艺2020校招C方向笔试题考点解析与实战技巧

爱奇艺2020校招C方向笔试题考点解析与实战技巧 聊到“爱奇艺2020校招C方向笔试题第二场”很多同学的第一反应是找原题、背答案。这个心情我能理解但方向真不太对。考试前一天背十道题不如花一晚上弄清楚这套题背后的考察逻辑——因为大厂C方向笔试筛选的从来不是“你背过什么”而是“你有没有用C解决过真实问题的底子”。这篇文章不打算逐题念答案而是把这套题拆开揉碎为什么考指针、为什么考字符串、为什么最后还有一道文件IO的场景题以及你在考场上最容易忽略的细节都在哪里。适合正在准备校招笔试的同学也适合那些C语言基础不牢、想系统自查的人。1. 岗位画像决定考点先看懂这份试卷在筛什么人1.1 从试卷结构看广度筛选和深度筛选是两回事爱奇艺这类视频公司的C方向笔试几乎都长一个骨架单选题、多选题、填空题最后跟一两道手写编程题。这个结构本身就有讲究它不是随便拼的。选择题的量一般不小二十道上下覆盖指针、运算符、内存分配、宏定义、结构体、字符串函数。这部分的目标是快速检验“基础广度”。换句话说面试官想把那些只会照着教程抄代码、但一换到语法细节就懵的人筛掉。多选题更狠它考的是“概念辨析”比如“以下哪些写法会导致未定义行为”少选扣分、错选扣分想靠蒙混过去很难。填空题通常给出一段简短的代码让你补出关键语句或者直接问你某行代码的输出结果。这类题考的是“对代码执行过程的精确推演”不是“大概知道怎么回事”。举个例子sizeof和strlen的区别很多人嘴上会说但真拿到一个数组名和一个指针变量让他写出各自结果十个里有四五个会错。这种题就是要把“感觉懂了”的人揪出来。而最后的编程题才是深度筛选。它要看的不是你会不会写循环而是你有没有算法思维、能不能在限定时间内写出健壮可跑的代码、会不会分析复杂度。一场笔试下来广度过关的人可能有一半深度过关的往往剩下不到两成。所以你要明白选择题决定了你能不能进下一轮编程题决定了面试官愿不愿意跟你聊下去。1.2 视频公司C方向的实际业务场景决定考点倾向同样是C方向笔试做嵌入式、做数据库内核、做视频服务考题的出法完全不同。爱奇艺这类公司的C方向岗位围绕的是播放内核、CDN边缘节点、音视频转码服务、基础网络组件这些方向。这类业务的共同点是对性能极其敏感对内存和IO的掌控要求极高而且一旦出问题排查成本很高。这就解释了为什么试卷里指针和内存管理的比例那么大。一个播放器内核要处理的是来自网络的数据块、解码器输出的帧缓冲区、渲染线程共享的共享内存。谁分配、谁释放、怎么避免拷贝、怎么防止野指针这些不是八股文而是每天都要面对的真实问题。所以笔试里的指针题本质上是在提前模拟岗位的日常。另外文件读写和网络字节序这类题也常出现。原因很简单大量视频文件是二进制格式转码服务要反复读写文件头、帧数据网络传输过程里还要做字节序转换。一个人在笔试里连fread的返回值都不检查你很难相信他能在生产环境里写出可靠的文件解析代码。把岗位需求对应到考点上整份试卷的脉络就清楚了。2. 指针与内存管理的连环坑选择题才是真正的主战场2.1 数组退化与sizeof一道题看清C语言函数传参的本质C方向笔试里数组和指针的关系几乎是必考项。很多选择题表面在问sizeof实际上考的是“数组名在函数参数里发生了什么”。这道题我觉得是最典型的一种#include stdio.h void test(char arr[]) { printf(in function: %zu\n, sizeof(arr)); } int main(void) { char buf[64] {0}; printf(in main: %zu\n, sizeof(buf)); test(buf); return 0; }在64位平台上答案是64和8。buf是真正的数组sizeof(buf)返回整个数组占用的字节数但一旦把它传给函数数组名就“退化”成指向首元素的指针sizeof(arr)只能得到指针本身的大小。我见过太多同学在这里栽跟头不是不懂而是做题太快根本没注意到形参写的是数组形式。这个知识点的本质是C语言函数传参永远是值传递数组不可能整体传进去传的只是地址。为了应付这类题你应该形成条件反射——看到sizeof先问自己对象是数组还是指针如果是函数形参不管它声明成arr[]还是*arr一律按指针处理。这个习惯养成之后类似的题再出现就不会错。2.2 未定义行为与运算符优先级遇到就别硬算有一类选择题是专门“钓鱼”的。比如给你这样的代码int i 0; i i i; printf(%d\n, i);然后选项里列了一堆结果1、2、3、4。很多同学真就在草稿纸上吭哧吭哧地推演试图算出一个“合理”的答案。但我告诉你这道题的正确选项应该是“未定义行为”而不是任何一个数字。i和i在同一个表达式里多次修改同一个变量中间没有序列点标准规定这就是未定义行为编译器想给你什么结果都合法。类似的还有逗号表达式和运算符优先级混在一起的题int a 1, b 2; int c (a, b a, a b);这道就不是UB了。逗号表达式从左到右依次求值整个表达式的值是最后一个逗号后面的结果。先算aa变为2再算b ab变为4最后a b得6所以c是6。这类题的考点是运算符优先级和求值顺序。我的经验是考试时遇到和--混在复杂表达式里的题目别试图硬算先判断有没有未定义行为如果有直接选UB选项如果没有再按优先级一步步拆。这个习惯不仅仅是应试技巧。真实工程里代码规范明确规定禁止在表达式中对同一变量多次自增自减就是为了避免这种不可移植的行为。笔试出这种题其实是在暗示你我们团队不欢迎写这种代码的人。2.3 内存分配、释放与对齐从选择题延伸到实战指针和内存的考点除了数组退化和UB还有一块重头戏——动态内存。char *p (char *)malloc(100); free(p); // 此时 p 是野指针free之后有没有把指针置为NULL这个问题在选择题里以各种形式出现有些题干说“free之后再free一次会发生什么”有些说“free之后使用这块内存会发生什么”。正确答案是这些都是未定义行为可能在部分平台上“看起来没事”但绝不能依赖这种运气。实际项目里我见过因为free之后没有置NULL导致同一块内存被释放两次、堆结构被破坏、程序在完全无关的地方崩溃的案例排查起来极其痛苦。再有一个高频考点是结构体对齐#include stdio.h struct A { char a; int b; char c; }; struct B { char a; char c; int b; }; int main(void) { printf(A: %zu\n, sizeof(struct A)); printf(B: %zu\n, sizeof(struct B)); return 0; }默认4字节对齐下第一个输出12第二个输出8。原因很简单结构体成员顺序不同编译器填充的空隙就不同。把小的成员排在一起能省内存。这个知识点看起来是“填空级别”的简单题但它直接关系到网络协议的数据封包、文件格式的解析结构体。如果你写了一个#pragma pack(push, 1)那对齐规则又会变。考试里遇到这类题一定要先看清有没有pack关键字再决定对齐策略别上来就按默认规则答。3. 字符串处理大题边界条件比算法本身更值钱3.1 字符串逆序的标准解与变体双指针为什么是首选编程题里字符串相关题目出现频率极高“字符串逆序输出”更是家常便饭。但很多人以为这题就三行代码的事儿写出来却拿不到满分原因在于边界条件处理得不够干净。我给出一个标准实现void reverse(char *s) { if (!s || *s \0) return; char *left s; char *right s strlen(s) - 1; while (left right) { char tmp *left; *left *right; *right-- tmp; } }这段代码里第一行if (!s || *s \0)就处理了两个边界空指针和空字符串。很多同学不写这个防御直接取strlen(s) - 1遇到空字符串时strlen返回0再减1就成了-1right指向字符串地址之前的位置这已经是越界了。另外一个值得注意的细节是strlen的调用它在O(n)时间内扫一遍字符串所以整体逆序是O(n)。如果你在循环里反复调用strlen复杂度会退化到O(n^2)这种问题面试官一眼就能看出来。笔试里还常在这个基础上加变体比如“按单词逆序给定hello world输出world hello”。解法是两轮逆序先整体逆序变成dlrow olleh再对每个单词单独逆序。这种题目考的是你能不能把一个复杂问题拆成两个简单步骤而不是硬写一个复杂的循环。回答的时候一定要在注释里先写思路再写代码。3.2 手写字符串函数的几个隐藏考点字符串函数的手写题比如手写strcpy、strcat、strcmp、atoi也是笔试的老朋友。先说strcpy经典版本char *my_strcpy(char *dest, const char *src) { if (!dest || !src) return NULL; char *ret dest; while (*dest *src) ; return ret; }这里有几个考点第一返回值必须是目的地址的原始值很多同学没保存dest就直接返回结果返回了尾部的空字符第二src必须用const修饰表达“源字符串不会被修改”的语义第三要注意dest和src不能重叠如果源和目标指向同一块内存且发生重叠需要用memmove而不是strcpy。这些都是选择题的高频干扰项也是实际工程里会踩的坑。再比如手写atoi这个题就更有意思了。要处理的边界条件包括字符串前后的空格、正负号、出现非数字字符时的处理、整数溢出怎么办。我在复习的时候给自己列过一张检查表边界场景处理方式空字符串返回0前导空格跳过正负号记录符号非法字符停止解析并返回已解析部分溢出返回INT_MAX或INT_MIN这张表里的每一条在笔试题里都可能被拆成一个单独的选择题来考。比如“字符串 -123abc用标准atoi解析结果是”答案是-123因为解析到a就停止了。你要是没看过标准库的这个行为很容易想当然地认为返回0。这些细节就是拉开分数差距的地方。3.3 代码补全题的实战策略填空题里有一种常见形式给出一段有残缺的代码让你补上关键行。比如一个字符串去重函数中间留了两行空或者一个计算字符串长度的函数用递归写法让你补递归终止条件。这类题的答题策略我觉得有三条。第一条先通读上下文确认缺失部分的职责。缺的是终止条件、赋值语句还是指针移动从整体逻辑判断不要盯着局部硬想。第二条注意类型匹配。填进去的语句要和前后变量的类型能对上比如该填*s还是s一眼看不出来的时候看它参与运算的类型。第三条用手头例子做快速验证。在草稿纸上用hello走一遍逻辑比自己空想要靠谱得多。这三条用熟了代码补全题基本能稳拿。4. 手写链表与排序算法阅卷人会看的得分点4.1 链表反转的两种写法从考卷到面试都要过关链表题在笔试编程题里的地位跟指针在选择题里的地位差不多——都是必考的。其中“反转链表”出场率最高。我见过太多同学被这道题考倒不是因为他们不会写而是因为思路停留在“画图理解”一到手写就乱。这里给出迭代版本我认为这是考场上最稳妥的写法struct ListNode { int val; struct ListNode *next; }; struct ListNode *reverseList(struct ListNode *head) { struct ListNode *prev NULL; struct ListNode *curr head; while (curr) { struct ListNode *next curr-next; curr-next prev; prev curr; curr next; } return prev; }关键点在于必须先保存curr-next否则一旦把curr-next指向prev原本的下一个节点就找不到了。这个代码我建议每个准备笔试的人都背到肌肉记忆。不过光背还不够你最好也掌握递归版本因为很多面试官会在笔试通过后的技术面里问“能不能用递归实现”想看看你是不是真的理解这个结构。递归版的核心理解方式是假设当前节点后面的链表已经反转好了只需要把当前节点放到最后面。但需要注意递归深度等于链表长度万一链表特别长可能会导致栈溢出这是它的天然劣势。笔试中如果题目没有特别要求我推荐写迭代版效率更高、不容易栈溢出、代码也好理解面试官看着也舒服。4.2 快排与TopK时间复杂度的表达比默写模板更重要排序算法里手写快排是另一个高频编程题因为它的平均复杂度是O(n log n)而且不用额外空间其实是递归栈空间这里不较真。我自己的手写模板是这样的void quick_sort(int *arr, int left, int right) { if (left right) return; int i left, j right, pivot arr[left]; while (i j) { while (i j arr[j] pivot) j--; if (i j) arr[i] arr[j]; while (i j arr[i] pivot) i; if (i j) arr[j--] arr[i]; } arr[i] pivot; quick_sort(arr, left, i - 1); quick_sort(arr, i 1, right); }写这个题的时候阅卷人最看重的是这么几件事递归终止条件写没写、等号怎么处理用和跳过等于基准值的元素可以避免极端情况下的死循环、最后基准值有没有放到正确位置。还有一个必须写清楚的点是复杂度分析——平均O(n log n)最坏O(n^2)当数组已经有序且每次基准值都取最小/最大元素时。考试时如果你在注释里把复杂度写明白这题就稳了一大截。和排序紧密相关的一类场景题是TopK“在海量数据里找最大的K个数”。高效做法是用一个大小为K的小顶堆遍历一遍数据每次遇到比堆顶大的元素就替换并调整堆最后堆里就是最大的K个。复杂度是O(n log K)。笔试里如果遇到这种题你只写“先排序再取前K个”能拿一半分但如果能把堆的思路写出来再补上空间复杂度基本就是满分答案了。4.3 编程题的“隐性格式分”很多人不知道编程题是存在“隐性格式分”的。阅卷人不只看最终正确性也会看你的代码长什么样。几条我认为很重要的细节变量命名要有语义。用i、j做循环变量没问题但链表题里用curr、prev、next比用p、q、r清晰得多。一定要在关键步骤写注释。哪怕只是“// 保存next防止断链”这种说明性注释也能让阅卷人确认你理解这行代码的作用而不是瞎碰出来的。防御性判断要保留。处理空指针、空字符串的代码要写出来即便题目没说需要这也是加分项。写在最后的是复杂度分析一定要写。这个习惯在准备阶段就要养成而不是考场临时发挥。很多同学平时在IDE里写代码靠自动补全缩进还靠格式化工具一到手写就原形毕露。所以我建议你平时练习手写代码的时候就刻意不看IDE提示每道题都从头写到尾模拟考场的真实状态。5. 文件读写与底层IO视频业务场景下的C语言延伸考点5.1 从fopen到mmap文件操作题的规范过程爱奇艺这类视频公司的C方向笔试很喜欢在最后放一道和文件读写相关的综合题。这跟业务强相关视频转码、切片、CDN缓存回源全都涉及大规模文件处理。文件读写的“标准答案”其实有固定套路我以读取整个文件到内存为例FILE *fp fopen(data.bin, rb); if (!fp) { perror(fopen); return -1; } fseek(fp, 0, SEEK_END); long size ftell(fp); rewind(fp); char *buf (char *)malloc(size); if (!buf) { fclose(fp); return -1; } size_t n fread(buf, 1, size, fp); if (n ! (size_t)size) { // 处理短读 } free(buf); fclose(fp);这个看似简单的过程里全是考点。第一fopen之后必须判空很多同学把这个跳过。第二fread的返回值必须检查因为在磁盘IO出错、文件被截断等情况下实际读到的字节数可能小于请求的字节数。第三malloc失败要处理不能直接拿来就用。第四用完的fp和buf要释放。这四条有一条没写阅卷人就会觉得你“没有生产级代码意识”。笔试里还可能考mmap和标准IO的对比。mmap可以把文件直接映射到进程地址空间避免用户态到内核态的数据拷贝读大文件时性能好很多但它的代码写起来更复杂还要处理映射失败、页对齐等问题。我在实际项目中处理几百MB的视频切片文件时会优先考虑mmap但如果笔试题目没有明确要求用标准fopenfread是更安全的回答不容易被挑毛病。5.2 那些不直接考文件、但和IO底层绑定的题文件读写经常连带考另外两类底层知识哪怕题目表面上只是一道“网络编程题”或“数据处理题”。第一类是字节序。视频文件里的很多字段是多字节整数比如一个4字节的uint32数值在存储顺序上有大端和小端之分。C语言笔试里可能会问网络字节序是大端还是小端如何判断当前机器是大端还是小端后者常用的回答是取一个整数、取它首字节地址、判断存储的是高字节还是低字节。这类题考的实际上是“你有没有被跨平台数据交换坑过”。第二类是整型溢出。比如有一个二分查找的经典题很多人写int mid (left right) / 2但left right可能溢出。正确写法是int mid left (right - left) / 2。文件处理场景里也类似当你拼接文件路径、计算偏移量时如果用的是32位整数去存一个大于2GB的偏移量就会得到负数。笔试考这些不是因为面试官想刁难你而是这些坑在视频处理业务里真实发生过无数次。5.3 一个综合场景题的完整作答示范假设笔试最后一道题是“给定一个大文本日志文件每行格式是时间戳 错误码 描述统计每个错误码出现的次数按次数降序输出。”这道题综合了文件读写、字符串解析、排序非常适合用C实现。我的作答思路是这样的#include stdio.h #include stdlib.h #include string.h #define MAX_LINE 1024 #define MAX_ITEMS 10000 typedef struct { int code; int count; } Item; int cmp(const void *a, const void *b) { return ((Item *)b)-count - ((Item *)a)-count; } int main(void) { FILE *fp fopen(app.log, r); if (!fp) { perror(fopen); return -1; } Item items[MAX_ITEMS]; int n 0; char line[MAX_LINE]; while (fgets(line, sizeof(line), fp)) { int code 0; // 解析行例如时间戳空格错误码 if (sscanf(line, %*d %d, code) ! 1) continue; int found -1; for (int i 0; i n; i) { if (items[i].code code) { found i; break; } } if (found 0) { items[found].count; } else if (n MAX_ITEMS) { items[n].code code; items[n].count 1; n; } } fclose(fp); qsort(items, n, sizeof(Item), cmp); for (int i 0; i n; i) { printf(%d %d\n, items[i].code, items[i].count); } return 0; }这份代码里我用fgets按行读取避免了一次性加载整个文件的压力用sscanf解析错误码并且检查了返回值最后用qsort排序。这些问题如果在笔试里让我挑毛病我会说它在线性查找上太慢——如果错误码种类很多可以用哈希表优化。但在笔试时间紧张的情况下先写出一个正确、清晰的O(n*m)版本再在注释里补充“可用哈希表优化到O(n)”是性价比最高的策略比闷头写一个哈希表然后半天调不出来要强得多。6. 复盘几个考场上最容易翻车的瞬间6.1 选择题耗时过多编程题来不及做我见过太多同学在两道有争议的选择题上死磕十分钟最后编程题只剩十来分钟手忙脚乱写不完。这是典型的考场策略失误。选择题再难分值也就一两分编程题一道就可能占二三十分。正确的时间分配我建议是先花两分钟浏览全卷把编程题留出至少一半的时长。遇到拿不准的选择题先标记最后有时间再回头想编程题先写主体框架哪怕不考虑边界情况也比留白强得多。6.2 本地能跑、提交编译失败笔试平台和本地环境的差异是每年校招都有人踩的坑。最常见的问题有三个第一本地用的编译器默认是C但要求提交C代码于是忘了C和C在强制转换、空指针上的细微差异第二本地用了#include bits/stdc.h这种非标准头文件平台编译器直接报错第三变量名用了关键字或者是平台保留字比如你声明一个register变量。我自己的习惯是提交前在脑子里过一遍代码确认所有头文件都是标准C库里的比如stdio.h、stdlib.h、string.h确认没有使用任何平台相关的扩展。6.3 人肉模拟执行画内存图别心算编程题里有一类“写出以下代码运行结果”的输出题很多同学直接在脑子里快速过一遍就写答案错误率很高。我的建议是涉及指针、数组、结构体时一定要在草稿纸上把内存图画出来。每个变量占哪块空间、指针指向哪里、赋值之后指向有没有变化一画就对全靠心算十有八九要错。这跟调试线上问题的思路一模一样——空想不如看现场手绘内存图能帮你还原代码执行的真实状态。还有一个小细节考前务必确认你笔试平台对C语言的标准支持。是用C89还是C99还是C11直接影响你能不能在中途声明变量。有些在线平台默认开C99有些则严格按C89编译循环里声明int i都可能报错。这属于环境问题提前摸清楚能省很多不必要的麻烦。最后分享一点我的个人体会刷了这么多遍C方向笔试题我自己最大的体会是笔试不是期末考它不靠突击靠平时的积累和正确的方法。C语言的好—难—坑其实是一体的——你越熟悉指针越能看懂内存管理越理解内存管理越明白为什么字符串处理要写防御代码。如果你离笔试还有两三个月我建议把《C程序设计语言》里的习题认真过一遍再配合在线题库每天手写两三道算法题保持手感。笔试前一周开始刷往年真题重点不是背答案而是总结考点分布和你的薄弱环节有针对性地补。最后再提醒一句考场上稳最重要会做的题拿满分没把握的题拿步骤分别让任何一道选择题拖垮整张试卷的节奏。
返回列表