1. 这套C卷的出题逻辑与备战价值
看到"欢聚时代2017校招笔试题目(C 基础类)C卷"这个标题,估计不少准备校招的同学第一反应是:这都几年前的老题了,还有翻出来的必要吗?
我的答案是:太有必要了。
欢聚时代当年在互联网圈的地位不用多说,YY语音、YY直播、虎牙游戏直播(后来分拆独立)都是它旗下的产品。这类做实时音视频、直播互动、游戏加速业务的公司,对C/C++岗位候选人的基本功要求是出了名的严格。原因很简单:直播场景下的音视频编解码、流媒体传输、客户端底层SDK、服务端高并发网络框架,每一层都离不开对内存、指针、并发模型和系统资源的精细控制。一个指针用不利索的人,在线上环境里很可能就是一次内存泄漏或者一次段错误的事故——在直播场景里,那就是几百万用户同时卡顿的惨案。
所以欢聚时代的C基础笔试题,基本不打擦边球,问的就是你大学四年到底有没有真的"C语言入门"。C卷里的题目几乎全部围绕C语言的核心机制展开,包括但不限于:
- 指针与数组的关系、指针运算
- 内存分区与生命周期
- 字符串处理与边界条件
- 结构体、联合体、位域的内存布局
- 关键字(static、const、volatile、extern)的语义辨析
- 链表的基本操作与边界处理
- 递归、排序、查找等基础算法的手写能力
- 预处理指令与宏定义的使用
这套题对没有任何项目经验、刚从学校走出来的大三/大四学生来说,就是一块试金石。它能快速过滤掉那些简历上写着"精通C语言"、实际上连const char* p和char* const p都分不清的候选人。反过来,如果你能稳稳地把这套卷子拿下来,说明你的C语言基本功在应届生里属于前20%的水平,后面无论是去做嵌入式、客户端、后台还是游戏开发,都有了可以继续往上盖楼的地基。
这篇文章我不会只讲答案,而是会把每一类核心考题背后的为什么拆开来讲清楚,再结合我自己当年校招和后来带应届生时的一些体会,帮你把"会做题"升级成"真懂C"。不管你是正准备校招的2025届同学,还是刚入职想夯实基础的初级工程师,这篇都值得花二十分钟读完。
2. 指针与内存管理:校招C卷的必考重灾区
说C语言笔试绕不开指针,就跟说面试绕不开自我介绍一样。欢聚时代C卷里,指针和内存相关题目通常能占到总分的四成左右。这个比例一点不夸张,因为指针就是C语言的灵魂,也是区分"背过语法"和"真会写C"的分水岭。
2.1 指针和数组的等价关系,其实藏着两个坑
先看一个C卷上非常经典的选择题变体:
#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int *p = a; printf("%d\n", *(p++)); printf("%d\n", *p++); printf("%d\n", (*p)++); printf("%d\n", *++p); printf("%d\n", ++*p); return 0; }问你程序的输出是什么。
第一眼看上去,五个printf长得几乎一模一样,全是p和*和++的组合。但这恰恰是出题人最阴险的地方:*p++、(*p)++、*++p、++*p这四种写法,在C语言里语义完全不同。
*(p++):先取*p的值用于表达式,然后p自增,等价于*p++。(*p)++:先取出*p的值,然后把p指向的那个变量自身加1,p指针本身不动。*++p:p先自增,再解引用,取的是下一个元素的地址上的值。++*p:先解引用取到变量,再把这个变量的值加1,等价于++(*p)。
要得分,你对运算符优先级必须有肌肉记忆。++和*同为单目运算符,结合方向是自右向左,所以*p++实际上被解析成*(p++),这一点很多初学者第一眼会看错。而一旦涉及到"变量本身自增"还是"指针移动",就要求你对"表达式求值结果"和"副作用发生时机"有清晰的认知。
我自己当年笔试的时候就栽在(*p)++上了,因为a数组里的第三个元素3被自增成了4,而不是打印出3之后把指针往后挪。这个细节不注意,后面所有依赖数组内容的题目全都会连锁出错。
2.2 指针运算:不要只记公式,要理解地址的本质
另一类高频考点是数组指针加减后的结果判断,典型题目长这样:
int a[4] = {10, 20, 30, 40}; int *p = a;问*(p + 3)和*(int*)((char*)p + 3)分别是多少。
第二个表达式估计能卡掉一半人。因为p + 3走的是int指针的步长,一个int是4字节,所以p+3指向第四个元素40;但先把p强转成char*再加3,就是只往后挪3个字节。在整型4字节、小端存储的机器上,数组第二个元素20在内存里的布局是14 00 00 00,从起始地址往后3个字节是00 00 00 14,按int读出来就是从地址偏移3开始的一个新int值,具体结果取决于你机器的字节序。
这类题在C卷里出现的意义,不是为了让你考试时去算字节序,而是提醒你:指针运算的本质是"指向某个内存地址",而移动多少取决于你当前这个指针的类型大小。很多线上崩溃问题,归根溯源就是指针类型用错导致的越界读写。校招阶段就把这个肌肉练出来,远比到时候靠debugger一点一点查要稳。
2.3 内存分区的生命周期题,背下来不如画出图
C卷的填空题里十有八九会出现类似下面这道:
#include <stdlib.h> char *func(void) { char *p = "hello"; static char s[] = "world"; char buf[] = "temp"; char *q = (char*)malloc(16); return p; }问你这些变量分别存储在哪个内存区域,生命周期各是什么。
如果你只是死记硬背"字符串常量在常量区、静态变量在全局区、局部变量在栈上、malloc在堆上",那这道题你能拿一半分,但有一个坑你可能意识不到:p指向的"hello"字符串字面量在静态存储区,等函数返回时局部变量p和buf虽然销毁了,但那个字符串字面量仍然还在,所以返回p不会像返回buf那样产生悬垂指针。
但如果你返回的是buf呢?buf是栈上的数组,函数返回后这块内存就成了"未定义区域"。它的值可能还在,也可能被下一次函数调用覆盖。这就是经典的返回栈地址问题,很多C卷会在编程题里给你一个类似的结构,让你指出代码的运行时问题。
关于malloc,校招题通常还会考两个细节:
- malloc之后没有判断返回值是否为NULL,就直接使用——这是新手最常见的毛病,在嵌入式这种内存受限环境里尤其致命。
- malloc和free必须配对出现,但"配对"不是简单的数量相等,而是每次free的必须是那一次malloc返回的原地址,不能是移动过的指针。比如:
char *p = (char*)malloc(32); p++; free(p); // error: 释放的不是malloc返回的原始地址上面这种错误形式在C卷的改错题中很常见。
我给你的建议是:别死背知识点的条目,而是拿一张白纸,把进程的虚拟地址空间从低到高画出来——代码段、只读数据段、数据段、BSS段、堆、栈、命令行参数和环境变量区——然后把你代码里的每个变量手动放进对应的段里,标记它的生命周期起点和终点。这个过程做三遍,后面涉及内存的C卷题基本就是送分题。
3. 字符串、宏定义与结构体:选择题里的"文字陷阱"集中营
C卷的客观题部分,除了指针,最喜欢在三个方向上下套:字符串处理细节、宏定义副作用、结构体对齐。这几类知识点不涉及复杂算法,但坑极多,非常能拉开"背过书"和"写过代码"的人之间的差距。
3.1 字符串函数的分寸感:strlen、sizeof、strcpy、strcat
很多选择题会给你下面这样的代码,问输出:
char s1[] = "hello"; char s2[] = {'h', 'e', 'l', 'l', 'o'}; printf("%d %d\n", sizeof(s1), strlen(s1)); // 6, 5 printf("%d %d\n", sizeof(s2), strlen(s2)); // 5, ?s1用字符串字面量初始化,末尾自动带一个'\0',所以sizeof是6,strlen是5;s2用字符序列初始化,根本没有结束符,所以sizeof是5,strlen会顺着内存一直数下去直到遇到一个随机的0,得到的结果是无法预知的。
这类题考察的就是你对C字符串本质的理解:所谓字符串,就是一段以'\0'结尾的字符序列。'\0'是一个值为0的字节,它的存在决定了字符串的长度和边界。strcpy、strcat、sprintf这些函数在拷贝或拼接时,都不会主动检查目标缓冲区是否够大,一旦源字符串超长,就会发生缓冲区溢出——这个知识点也是C卷常考的"改错题"素材。
我给你一个实际例子,当年笔试卷子里的改错题大概是这样的:
char buf[16]; strcpy(buf, "hello, world, this is a test"); printf("%s\n", buf);一眼就能看出,源字符串长度远超16字节,strcpy会一路写下去,把buf后面的栈内存全部覆盖。在真实业务里,这类漏洞如果被恶意利用,就可能变成溢出攻击的入口。校招笔试不需要你写得像安全专家那么深,但你必须能指出问题并给出改进方案:要么用strncpy并手动确保结尾置零,要么直接改用snprintf。
3.2 宏定义的括号问题,简直是为坑人而生的
C语言笔试如果没有宏定义相关的题目,基本不完整。最典型的是:
#define SQUARE(x) x * x问你SQUARE(1 + 2)的结果是什么。
如果你不假思索地写9,那就掉坑里了——因为宏展开是纯文本替换,SQUARE(1 + 2)被替换成1 + 2 * 1 + 2,按照运算优先级,结果是1 + 2 + 2 = 5,不是9。
正确的写法应该是:
#define SQUARE(x) ((x) * (x))括号要加两层:每个参数本身加括号,整个表达式再加括号。少一层都不行。
C卷里这类题还会进一步升级,比如:
#define MAX(a, b) ((a) > (b) ? (a) : (b))然后问MAX(++a, b)有没有问题。当然有问题:如果a大于b,宏展开后++a会被执行两次,行为完全不可控。正确的做法是改用内联函数:
static inline int max_func(int a, int b) { return a > b ? a : b; }为什么宏在C里还大量存在?因为它可以处理任意类型的参数,C89标准还没有inline关键字,只能靠宏实现泛型效果。但代价就是没有类型检查、容易产生副作用。在笔试里,你要能一眼识破这类陷阱,并且在代码里尽量避免写复杂的宏。
3.3 结构体对齐:sizeof结果为什么总和你心里想的不一样
结构体的sizeof是C卷选择题里另一个被翻来覆去使用的考点。来看一个非常经典的结构体:
struct Node { char c; // 1字节 int i; // 4字节 char d; // 1字节 };问sizeof(struct Node)是多少。
很多人会答6。但在默认4字节对齐的平台上,答案是12。原因在于:结构体成员在内存中的存放要遵循对齐规则。编译器会在c后面填充3个字节,把i放到位偏移4的位置;d紧接在i后面,然后整个结构体的大小还要对齐到最大成员对齐数的整数倍,所以结构体末尾又会补3个字节,最终是12。
对齐规则的本质,是CPU访问内存时按字长读取效率最高。如果int从地址偏移3开始放,CPU可能需要两次内存访问才能拿到完整的int,性能损失很大。所以编译器宁可浪费一点点空间来换取访问效率。
这个知识点在笔试里通常有三种考法:
- 给一个结构体,让你算sizeof——注意成员顺序不同,结果也可能不同。把大的成员放到前面,小的放后面,往往能减少填充,节省结构体内存。
- 给你两个成员相同的结构体,只是声明顺序不同,问它们的大小是否一样。
- 用
#pragma pack(1)改变对齐方式,问sizeof变成多少。
面试官问这类题,表面考结构体布局,实际是想看你有没有系统级的意识——结构体可以是一条网络报文、一个磁盘记录、一个内存池节点,布局不合理的结构体在千万级并发读写下浪费的内存是相当可观的。C卷里考它,本质上是在挑选对"底层细节敏感"的人。
4. 从经典程序题看代码实现风格:链表操作与排序算法的得分要点
C卷的编程题,通常不会出太偏太难的算法,毕竟考察对象是应届生,而这些算法在真实场景里也确实用得最多。欢聚时代那套题里,我印象最深的程序题类型有三个:链表逆序、字符串逆序输出、冒泡排序或选择排序的手写。
4.1 链表逆序:边界条件的"三连环"考验
链表题在笔试中的出现频率极高,因为链表涉及指针操作、结构体定义、循环控制和边界处理,一道题能同时考察C语言最核心的几个能力点。
题目描述一般是:给定单链表头指针,实现反转链表的函数。
struct Node { int data; struct Node* next; }; struct Node* reverse(struct Node* head) { struct Node *prev = NULL; struct Node *cur = head; struct Node *next = NULL; while (cur != NULL) { next = cur->next; // 保存后继节点 cur->next = prev; // 指针反转 prev = cur; // 前驱后移 cur = next; // 当前节点后移 } return prev; }这段代码的得分点在于你能否判断三个地方的边界:
- 输入是空链表(head == NULL)时,循环直接不执行,返回NULL,逻辑上没有崩溃。
- 只有一个节点时,prev最终指向这个唯一的节点,返回正确。
- 循环中的三行赋值顺序不能颠倒:必须先保存next,再改cur->next,然后更新prev和cur。如果先改cur->next,原来的后继节点就找不到了。
很多同学在笔试时能写出循环的框架,但忘记了保存next这一行,或者返回的是cur(循环结束后的NULL)而不是prev。这些细节就是阅卷人扣分的地方。校招笔试的编程题,从来不是只靠"思路对"就给满分的,写完代码后一定要自己拿空链表、单节点、双节点、多节点这四组输入在脑子里跑一遍。
4.2 字符串逆序:注意"原地逆序"和"逆序输出"的区别
字符串逆序是C语言入门级题目,但笔试题里出得极其频繁,而且和它配套的知识点往往不止一个。
一种考法是要求逆序输出,比如输入"hello",输出"olleh"。很多人第一反应是直接用strlen求出长度然后从后往前for循环打印。这样写没问题,但如果题目要求"原地逆序"(即不能申请额外空间,不能使用临时数组),实现就略有不同了:
void reverse_string(char *s) { if (s == NULL) return; int len = strlen(s); int left = 0; int right = len - 1; while (left < right) { char temp = s[left]; s[left] = s[right]; s[right] = temp; left++; right--; } }这里有两个隐藏考点:其一,对NULL和空字符串的处理;其二,strlen(s)的时间复杂度是O(n),加上后面的逆序又是O(n/2),整体仍然O(n),你能不能在写代码时主动说出这个复杂度分析?笔试阅卷时,如果你在注释里清晰标注每个操作的时间复杂度和边界条件,通常会比只给一个裸代码的答卷更有好感。
还有一类变体把字符串逆序和单词逆序结合:给定"I am a student",要求输出"student a am I"。这类题的思路分两步:先把整句字符串字符级逆序成"tneduts a ma I",再把每个单词逐个逆序回来。代码量不大,但属于"思路稍拐一个弯"就能考倒一票人的题,建议你备考时把这类"两次逆序"的套路记住。
4.3 冒泡排序和选择排序:要能回答"为什么效率低"
排序算法的笔试题一般不会只让写代码,还会跟着追问复杂度、稳定性以及是否适合某种数据规模。
比如冒泡排序的C语言实现,几乎每个计算机系学生都背过:
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 temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = 1; } } if (!swapped) break; // 全程无交换,说明已有序 } }我见过很多人在笔试里连这个swap都没有写对,也有不少人没有加swapped这个提前退出优化。加了它之后,在基本有序的数组上冒泡排序可以从O(n^2)退化场景下直接提升到O(n)——这个优化在直播平台的排行榜数据或者聊天室高频小数据量排序里,其实有明确的应用价值。
笔试答案里,除了代码本身,我还建议你在旁边写清楚:冒泡排序是稳定排序(相等元素不会交换顺序),最好时间复杂度O(n),最坏O(n^2),空间复杂度O(1)。这个注释说明了你是清楚这些结论的,而不是仅凭记忆默写代码。
5. 从应试到编码习惯:C卷题目背后的工程思维
很多同学把校招笔试复习当成"刷题过关",觉得把常见考点背熟、代码默写流畅就够了。这当然能帮你在笔试中拿分,但我接触过的技术面试官,真正想在C卷里看到的东西,其实比"答对"要多一层——他们想确认你是不是一个在写C代码时自带工程意识的人。
5.1 变量命名和代码注释,是阅卷人给你的"软性加分项"
笔试卷子里的编程题,阅卷人不会一行一行去运行,而是快速扫读你的代码结构和关键逻辑。这时候,好的命名习惯就格外重要。
我见过有同学写链表逆序时用p1、p2、p3这样的命名,逻辑虽然对,但阅卷人必须花时间去对照每行代码才能看懂哪个是前驱、哪个是当前、哪个是后继。如果你用prev、cur、next,一眼就能看懂。相比之下,同样正确的一段代码,后者的印象分高出一截。
对于关键的二三行,我建议你顺手加上简短注释,说明自己的意图。不用写长段落,就比如:
// 先用next保存后继,否则修改cur->next后会丢失链表这类注释在笔试环境下,是一种额外沟通:它向阅卷人证明你不光会写,还真的理解为什么这样写。这在应届生笔试中非常加印象分。
5.2 防御性编程:在笔试里就开始考虑"参数不对会怎样"
防御性编程是工程经验丰富的程序员写代码时的自然习惯,但它在校招笔试中并不是默认出现的。你可以在自己的答卷中主动体现:
struct Node* reverse(struct Node* head) { if (head == NULL || head->next == NULL) { return head; } // ... }提前处理空指针和单节点,能规避在后续代码里的越界访问和空引用,也让代码的健壮性一目了然。对于malloc,也要在申请后判断是否失败:
int *p = (int*)malloc(sizeof(int) * n); if (p == NULL) { // 错误处理,比如返回错误码 }这种写法在真实项目里是底线要求。直播系统的每路推流都要频繁分配缓冲区,不判空直接使用,一旦内存紧张就会发生空指针崩溃。备考时养成这个习惯,考场上就不用刻意提醒自己,自然而然就会写出来。
5.3 不要只为了过笔试而准备:这些C基础是未来业务的硬承载力
回到标题本身——"C 基础类"。欢聚时代当年之所以出这么一套基础为主的C卷,根本原因是它的核心业务(音视频、直播、网络传输)建立在大量C/C++的高性能基础设施上。等到你入职之后,面对的可能是几万路并发推流、低延迟调度、底层媒体管线内存池管理,这些场景下的所有bug排查,最后几乎都能回到C语言基本功的某个知识点上。
所以在准备这套题的时候,我的建议是别只盯着"答案对不对"。每做完一类题,问自己三个问题:
- 这个知识点如果放到真实项目里,会在什么场景下出问题?
- 我能不能写一段小demo,实际跑一跑验证我的理解?
- 我能不能用一句话向别人解释清楚这里的原理?
如果这三个问题都能答上来,那么这张C卷对你来说就不再是"一道需要背的题",而是你工程能力的一块基石。反过来讲,就算你这次笔试过了,基本功不扎实,后面入职后面对真实模块时依然会非常吃力。C语言基础类的笔试,与其说是门槛,不如说是给所有候选人提供的一次自我体检。
我个人在带新人的时候,经常说一句话:你可以暂时不熟悉某个业务框架,但不可以在指针和内存管理上犯糊涂。因为业务框架可以学,而C语言的基础决定了你在系统级问题面前是能接得住,还是必然会漏。这也是为什么这类2017年的旧题放到今天,依然值得你花时间认真拆解一遍。