“欢聚时代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是多少?很多人认为是9(1+4+1+3),但实际上在默认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 strlen | sizeof字符串指针以为是字符串长度 | 指针的sizeof是固定值,数组的sizeof包含'\0' |
| const修饰 | const char *p和char *const p搞混 | const靠近谁就限定谁 |
| static变量 | 递归函数里static变量每次重新初始化 | static只在程序启动时初始化一次 |
| 数组传参 | 函数内用sizeof算数组长度 | 数组传参退化为指针,永远不要这样做 |
| 宏定义 | 直接把宏当函数用,忽略类型问题 | 宏是文本替换,不是类型安全的函数 |
| 结构体对齐 | 直接把成员大小相加 | 需要按对齐规则填充,不能用加法 |
| 字符串常量 | 试图通过char *修改字符串内容 | 字符串常量存储在只读区,修改非法 |
| 整数溢出 | 无符号整数相减得到负数 | 无符号整数运算结果仍是无符号,注意隐式转换 |
这份表格消化掉,选择题的基本盘就有了。
5.2 编程题最容易翻车的三个细节
编程题翻车往往不是算法不对,而是细节崩了。我总结三个最常见的细节问题,每一个都见过无数人栽过。
第一个是数组越界。很多人写字符串逆序时,边界判断用left <= right,这会导致两指针相遇后继续交换,把同一个字符自己和自己换一次,不会报错但逻辑不干净。更严重的是在没有处理空字符串的情况下,strlen为0,right变成-1,直接访问s[-1],这是未定义行为,在线笔试题的测试用例如果包括空字符串,这段代码直接崩。
第二个是忘记包含头文件。在笔试环境里,编译错误一眼就能看出来,但很多人还是会在写代码时忘记include <stdio.h>、include <string.h>。虽然有些在线编译器会自动带上常用头文件,但依赖编译器的默认行为不是一个好习惯。笔试题的编程环境不会帮你处理这种问题,还是老老实实把必要的头文件写全。
第三个是读写函数的细节。scanf、sscanf、fgets、strtok这些函数在笔试里很常用,但它们的细节很容易出错。比如scanf不会读取空格,所以读一行含空格的字符串时要用fgets;strtok会修改原字符串,如果你想保留原串,必须先复制一份。
5.3 在线笔试平台的操作经验
除了知识点本身,在线笔试平台的操作也值得提前准备。我见过太多人因为不熟悉平台,在考试开始后手忙脚乱。这里分享几条经验。
提前半小时进入平台,测试摄像头、麦克风、浏览器兼容性。很多平台只支持Chrome或指定版本的浏览器,如果你用的是Firefox导致插件加载不了,心态直接崩。笔试开始前先把代码编辑器跑通,确认自动补全、编译运行、测试用例输入的入口在哪里。还有一些平台要求你“自己写main函数”,有些则只需要提交函数体,这种差异直接影响你最终提交的代码格式,提前摸清楚很重要。
在线笔试的代码运行环境往往是黑盒,你只能通过预先给出的测试用例来验证代码正确性。所以提交前一定要设计几组自己的测试用例:空字符串、单个字符、两个字符、全是相同字符、中文输入、极长输入等。这些边界条件测试通过的概率,基本就是AC与否的关键。写代码前先想好这几点,比写完再调试省太多时间。
还有一个小技巧:如果有编译错误,优先看第一个错误,而不是最后一个错误。因为很多后续错误是第一个错误引起的连锁反应,改掉第一个错误,后面的错误可能就自动消失了。这个习惯在我多年的笔试和实际开发中都特别管用。
说实话,这套B卷放在今天看,依然是一份很值得认真对待的C语言试卷。它没有问你“最新的C17标准增加了什么”,也没有问“C和C++哪个更好”,它考的就是每个C程序员每天都要面对的那些事:指针指向哪里、内存如何布局、边界怎么处理、数据如何组织。
我自己当年复习C语言笔试时,最深的体会是:不要靠背题,而是要把每一个知识点都在编译器里跑一遍,把代码的每一步执行过程在脑子里模拟一遍。C语言不像其他语言有那么多框架和语法糖,它的学习成本很高,但一旦你真正理解了指针对内存的操作方式,后面学什么都会很快。
最后分享一个小技巧:准备校招笔试时,不管你投的是哪个岗位,C语言基础题都值得刷一遍。因为很多公司即使招Java或Python岗,也会有一两道C语言题目作为加试题。而且做C语言题训练出来的边界意识、内存意识,放在任何语言里都是通用的。祝大家笔试顺利,拿到心仪的offer。