
2018年欢聚时代的校招笔试B卷在当年算是C/C方向非常有代表性的一套题。这套题给我的第一感觉是不装、不炫技但想拿高分也不容易。它考察的范围很集中——指针、内存、字符串、排序、基础算法恰好都是毕业后真正写工程代码时最常碰上的东西。所以直到现在我偶尔还会把这几道题翻出来用来给准备校招的同学做模拟练习。今天就来把这份B卷的考点、答题思路、容易踩的坑一次性说清楚。顺便提一句我这篇文章相关的热搜词列表里混进了一些奇怪的东西比如“C盘满了怎么清理”和“npm无法加载文件”这些其实是环境问题不是笔试问题。不过如果你是应届生我建议先把开发环境比如VSCode配C/C插件、gcc编译器搞定再刷题环境都跑不通笔试再会写也没用。当年我身边就有同学在笔试现场才发现自己的代码连编译都过不了非常可惜。那么下面进入正题。1. 当年这套题到底在考什么整体思路拆解1.1 B卷的题型分布与分值占比先把我根据当年参加过的同学回忆以及在社区里流传的题目版本整理出来的整体结构放在前面。笔试时长大概90分钟题量不算大但每一道题都有足够的深度想蒙混过关很难。题型题量分值占比考察方向选择题15题左右30%C基础语法、指针、内存布局、运算符优先级填空题5题左右15%输出结果、sizeof运算、字符串处理结果简答题2题左右10%概念对比如while/do-while、动态内存管理编程题3题左右45%字符串处理、排序、链表或图算法选择题和填空题占了将近一半的分值这部分主要看平时积累临时抱佛脚很难速成。编程题是真正的分水岭前面基础题大家都能拿分编程题一拉开就是几十分的差距。我记得当时B卷的编程题里有一道字符串逆序、一道冒泡排序的变体、还有一道最短路径相关的题这三道题几乎决定了你能不能进下一轮面试。1.2 为什么把考察重心放在C语言基础上欢聚时代是做音视频、直播后台、客户端起家的这类业务对性能和内存管理的要求非常高。客户端底层要跟采集、编码、渲染打交道服务端要处理高并发网络请求这些场景都和C/C的血缘关系最近。所以笔试不考花哨的框架、不考最新的语言特性而是盯着C语言的基础打目的就是筛选出真正理解计算机底层运行逻辑的人。这套题的设计思路其实很清晰先确认你会不会写C再确认你懂不懂内存最后确认你有没有算法思维。很多人一上来就刷一堆LeetCode难题觉得算法题搞定就能进大厂结果栽在最基础的指针题上这是我见过最多的遗憾。1.3 B卷与A卷有什么不同同一年的笔试通常会分A、B卷防止前后场次的考生泄题。按我当时收集到的信息A卷偏后端通用方向算法题侧重动态规划和栈队列B卷则更偏向C/C底层方向字符串和图的题更多。如果投的是客户端、基础架构、音视频引擎这类岗位拿到B卷的概率会更大。这里提醒一句我这篇写的是根据多方信息还原出来的样卷考点不是官方原题。官方每年都会调整题目但你把它当成一份“考点体检单”来用效果是一样的——哪一块知识卡住了就说明你该补哪一块。2. while与do-while、字符串逆序基础题里的细节陷阱2.1 while 和 do-while 到底有什么不同这道题几乎是B卷简答题的常客看着简单但答不到点子上的人很多。简单说while是先判断后执行循环体可能一次都不执行do-while是先执行后判断循环体至少执行一次。代码上看就两行的差别但理解不到位写出来的程序行为可能完全不一样。int a 10; while (a 20) { a--; } printf(%d\n, a); // 输出10while循环体一次都没执行 int b 10; do { b--; } while (b 20); printf(%d\n, b); // 输出9do-while循环体至少执行了一次笔试里常见的变形是问“哪种循环适合做菜单选择”答案通常是do-while因为菜单至少要让用户选一次。还有一个容易被忽略的用法do-while在宏定义里可以安全包裹多行语句写成#define LOOP_ONCE(expr) do { expr; } while (0)这样能避免宏展开后if/else配对出错的问题。如果你能在简答题里写出这个例子面试官对你的印象分会明显提高。2.2 字符串逆序输出的三种常用写法字符串逆序是B卷编程题里出现频率最高的一道没有之一。它考的不只是你会不会写而是你能不能写出不崩、不泄漏、边界正确的代码。我见过很多人一上来的写法是新建一个等长数组从后往前拷贝这当然没错但如果题目要求“原地逆序”就必须用双指针。#include stdio.h #include string.h void reverse_str(char *s) { if (s NULL) return; int left 0; int right strlen(s) - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } } int main() { char str[] hello; reverse_str(str); printf(%s\n, str); // 输出 olleh return 0; }这里有个细节很多人会掉坑如果直接写char *str hello那这个字符串是存放在只读常量区的一旦调用reverse_str修改它程序直接崩溃。正确的写法是用字符数组char str[]。这个点B卷的选择题里也考过问的就是“以下哪个字符串能被安全修改”。我当时看到题还觉得太基础后来监考时发现真有不少人栽在这上面。除了双指针递归也可以实现逆序输出但递归版本会改变遍历顺序、且每次递归消耗栈空间如果字符串很长还可能栈溢出。笔试中我建议优先写双指针简单、高效、不容易出错。2.3 冒泡排序的“隐藏加分项”冒泡排序在B卷里通常不是单独考而是和“逆序输出”“数组去重”这类题混在一起让你在某个环节里用到排序。但有一年笔试单独出了一道“手写冒泡排序并说明如何优化”这就不是背代码能搞定的了。基础版很简单两层循环相邻元素比较交换。但真正加分的写法是加一个swapped标志位如果内层循环一整轮都没有发生交换说明数组已经有序直接跳出外层循环。这在近乎有序的数组上能把时间复杂度从O(n^2)降到接近O(n)。void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped 1; } } if (!swapped) { break; } } }还有一个优化是记录最后一次交换的位置下一轮外层循环只需要排到这个位置为止。这个技巧考场里能写出来的人很少但一旦写出来面试官基本都会多看两眼。笔试不是只要求“能跑”而是要求“跑得聪明”。3. 指针、内存和字符串函数C语言笔试题的重头戏3.1 指针与数组传参的经典坑B卷选择题里有一类题很经典给一个数组调用函数后问你sizeof的结果是多少。很多人记过结论——数组作为函数参数会退化成指针sizeof(arr)在函数内部拿不到数组长度。但考场上换成“二维数组”“指针数组”变体时还是会有一批人掉坑。void test(int arr[]) { printf(%zu\n, sizeof(arr)); // 64位系统上是8不是40 } int main() { int a[10] {0}; printf(%zu\n, sizeof(a)); // 输出40 test(a); // 输出8指针大小 return 0; }关于二维数组笔试常考的是函数参数怎么声明。如果函数要接收int a[3][4]参数必须带着第二维的长度比如void func(int a[][4], int rows)。只传第一维指针是编译不过的因为编译器要拿第二维长度去计算行偏移。另外还有一个高频知识点二级指针和指针数组的关系。很多人分不清char *argv[]和char **argv在参数形式上是等价的但在局部变量声明里它们并不完全一样。char *arr[10]是10个指针组成的数组char (*arr)[10]是指向“包含10个char的数组”的指针。笔试里这两个声明放在一起让你判断类型能全对的人其实不到一半。3.2 栈、堆、全局区内存管理选择题考点C语言的内存分区几乎是每年必考。B卷有一道选择题大概意思是给出一段代码问变量分别存在哪个区。要答对这道题脑子里得先有一张完整的内存布局图。内存区域存放内容特点栈区局部变量、函数参数、返回地址自动分配回收空间有限堆区malloc/new 动态分配的内存手动管理空间大但易泄漏全局/静态区全局变量、static变量程序启动时分配结束才释放常量区字符串字面量、const修饰的全局数据只读修改会崩溃代码段编译后的机器指令只读选择题通常会给一个包含全局变量、static局部变量、局部变量、malloc分配的内存的程序让你按存储区域分类。这类题的坑点在于局部变量哪怕被static修饰生命周期变了存储区域也从栈变成了全局/静态区但它的作用域还是局部的。还有一个和malloc配套的考点是内存泄漏。B卷简答题曾经问过“动态分配的内存是否一定要释放如果不释放会发生什么”。答案不是简单的“一定会泄漏”如果程序本身就濒临退出操作系统会回收进程全部内存但如果在循环里反复malloc而不free内存会持续增长直到程序崩溃这在高并发服务端是不可接受的。所以笔试答题时最好分场景讨论面试官喜欢看到这种严谨度。3.3 字符串函数笔试最喜欢让你“手写”字符串函数这块B卷不考背诵而是让你直接实现。最常见的几道题包括手写strlen、strcpy、strcmp、strcat。这四道题代码量都不大但可以考出一个人有没有真正理解指针和边界处理。size_t my_strlen(const char *s) { const char *p s; while (*p ! \0) { p; } return (size_t)(p - s); } char *my_strcpy(char *dest, const char *src) { if (dest NULL || src NULL) return NULL; char *ret dest; while ((*dest *src) ! \0) { ; } return ret; }笔试中容易丢分的点有几个一是没有判断src和dest是否为NULL直接解引用崩溃二是strcpy返回值应该是dest的原始地址而不是最后一个字符的地址因为要实现链式调用三是如果题目要求“考虑内存重叠”那么strcpy是不安全的需要调用memmove而不是memcpy因为两个地址区间有重叠时memcpy属于未定义行为。手写strcmp时要注意返回值的约定大于0、小于0、等于0分别表示第一个字符串大于、小于、等于第二个字符串。很多人只记得返回0表示相等却忽略了正负号代表的大小关系这是填空题里很阴险的考点。4. 文件读写与边界处理工程能力的第一道门槛4.1 标准文件读写流程与完整示例文件读写这块在B卷里不一定作为独立编程题出现但面试追问环节经常被拿来扩展。比如编程题要求你“从输入文件中读取一段文本逆序后写到另一个文件”这就把文件操作和字符串处理结合起来了。规范的C语言文件操作流程是固定的四步fopen打开文件、判断是否打开成功、执行读写操作、fclose关闭文件。少一步程序都可能出问题。#include stdio.h #include string.h #define MAX_LINE 1024 int main() { FILE *in fopen(input.txt, r); if (in NULL) { perror(open input.txt failed); return 1; } char buffer[MAX_LINE]; FILE *out fopen(output.txt, w); if (out NULL) { perror(open output.txt failed); fclose(in); return 1; } while (fgets(buffer, sizeof(buffer), in) ! NULL) { // 去掉换行符再逆序 buffer[strcspn(buffer, \n)] \0; int len strlen(buffer); for (int i 0, j len - 1; i j; i, j--) { char tmp buffer[i]; buffer[i] buffer[j]; buffer[j] tmp; } fprintf(out, %s\n, buffer); } fclose(in); fclose(out); return 0; }代码里有一个细节值得单独拿出来说fgets会把换行符一起读进缓冲区如果不处理逆序后换行符会跑到行首导致输出格式错乱。我用strcspn(buffer, \n)找到换行符的位置并替换成\0这是处理fgets残留换行的最常用写法笔试和实际开发都适用。4.2 文件读写中容易翻车的细节文件操作里最容易翻车的几个点我按踩坑概率从高到低排一下。第一个是打开文件后没有判断返回值就继续操作。文件可能不存在、权限不足、磁盘满这些都会导致fopen返回NULL。如果不判断下一步fread或fgets就会操作空指针程序直接崩溃。笔试阅卷时看到你写了判断印象分会好很多。第二个是二进制文件和文本文件的模式区别。Windows下fopen(file, r)和fopen(file, rb)行为不同文本模式会把\r\n转换成\n二进制模式不会。如果在Windows上用文本模式读一个二进制文件数据会被悄悄改掉这个坑在跨平台开发时特别恶心。笔试如果考文件操作建议在答案里注明“二进制文件要用rb/wb模式”显得你真有工程经验。第三个是fclose漏写。很多人在笔试现场紧张写完读写逻辑就交卷了忘了关文件。短小程序运行结束系统会回收资源看起来没事但在长驻进程里这就是文件描述符泄漏跑一段时间就会报Too many open files。所以这个习惯一定要在平时就养好。5. 手写迪杰斯特拉算法题的答题节奏与实现细节5.1 算法思路与代码实现B卷编程题里如果出现图算法迪杰斯特拉Dijkstra是最常见的。这道题的复杂度在于它不像字符串逆序那样几行就写完需要完整实现一个单源最短路径算法。先讲思路从起点出发维护两个数组一个记录起点到每个节点的当前最短距离一个记录节点是否已经确定最短路径。每次从未确定集合里挑一个距离最小的节点标记为已确定然后通过这个节点“松弛”它的邻居——也就是看看能否经由当前节点得到更短的距离。重复这个过程直到所有节点都被确定。#include stdio.h #include limits.h #define N 5 void dijkstra(int graph[N][N], int src) { int dist[N]; int visited[N] {0}; for (int i 0; i N; i) { dist[i] INT_MAX; } dist[src] 0; for (int count 0; count N - 1; count) { int u -1; int min INT_MAX; for (int i 0; i N; i) { if (!visited[i] dist[i] min) { min dist[i]; u i; } } if (u -1) break; visited[u] 1; for (int v 0; v N; v) { if (!visited[v] graph[u][v] ! 0 dist[u] ! INT_MAX dist[u] graph[u][v] dist[v]) { dist[v] dist[u] graph[u][v]; } } } for (int i 0; i N; i) { printf(%d - %d : %d\n, src, i, dist[i]); } }这段代码的细节值得慢慢看。首先是graph[u][v] ! 0这个判断我用0表示没有边实际工程中如果边长可能为0就需要换一个专门的状态数组或者把无效边设为-1。其次是dist[u] ! INT_MAX的判断如果不加当起点到某个节点不可达时dist[u] graph[u][v]会整数溢出变成负数导致松弛结果错误这是最容易被忽略但也最容易丢分的地方。时间复杂度方面不加堆优化的版本是O(n^2)适合稠密图如果边数远小于n^2可以用优先队列优化到O((ne)log n)。笔试时如果时间充裕可以在代码后补一句“稀疏图可用堆优化”面试官会看出你对复杂度有意识。5.2 笔试现场算法题怎么答才能拿全分场上的时间分配和答题节奏比代码本身更影响分数。我有几条实际经验适用所有算法编程题。第一先写思路再写代码。笔试纸上或在线编辑器里先写两三行注释说明算法核心和复杂度即使代码没写完阅卷人也知道你懂。很多人上来就埋头敲代码敲一半卡住了整道题交白卷非常可惜。第二考虑边界条件。迪杰斯特拉里要处理起点不可达的情况、空图的情况、只有一个节点的图。字符串题里要处理空字符串、全等字符串、超长字符串。这些边界测试用例不需要写进代码但要在注释里列出或者用几行代码明确处理。第三把代码写得“能编译”。所有的头文件、函数签名、返回值都写完整不要依赖阅卷人帮你脑补缺失的include或分号。我实际批改过笔试代码很多人的解题思路是对的但代码里漏了#include stdio.h、忘了声明返回值类型、函数调用和定义不匹配这类问题让阅卷体验很差分数自然上不去。6. 高频失分点与面试追问实录6.1 阅卷时最可惜的五种丢分我帮朋友公司看过几次校招笔试代码发现大家的失分点高度一致。这里整理成一份速查表考前对着自查一遍能避开绝大部分坑。失分类型具体表现解决建议不写头文件用了printf却漏了#include stdio.h每道题先把include写全不检查返回值fopen/malloc的结果直接使用一律加判断失败要处理忽略空指针对NULL解引用导致崩溃函数入口先判断参数变量命名混乱a1、a2、tmp1满天飞用语义化命名如leftIndex不分析复杂度写完代码没有一句话说明效率在注释里写清时间/空间复杂度还有一类丢分不太容易量化但对面试官的主观影响很大——代码排版。到处乱缩进、花括号风格不统一、长函数没有拆分这些问题不会让程序跑错但会让阅卷人觉得你平时写代码的习惯不好。这就像作文里有一堆涂改痕迹内容再好也拉低印象分。6.2 面试官拿到卷子后爱问的问题笔试结束不等于战斗结束面试官会拿着你的答卷展开追问。这些追问往往比笔试本身更深入更像是在测你是不是真的理解了自己写出来的代码。字符串逆序那道题面试官常见的追问是“如果字符串有中文还能按字节逆序吗”这实际上是在考察字符编码意识。UTF-8编码下汉字占3个字节直接逐字节逆序会把汉字拆碎。要么按完整字符处理要么明确告诉面试官当前函数只处理ASCII码。我见过有人被这一问卡住可见平时写代码时习惯性忽略了多字节字符的问题。冒泡排序的追问方向通常是“这个排序稳定吗为什么”。这个问题需要你能讲清楚值相等的元素在排序过程中不会交换所以相对顺序保持不变因此是稳定排序。顺带再答一下选择排序为什么不稳定会显得知识体系更完整。迪杰斯特拉最大的坑是“它能不能处理负权边”。答案是不能。因为Dijkstra基于贪心思路一旦确定某个节点是最短距离就不会再更新而负权边可能让这个结论失效。如果题目里出现负权但不能有负环应该用Bellman-Ford。面试官问这个问题的潜台词是你写算法时有没有意识到它的适用范围。至于while和do-while面试官不太会追问技术细节更多会问“你平时写项目时用do-while多吗”。这时候可以坦白地说在普通业务逻辑里确实用得少但在宏定义和嵌入式平台的外设轮询场景里非常有用。这样既回答了问题又展示了你对底层场景的熟悉度。还有一点想专门提醒部分面试官会针对代码里的“防御性写法”提问比如“你为什么要判断malloc返回NULL难道还指望程序恢复吗”。这个问题没有标准答案你可以说判断是为了提前报错避免空指针在更远的地方造成难以调试的崩溃。关键是给出逻辑自洽的理由让面试官看到你写每一行代码都有背后的思考。我后来再看这份B卷最大的体会是它考的其实不是“你会不会C语言”而是“你有没有真正用C语言写过东西”。面试官不指望你把每一个知识点背得滚瓜烂熟但希望看到你在遇到问题时能快速定位到指针、内存这些核心概念上。如果你现在准备校招我建议把这份卷子当成一次基础体检哪一块卡住了就回去补哪一块。基础不牢后面写再多项目都容易虚。祝各位笔试顺利。