
“欢聚时代2018校招笔试题C B卷”——如果你正在准备校招看到这个标题大概率会心头一紧。先说结论这套卷子不是什么偏题怪题集反而是很多互联网公司C语言岗位笔试题里非常有代表性的一套。它不考你背了多少API也不考冷门语法整个卷子的核心就三件事指针和内存有没有真正理解、字符串和链表的边界条件能不能写对、以及面对一个实际问题时能不能写出简洁可靠的C代码。我在秋招那会儿刷过不少公司真题欢聚时代的C卷在难度上属于中上但区分度很高——基础扎实的人能50分钟做完基础不牢的人可能连选择题都要纠结半天。这篇博客我会把B卷的常见题型、核心考点、典型题目解法、以及阅卷时的一些隐性要求全部拆开讲尽量让准备校招的同学看完之后心里有底也让大家明白刷题到底在刷什么。1. 试卷整体设计与考点分布解析1.1 欢聚时代C卷的考察逻辑不是刷题是抠基础先说一个很多人对校招笔试的误解以为企鹅、阿里、字节这些大厂笔试考的是算法竞赛题所以一门心思去刷LeetCode的Hard题。但欢聚时代这种以直播、IM、音视频为核心业务的公司对C语言岗位的定位很明确——他们要的不是竞赛选手而是能写底层模块、能处理网络包、能调内存问题的人。所以笔试题的导向非常清楚你的C语言功底到底扎不扎实。B卷整张卷子的难度曲线很有意思。前面10道选择题难度递增前3题是送分题中间4题需要你认真推演最后3题几乎人人都会错后面4道编程题则是“一看就懂、一写就错”的典型。这种设计不是随意为之而是HR和技术面试官配合的结果选择题用来快速筛掉基础不牢的人编程题用来在面试环节做深度追问的引子。很多人笔试被刷不是编程题没写出来而是选择题错得太多。我记得当时一起准备秋招的同学有些人专门去背“C语言面试题500例”结果做这套卷子还是栽了。原因很简单欢聚时代的选题很会抓盲点它不问你“什么是野指针”这种概念题而是给你一段代码让你判断输出所有陷阱都藏在细节里。1.2 B卷考点分布与分值结构先给大家一个整体印象。根据多届校招同学的回溯以及我自己的实际体验B卷的结构大体分成两部分满分100分笔试时间120分钟题型题量分值主要考点单项选择题10题每题3分共30分指针、数组、关键字、内存、运算符、结构体编程题4题每题15~20分共70分字符串处理、链表操作、排序与查找、文件或日志解析选择题覆盖的知识点非常固定几乎每年都围绕这几块转指针运算与数组的关系、sizeof与strlen的区别、static/const/volatile等关键字的作用、结构体对齐、以及宏定义相关的陷阱。编程题则更偏向工程场景字符串逆序、链表反转、冒泡排序优化这类题目反复出现偶尔会有一道“文件内容统计”或“日志解析”类问题考察文件读写和格式化输入输出。这里要特别提醒一句编程题的分值占比非常高70分意味着你如果只有编程题写得漂亮前面选择题错一半也能过笔试。反过来选择题全对但编程题只写了一题大概率也进不了面试。所以策略上应该把复习重心放在编程题上尤其是字符串和链表这两类题几乎占据了编程题的半壁江山。1.3 为什么这样设计直播和IM场景下的C语言要求很多人好奇一家做直播和社交的公司笔试为什么不考音视频编解码不考TCP/IP状态机反而考一堆基础语法题。这个问题的答案其实在你入职之后才会真正理解。直播业务的客户端和服务器端都有大量C/C代码尤其是媒体引擎、消息推送网关、基础组件库这些部分。这些代码的共性是生命周期长、并发压力大、对稳定性和性能要求极高。在这种代码里最致命的问题往往不是架构设计失误而是一个指针用错了、一个缓冲区越界了、一个字符串没有以\0结尾。这些问题的根源都是基础不牢。所以欢聚时代的笔试其实是“以考代筛”它不指望你笔试就写出多牛的架构而是希望通过这些基础题目判断如果让你维护一个运行了十年的老模块你能不能在不懂全貌的情况下把一个小问题改对而不引入新bug。这就是为什么题目看起来“简单”但错误率却居高不下。2. 选择题核心细节拆解常量、指针与内存布局2.1 sizeof与strlen数组退化的经典陷阱B卷选择题里出现频率最高的一个考点是sizeof和strlen的区别。这题几乎是必考的而且年年都有新花样。最常见的一种出法是给你一段代码char *p hello; char arr[] hello; printf(%lu\n, sizeof(p)); // 输出什么 printf(%lu\n, sizeof(arr)); // 输出什么 printf(%lu\n, strlen(p)); // 输出什么如果你觉得sizeof(p)是5那这一题就成功踩坑了。p是字符指针不是数组在64位系统下sizeof(p)是8在32位系统下是4。而arr是字符数组hello实际有6个字节因为末尾还带了一个\0所以sizeof(arr)是6。strlen(p)才是5它只计算到\0为止不包含结束符。这个知识点的进阶版是把数组作为函数参数传递时的大小变化。很多同学在main里算sizeof(arr)是6结果到了函数里再算就变成8了怎么都想不通。原因在于C语言中“数组作为函数参数时会退化为指针”你在函数形参里写int arr[]编译器实际看到的是int *arr。所以永远不要在函数内部用sizeof去算数组长度这是C语言新手最常犯的错误之一。2.2 指针运算与const修饰符的优先级B卷特别喜欢考一类题const和指针结合在一起时到底谁不能变。比如下面这两个声明const char *p; // p指向的内容不可修改p本身可以改 char *const p; // p本身不可修改p指向的内容可以改真正让人头疼的是三个const叠加例如const char * const p这个才算真正彻底的“只读指针”既不能改指向也不能改内容。很多人在这种题上栽跟头是因为记反了const的修饰对象。我的经验是看const离谁近const修饰的就是谁。const在左边修饰的是指针指向的内容const在右边修饰的是指针本身。这类题还有另一种出法就是让你判断哪一行代码编译会报错。给你一段代码在中间穿插几个赋值语句让你选哪一行“不能通过编译”。这种题如果你对const的修饰规则不熟就只能凭感觉蒙。我建议大家专门花半小时把const修饰指针的那四种情况普通指针、指向常量的指针、常量指针、指向常量的常量指针写一遍代码编译器会告诉你答案比死记硬背有效得多。2.3 static、extern与volatile的语义辨析选择题还喜欢考的关键字有三个static、extern、volatile。这三个在C语言里都是“多面手”每个都有至少两种用法把它们组合在一起考很容易形成陷阱。static是最常考的一个。它在函数内部修饰局部变量时表示变量存储在静态区而不是栈区生命周期延续到程序结束但作用域仍然限定在函数内部。它在函数外部修饰全局变量或函数时表示该变量或函数只在当前源文件可见外部文件无法extern引用。我见过一道非常经典的题在一个函数里定义了一个static局部变量然后递归调用这个函数问最终输出多少。如果你理解static变量的初始化只在第一次调用时执行后续调用会保留上一次的值这道题就不难。如果你不理解很容易算出错误的累加结果。volatile这个关键字在笔试题里出现的频率也很高它告诉编译器这个变量可能被外部因素修改所以每次访问都必须从内存读取不能优化到寄存器里。最经典的例子是嵌入式开发中读取硬件寄存器的值以及多线程共享的全局标志位。笔试一般不会让你写出完整场景通常只考“哪个选项说法正确”这类判断题。记住一个核心结论volatile防止的是编译器优化不是线程安全和原子操作没有任何关系。2.4 结构体对齐与内存分区问题结构体对齐这种题在B卷里几乎每隔一年就会出现一次。给你一个结构体定义让你算sizeof很多人算出的结果和自己纸上写的不一样就是因为忽略了对齐规则。struct example { char a; // 偏移量0占用1字节 int b; // 偏移量4占用4字节 char c; // 偏移量8占用1字节 };这个结构体的sizeof是多少很多人认为是91413但实际上在默认4字节对齐的编译环境下结果是12。因为int b必须放在4字节对齐的地址上所以a后面会填充3个字节结构体整体大小又必须是最大对齐数这里是4的整数倍所以末尾还会再填充3个字节。这类题的正确做法是先列出每个成员的对齐系数和偏移量逐步填充而不是简单相加。针对结构体对齐我个人的建议是平时写代码的时候打开编译器的-Wpadding警告选项它会在你定义结构体出现填充空隙时给出提示久而久之你就会有空间感笔试的时候看到结构体就能直觉判断大概占多少字节而不是每次重新推演。内存分区题相对简单一般问你局部变量、全局变量、静态变量、malloc分配的内存、字符串常量分别存储在哪个区域。只需要记住一个图栈区、堆区、全局区静态区、常量区、代码区。这里最容易错的是字符串常量例如charp hello这个hello本身存在常量区指针p存在栈上。如果你尝试修改p的内容程序会崩溃这题在笔试里基本是送分题但真的让很多人栽过跟头。3. 编程题实战从字符串到链表再到排序的完整推演3.1 字符串逆序三个层次的解法与工程思维B卷编程题的第一题大概率是字符串相关。最经典的就是字符串逆序输出但别看题目简单阅卷时区分度非常大。第一层解法调用库函数strrev如果编译器支持一行代码搞定。但笔试最好不要这么干一是它不是标准C库函数二是面试官看不出你的能力。第二层解法创建新数组从尾部往前遍历并赋值然后输出新数组。这个方案能正确工作但在空间复杂度上是O(n)不是最优。第三层解法才是阅卷老师想看到的原地逆序双指针从头尾向中间交换字符。核心代码如下void reverse_string(char *s) { if (s NULL) return; char *left s; char *right s strlen(s) - 1; while (left right) { char tmp *left; *left *right; *right tmp; left; right--; } }这段代码里藏了几个阅卷时的加分点和扣分点。加分点包括入口判断是否为NULL、用strlen算出长度后减1得到最后一个有效字符而不是\0、循环条件是left right而不是left ! right虽然本题中二者等价但前者是更普适的写法。扣分点则是在循环体里忘了对空字符串做特殊处理直接访问s[-1]这是未定义行为遇到空字符串必然会崩。我做这类题的体会是笔试时不要一上来就写代码先在草稿纸上把指针移动的过程画一遍尤其是左指针和右指针相遇的那个瞬间。画一遍之后边界条件就清晰了代码基本一遍过。3.2 单链表反转画图推演与边界条件链表反转是B卷编程题的常客而且几乎是每年必考。这道题考察的不只是代码能力还有对指针操作的清晰度链表反转写不对的人通常问题出在“不知道什么时候该保存哪个节点”。先看迭代版本的标准写法struct ListNode { int val; struct ListNode *next; }; struct ListNode* reverse_list(struct ListNode *head) { struct ListNode *prev NULL; struct ListNode *cur head; while (cur ! NULL) { struct ListNode *next cur-next; // 先保存下一个节点 cur-next prev; // 反转当前节点指向 prev cur; // prev后移 cur next; // cur后移 } return prev; }很多人写的时候会卡在“如果先改了cur-next下一个节点就找不到了”。解决办法就是上面代码里的next临时变量先把cur-next存下来再改指向。这一步想清楚了整个代码就顺了。还有一个细节值得注意返回值。反转之后原来的尾节点变成了新的头节点所以返回的是prev而不是head。我在批改这类题目时经常看到有人返回head这等于白写一整题。另一个常见的错误是没有处理空链表和单节点链表的情况虽然这两种情况下代码能正常运行因为循环直接不执行但如果你在代码里明确写出来会显示你考虑问题更全面。如果想要满分还可以补充一个递归版本struct ListNode* reverse_list_recursive(struct ListNode *head) { if (head NULL || head-next NULL) return head; struct ListNode *new_head reverse_list_recursive(head-next); head-next-next head; head-next NULL; return new_head; }递归版本不需要额外的next临时变量但理解难度更高代码量也更少。笔试时只要写对迭代版本就够了不过面试官基本都会追问一句“换递归怎么写”所以两个版本最好都准备。3.3 冒泡排序的优化与手写细节排序题在B卷里出现的形式一般是“实现冒泡排序并对它做优化”。很多人觉得这道题太简单了直接写一个双重循环就交卷结果分数并不高。原因是冒泡排序有几种优化方式阅卷时看到你没写优化默认你的代码停留在教科书第一版不会给太高分。最基础的冒泡排序代码如下void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { 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; } } } }这个版本的缺点很明显如果某一轮循环中没有发生任何交换说明数组已经有序但程序并不知道还会继续跑完剩下的所有轮次。优化方案是加一个标志位void bubble_sort_optimized(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 0) break; } }这就已经是常见考卷里比较满意的答案了。如果再进一步优化还可以记录每轮最后发生交换的位置下一轮只遍历到那个位置因为后续部分已经有序这样“部分有序”的数组性能会好很多。笔试时写排序题有一点要特别留意函数的参数设计。你在读题时先看它要求是“在原有数组上排序”还是“打印排序结果”如果是前者函数应该接收数组指针和长度如果是后者你也可以在排序后遍历打印。参数设计合理阅卷时好印象分就直接上来了。3.4 文件读取与日志解析容易被忽视的工程题有些批次的B卷会有一道文件处理题题干通常是“读取一个文本文件统计每个单词出现的次数并按频率排序输出”或“从一个日志文件中统计某个字段的出现次数”。这种题不像链表反转那么标准很多人复习时容易跳过但实际上它才是最能体现工程能力的一道题。这类题的核心考点有三个文件打开与关闭、按行或按格式读取、以及数据的组织方式。以单词统计为例合理的数据结构是哈希表但C语言标准库没有哈希表需要自己实现。笔试时间有限很多同学会选择更简单的方案比如定义一个结构体数组每次遇到新单词就追加遇到已有单词就计数加一。struct WordCount { char word[64]; int count; }; void process_line(struct WordCount *table, int *size, char *line) { // 简化处理按空格切分逐词处理 char *token strtok(line, \t\n); while (token ! NULL) { int found 0; for (int i 0; i *size; i) { if (strcmp(table[i].word, token) 0) { table[i].count; found 1; break; } } if (!found) { strncpy(table[*size].word, token, 63); table[*size].count 1; (*size); } token strtok(NULL, \t\n); } }这种方案虽然不高效但在笔试场景里完全够用。阅卷的时候只要你把文件打开、逐行读取、切分单词、统计计数这四条链路走通了分数就不会低。要注意的坑有两个一是文件路径问题在线笔试系统里文件路径通常已经给出不要自己拼一个绝对路径二是忘记fclose虽然程序结束时操作系统会回收资源但在代码里fclose是一个负责任的习惯以及最好判断一下fopen是否成功如果文件不存在就直接返回错误而不是继续往下读导致崩溃。4. 答题策略与时间管理4.1 120分钟的时间分配建议B卷的总时长是120分钟看起来挺充裕实际上很多人到最后20分钟还在写编程题。我的建议是把时间切成三段前25分钟做选择题中间70分钟做前三道编程题最后20分钟留给第四道题和检查。选择题25分钟看起来很短但足够用了。如果你在一道选择题上卡了超过3分钟大概率是你对这个知识点不熟继续纠结也是浪费时间。先标记一个可能的答案跳到下一题等所有题做完还有时间再回头推敲。编程题的分配更关键前三道题每题最多20分钟最后一道工程题放到最后即便没完全写完也要把思路、关键的数据结构和伪代码写出来。阅卷的时候有思路但没写完比完全空白要高很多分。我在学校帮老师批过卷看到白卷我什么信息都得不到但看到关键代码片段即使缺了几行我至少能判断这个人真的会做值得给一半分。4.2 从阅卷视角看什么样的答案能拿高分我自己参与过几轮笔试阅卷工作可以坦率地告诉你没有哪个阅卷人会一行一行地跑你的代码。阅卷流程通常是先看整体代码结构再看关键逻辑点最后扫描边界处理。所以你的代码“看起来”怎么样直接影响初始分档。什么样的答案能拿高分第一变量命名清晰别用a、b、c这种无意义的名字至少用left、right、prev、cur这种一眼能看懂含义的。第二有必要的空行和缩进不要把代码挤成一大坨。第三关键步骤有注释尤其是链表反转里的“先保存下一个节点”和“反转当前节点指向”这两步。第四函数入口处对非法参数做了防御性判断。这几点加在一起即便代码风格不那么高级阅卷人也愿意给你高分。反过来最让人头疼的做法是算法思路是对的但代码写得跟火星文一样变量名叫x、y、z函数没有返回值打印语句里混着一堆调试输出。这种卷子阅卷人要花费额外精力去理解一旦理解出现偏差分档就会往下走。4.3 笔试后的第一轮面试面试官会追问什么笔试不是终点它只是面试的筛选棒。如果你通过了笔试面试官手里会有两份材料一份是你的简历一份是你的笔试答卷。面试官一定会对你笔试里的题目进行追问尤其是编程题。以链表反转为例面试官可能问的问题包括为什么需要next临时变量如果不保存next会怎么样递归版本的空间复杂度是多少如果链表有环会怎样如果你能把这些延伸问题答上来说明你是真的理解了链表操作而不是背了一段代码。还有一个经常被追问的点是“你的代码有没有内存泄漏问题”。虽然笔试题里不涉及malloc但面试官会顺着话题问如果链表节点是通过malloc创建的反转的时候需不需要关心内存释放正确的回答是反转只改变指针指向不改变节点数量所以不需要释放内存只有当整个链表不再使用的时候才需要逐节点free避免野指针。所以笔试之后不要马上放松趁热打铁把每一道编程题的变种都过一遍。我当年准备秋招的时候专门建了一个文档把每天做过的笔试题按“题目类型、解题思路、边界条件、常见追问”四个维度整理面试前翻一遍效率非常高。5. 常见失分点与避坑经验实录5.1 选择题高频陷阱速查表这里我整理了一份B卷选择题常见陷阱速查表是我带过的小伙伴们反复踩坑的地方也是每年出现率最高的失分点陷阱类型具体表现正确认知sizeof vs strlensizeof字符串指针以为是字符串长度指针的sizeof是固定值数组的sizeof包含\0const修饰const char *p和char *const p搞混const靠近谁就限定谁static变量递归函数里static变量每次重新初始化static只在程序启动时初始化一次数组传参函数内用sizeof算数组长度数组传参退化为指针永远不要这样做宏定义直接把宏当函数用忽略类型问题宏是文本替换不是类型安全的函数结构体对齐直接把成员大小相加需要按对齐规则填充不能用加法字符串常量试图通过char *修改字符串内容字符串常量存储在只读区修改非法整数溢出无符号整数相减得到负数无符号整数运算结果仍是无符号注意隐式转换这份表格消化掉选择题的基本盘就有了。5.2 编程题最容易翻车的三个细节编程题翻车往往不是算法不对而是细节崩了。我总结三个最常见的细节问题每一个都见过无数人栽过。第一个是数组越界。很多人写字符串逆序时边界判断用left right这会导致两指针相遇后继续交换把同一个字符自己和自己换一次不会报错但逻辑不干净。更严重的是在没有处理空字符串的情况下strlen为0right变成-1直接访问s[-1]这是未定义行为在线笔试题的测试用例如果包括空字符串这段代码直接崩。第二个是忘记包含头文件。在笔试环境里编译错误一眼就能看出来但很多人还是会在写代码时忘记include stdio.h、include string.h。虽然有些在线编译器会自动带上常用头文件但依赖编译器的默认行为不是一个好习惯。笔试题的编程环境不会帮你处理这种问题还是老老实实把必要的头文件写全。第三个是读写函数的细节。scanf、sscanf、fgets、strtok这些函数在笔试里很常用但它们的细节很容易出错。比如scanf不会读取空格所以读一行含空格的字符串时要用fgetsstrtok会修改原字符串如果你想保留原串必须先复制一份。5.3 在线笔试平台的操作经验除了知识点本身在线笔试平台的操作也值得提前准备。我见过太多人因为不熟悉平台在考试开始后手忙脚乱。这里分享几条经验。提前半小时进入平台测试摄像头、麦克风、浏览器兼容性。很多平台只支持Chrome或指定版本的浏览器如果你用的是Firefox导致插件加载不了心态直接崩。笔试开始前先把代码编辑器跑通确认自动补全、编译运行、测试用例输入的入口在哪里。还有一些平台要求你“自己写main函数”有些则只需要提交函数体这种差异直接影响你最终提交的代码格式提前摸清楚很重要。在线笔试的代码运行环境往往是黑盒你只能通过预先给出的测试用例来验证代码正确性。所以提交前一定要设计几组自己的测试用例空字符串、单个字符、两个字符、全是相同字符、中文输入、极长输入等。这些边界条件测试通过的概率基本就是AC与否的关键。写代码前先想好这几点比写完再调试省太多时间。还有一个小技巧如果有编译错误优先看第一个错误而不是最后一个错误。因为很多后续错误是第一个错误引起的连锁反应改掉第一个错误后面的错误可能就自动消失了。这个习惯在我多年的笔试和实际开发中都特别管用。说实话这套B卷放在今天看依然是一份很值得认真对待的C语言试卷。它没有问你“最新的C17标准增加了什么”也没有问“C和C哪个更好”它考的就是每个C程序员每天都要面对的那些事指针指向哪里、内存如何布局、边界怎么处理、数据如何组织。我自己当年复习C语言笔试时最深的体会是不要靠背题而是要把每一个知识点都在编译器里跑一遍把代码的每一步执行过程在脑子里模拟一遍。C语言不像其他语言有那么多框架和语法糖它的学习成本很高但一旦你真正理解了指针对内存的操作方式后面学什么都会很快。最后分享一个小技巧准备校招笔试时不管你投的是哪个岗位C语言基础题都值得刷一遍。因为很多公司即使招Java或Python岗也会有一两道C语言题目作为加试题。而且做C语言题训练出来的边界意识、内存意识放在任何语言里都是通用的。祝大家笔试顺利拿到心仪的offer。