
欢聚时代2017年校招笔试题目C基础类C卷这名字听着就带着一股秋招的怀旧味。当年我也在考场里见过类似的卷子现在回头看这些题其实特别适合用来检验一个人的C语言底子到底扎不扎实。无论你是准备校招的在校生还是工作几年想回头补基本功的开发者这份卷子的考点设计都值得认真过一遍。这份C卷主要面向的是C基础类岗位考察范围集中在指针、内存、字符串处理、结构体、经典算法手写这些硬核内容上。它能帮你快速定位自己在C语言上的短板在哪也顺带展示了一下互联网公司笔试的出题逻辑。接下来我把这份卷子的考点、出题思路、典型题解法和踩坑经验一次性说清楚。1. 整体考点布局与出题思路1.1 笔试的定位与筛选逻辑校招笔试和期末考试是两回事。期末考试追求知识覆盖面广而校招笔试的定位是“快速筛掉基本功不过关的人”。欢聚时代这份C卷给我的感觉就很典型它不考冷门语法也不考复杂算法更不考什么“茴字有几种写法”式的偏题它考的全是开发日常里天天要碰的东西。为什么这么设计因为笔试的目的不是选拔天才而是筛选出那些“丢到项目里能立刻上手写代码的人”。C语言岗位的基础要求就是能操作指针、能管理内存、能处理字符串、能写链表这些能力在笔试里都可以通过几道短小精悍的题目测出来。所以你看那些看似简单的题目背后其实藏着面试官对候选人工程能力的预判代码风格、边界条件意识、内存安全意识全部融入在短短的代码题里。1.2 考点分布地图我根据当年的回忆和相关资料把C基础类的常考知识点整理成了一张分布表方便你对照查漏补缺考点模块具体知识点考察频率建议掌握程度指针指针运算、指针与数组、指针参数必考熟练手写字符串处理字符串拷贝、逆序、查找、越界问题必考熟练手写内存管理malloc/free、野指针、内存泄漏高频原理清晰结构体与联合体结构体大小计算、内存对齐、位域中频会算会写经典算法手写冒泡排序、链表反转、递归转换高频熟练手写预处理器宏定义、条件编译、头文件守卫中频理解原理输入输出scanf/getchar/gets的坑中频能避坑从这张表能看出来指针和字符串是绝对的主角两者加起来占了整张卷子将近一半的分值。这不奇怪C语言的精髓就在这两个地方指针玩得转C语言就入门了字符串处理稳得住工程代码就不会出大乱子。1.3 题目难度阶梯与答题策略这份卷子的难度梯队是分层的。前面一般是选择题和填空题考的是概念和细节比如“以下哪个选项对指针的赋值是合法的”“sizeof和strlen的区别是什么”这类。中间是简答题要求解释某个代码片段的行为比如结构体对齐后的大小是多少、某个宏展开之后是什么结果。最后是编程题通常是两道到三道考字符串逆序、链表操作、排序这些经典题目。我建议的答题策略是先花3到5分钟把整张卷子扫一遍心里有个难度分布图。然后从最简单的概念题开始做把能拿的分先拿到手再啃中等难度的题最后集中精力做编程题。编程题宁可写得慢一点也不要写完就交一定要自己代入几组边界数据跑一遍比如空字符串、全相同字符、只有一个节点的链表这类情况很多失分都在边界条件上。2. 核心基础考点拆解2.1 指针与数组的纠缠关系指针和数组的关系是C语言笔试里最经典也最阴险的部分。很多人一开始搞不懂“数组名是常量指针”这个说法其实在绝大多数表达式中数组名会隐式转换为指向首元素的指针但有两个例外一是作为sizeof的操作数二是作为取地址符的操作数。笔试题里常见的一个坑是这样的int a[5]; printf(%d\n, sizeof(a)); // 输出 20假设int占4字节 printf(%d\n, sizeof(a 0)); // 输出 8指针大小64位系统 int *p a; printf(%d\n, sizeof(p)); // 输出 8指针大小 int (*pa)[5] a; printf(%d\n, sizeof(*pa)); // 输出 20解引用后是数组这个例子把数组名、指针、数组指针的区别全涵盖了。很多人会栽在a0这个表达式上以为它和a等价实际上在a0这个运算中a已经隐式转换成了指针所以sizeof得到的是指针大小。而a取出的是整个数组的地址类型是int()[5]和int完全不同在做指针运算时步长也不一样。笔试中如果遇到p1和a1的区别一定要先搞清楚p是什么类型的指针a是什么类型的数组。p是int*那么p1跳过一个inta是int[5]那么a1跳过整个数组。这算是基础中的基础但每次校招都能见到有人在这上面丢分。2.2 字符串处理的隐藏细节字符串是C基础类笔试的常客因为字符串处理直接考验指针操作、数组边界和库函数理解这三项能力。最常见的编程题包括字符串逆序、字符串拷贝、统计字符出现次数、判断回文等。自己实现字符串拷贝是一个很好的试金石很多人的第一版会写成这样void my_strcpy(char *dest, char *src) { int i 0; while (src[i] ! \0) { dest[i] src[i]; i; } }这个写法错误在于没有给dest末尾补上\0导致dest不是一个合法的C字符串。而且返回类型应该是char*而不是void标准库的strcpy就是返回目标地址这样支持链式表达式。改进版是这样的char *my_strcpy(char *dest, const char *src) { char *ret dest; while (*dest *src) ; return ret; }这个写法很经典循环条件利用了赋值表达式的值作为判断条件当拷贝到\0时赋值结果为0循环自然结束同时\0也成功拷贝到了目标数组里。值得注意的还有参数用了const char*这是工程代码里的好习惯源字符串不应该被修改所以用const限定笔试时这样写会给面试官留下好印象。字符串逆序的实现也值得一提。我自己常写的是双指针法void reverse(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--; } }这里要注意strlen返回的是size_t类型当字符串为空时s strlen(s) - 1这个表达式要特别小心因为strlen返回0的话s - 1是未定义行为。所以最好先判断字符串长度或者在进入循环前确保长度大于0。笔试时不要只写“核心逻辑”这些安全性检查至少要在注释里体现出来让阅卷人知道你有边界意识。2.3 内存管理与动态分配问题内存管理是C语言笔试中区分“会写代码”和“真懂C”的分水岭。考察方式一般是给一段代码让你判断哪里有内存泄漏、哪里是野指针、哪里是非法访问。常见的野指针场景是这样的char *get_string() { char buf[] hello; return buf; // 返回了局部数组的地址函数结束即失效 }这个buf是栈上的局部数组函数返回后栈帧被回收返回的指针就成了野指针。虽然有时候碰巧能读到数据但这是纯粹的未定义行为。正确做法是用malloc在堆上分配然后返回指针由调用者负责free。另一类高频考法是malloc和free的配套问题char *p (char *)malloc(128); // 中间若干操作 free(p); // 如果下面又有赋值操作 p[0] a; // 错误释放后再访问我见过不少人在free之后忘了把指针置NULL导致后面if(p)判断失效。正确的习惯是free之后立即p NULL这样后续访问空指针时程序至少能快速崩溃暴露问题而不是悄悄产生野指针行为让bug躲到线上才爆发。内存泄漏的识别也很重要常见场景包括每次循环里malloc但不free、结构体里嵌套的动态内存只释放了外层、函数提前return漏掉了释放。笔试中的代码题如果你用到了malloc一定记得在代码里补上对应的free哪怕不写完整实现也要用注释标注“此处需释放”这会让你的代码显得成熟不少。2.4 结构体、联合体与内存对齐的计算结构体大小计算是选择题和填空题的最爱核心是内存对齐。规则总结起来就三条结构体的每个成员按其自身对齐值对齐结构体的总大小必须是最大对齐值的整数倍对齐值一般是成员类型大小和编译器默认对齐值中较小的一个。举个例子假设默认对齐值是4字节typedef struct { char a; // 1字节从偏移0开始 int b; // 4字节对齐要求4所以从偏移4开始 char c; // 1字节从偏移8开始 } Test; // 总大小9但最大对齐是4所以要补齐到12很多人会说“char 1 int 4 char 1 6”结果就掉坑里了。实际上sizeof(Test)是12而不是9。这个计算题几乎是必考的唯一的变数是看考卷用的是32位还是64位环境64位下指针是8字节对齐规则会相应变化。联合体的大小计算也常见规则更简单联合体的大小等于最大成员的大小但同样要考虑对齐。比如typedef union { char c[9]; // 9字节 double d; // 8字节对齐到8 } U; // 大小按最大对齐的整数倍对齐所以是16因为double的对齐值是89不是8的倍数所以补齐到16。这类题考的就是你明不明白“联合体的大小由最大成员决定但收尾对齐规则依然生效”这句话。3. 经典编程题的实现演练3.1 字符串逆序与回文判断字符串逆序前面已经写过了回文判断则是在此基础上加了一个双指针向内收拢的过程。笔试里经常会把这两个放在同一道题里先让你逆序再问你如何判断回文。判断回文更简洁的写法是用两个下标从两端向中间扫描bool is_palindrome(const char *s) { if (s NULL) return false; int left 0; int right strlen(s) - 1; while (left right) { if (s[left] ! s[right]) return false; left; right--; } return true; }这道题的关键点是判断结束的条件是left right而非left right因为当字符串长度为奇数时中间那个字符不需要比较自然就是回文。如果写left right在长度为偶数时也没问题但奇数时会多一次无意义的自比较虽然不影响结果却在面试官面前暴露了你对边界条件思考不够细致。3.2 冒泡排序的多种写法与优化冒泡排序是校招手写代码里最常出现的排序题因为它简单、能快速考察代码规范性同时也有足够的空间让你展示“我懂得优化”。最基本的写法是两层循环但如果你能写上加标志位的优化版本档次就上去了。这个优化思路是如果某次内层循环没有任何交换发生说明序列已经有序继续循环纯属浪费时间。void bubble_sort(int arr[], int n) { if (arr NULL || n 1) return; 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; } }此外要注意参数的写法int arr[]在函数参数中本质上是一个int*指针所以写成int *arr也可以。但为了让阅读者一眼看出它是数组形态笔试中建议保留int arr[]的写法。还有n是数组长度如果笔试题目里没给你自己也要把这个参数带上不要试图用sizeof(arr)/sizeof(arr[0])来计算长度这在函数参数里是拿不到正确数组大小的这也是一个隐藏考点。3.3 单链表反转的迭代与递归实现链表操作题在C语言笔试中的出现率非常高因为链表天然考察指针操作和内存结构的理解。单链表反转通常要求手写而且面试官很可能会追问你“迭代写了递归会吗”迭代实现比较直观用三个指针prev、curr、next循环推进struct Node { int data; struct Node *next; }; struct Node *reverse_iter(struct Node *head) { struct Node *prev NULL; struct Node *curr head; while (curr ! NULL) { struct Node *next curr-next; // 先保存下一个节点 curr-next prev; // 反转当前节点指针 prev curr; // 移动prev curr next; // 移动curr } return prev; // 新的头节点 }这里有个细节必须注意curr-next prev这行执行之前必须先把curr的下一个节点保存下来否则反转当前节点后后面的节点就找不到了。我第一次写这个题时就栽在这里因此记忆特别深。递归实现要理解“反转以head为头部的链表返回新的头节点”这个函数定义struct Node *reverse_rec(struct Node *head) { if (head NULL || head-next NULL) { return head; } struct Node *new_head reverse_rec(head-next); head-next-next head; head-next NULL; return new_head; }递归实现虽然代码量更少但笔试时容易在边界条件上出错。关键在于if条件里必须同时判断head和head-next是否为空否则当链表只有一个节点时就会出现空指针解引用。递归写的代码面试官看起来会觉得你有一定的代码功底但在笔试这种环境下如果递归没写熟我建议优先写迭代版本稳妥拿分比展示华丽更重要。3.4 递归与经典数列的取舍问题斐波那契数列在笔试里的出现率也极高但它的考点其实不是“你会不会写递归”而是“你知不知道普通递归的性能问题”。很多人一上来就是int fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }这个写法没问题但当n比较大时重复递归计算量是爆炸增长的。面试官想看到的往往是迭代版本或者至少你能指出这个递归存在的问题int fib_iter(int n) { if (n 1) return n; int a 0, b 1; for (int i 2; i n; i) { int next a b; a b; b next; } return b; }笔试时如果题目明确说“请用递归实现”那就写递归如果只说“实现斐波那契数列”优先写迭代。这个判断能力本身也是考察点之一。类似的还有阶乘的递归与迭代选择、二分查找的边界处理都是同一个套路你不仅要会写还要知道不同写法的适用场景和性能表现。4. 笔试中的陷阱与失分点实录4.1 读题不仔细导致的低级失分我见过太多人编程题写对了逻辑最后却因为没注意到题目里的隐藏条件白白丢分。比如题目说“字符串长度不超过1000”那就应该在代码里定义char str[1001]留一个位置给\0。比如题目说“两个字符串可能有重叠”那么自己在实现strcpy时要考虑内存重叠情况否则行为未定义。还常见一种情况题目要求“不得使用库函数”结果考生用strlen、strcpy、strcmp用得飞起。这种属于典型的没读懂题意即使代码写得再漂亮也会被扣分甚至直接判零。建议拿到题目后把要求里的“不得使用”“必须保证”“时间限制”“空间限制”等关键词圈出来花30秒确认再动笔。4.2 手写代码时的环境差异校招笔试现在虽然很多是机考但也有一些是纸质试卷。纸质试卷考察时没有编译器帮你查错也不会有自动缩进和补全代码全靠手写。这时候最难受的错误主要是声明了变量但没用、忘了加分号、结构体没有用typedef、头文件没写。我踩过最亏的坑是纸质卷上写代码时忘了包含#include string.h然后用strlen。虽然阅卷时老师不一定严格按编译结果给分但这个错误在阅卷人眼里是非常刺眼的因为它说明你的工程习惯不好。建议即使是手写代码也要把需要的头文件写在代码块顶部哪怕题目只要求写函数片段我一般也会在函数上方写上#include的注释备注或者和阅卷老师说明“这里需要包含XX头文件”。4.3 边界条件与极端输入的思考缺失笔试代码题最拉分的地方就在边界条件处理。面试官的脑子里有一套测试用例空字符串、空链表、n0、n1、指向NULL的指针、数组长度刚好是一个边界值……所有这些都是算法正确性的试金石。以字符串逆序为例如果字符串是空串strlen返回0right s - 1这时不能解引用。正确的做法是一开始就判断*str \0如果成立则直接return。再比如写二分查找时如果数组只有一个元素要确保mid取整不会导致死循环。我给一个实用的小技巧代码写完后自己心里跑三个测试用例——最小用例0或空、常规用例普通数据、边界用例恰好满足某个边界条件。花两分钟过一遍能在笔试中避免大量低级错误。很多人在笔试里敲完代码就洋洋洒洒交卷结果回来一复盘发现自己在while循环条件上少写了一个这种失误非常可惜。4.4 宏定义与预处理的经典坑宏定义是C卷选择题里的常见坑点主要考的是“宏是纯文本替换不是函数”。一个经典例子#define SQUARE(x) x * x int result SQUARE(2 3);很多人会心算成25实际上宏展开后是2 3 * 2 3等于11。正确答案是把宏写成#define SQUARE(x) ((x) * (x))而且外层括号也不能省。笔试题经常在这个外层括号上做文章比如写成#define SQUARE(x) (x * x)传入23时展开为(2 3 * 2 3)依然是11只有两层括号全写才能正确。另一个常见坑是宏参数的自增运算SQUARE(x)这种调用在宏展开后参数会被计算两次导致x自增两次。这是宏天然的问题没办法根治只能在使用时避免传带有副作用的表达式。笔试中如果遇到这种题考点就是你知道不知道“宏的参数不会先求值只是文本替换”。5. 备考策略与实战感悟5.1 考前一个月的高效复习路径如果你距离校招笔试还有一个月不要整本整本地翻C语言教材效率太低了。我建议把时间花在刀刃上按优先级从高到低排大概是指针和内存管理、字符串处理、链表和经典排序、结构体对齐和宏、文件操作和输入输出。每天保证手写5到10个小函数不要看参考代码全部默写写完自己找边界条件测。字符串逆序、字符串拷贝、冒泡排序、链表反转、二分查找这五道题几乎覆盖了80%的C基础类笔试考点花一周把它们练到肌肉记忆性价比极高。LeetCode上的简单题和部分中等题也值得做一遍尤其是数组、链表、字符串相关的题目。但不是让你刷难题校招C基础类卷子的算法难度普遍不高重点是“会写”和“写得稳”而不是“写得炫”。5.2 答题时的代码规范收益同样的逻辑有人拿满分有人被扣分差在代码规范上。笔试阅卷人看到乱糟糟的代码第一印象就会打个折扣。缩进统一、变量命名清晰、关键逻辑有注释、边界条件有处理这些因素看起来不是“考点”但实际就是隐形的考点。我建议代码里的变量名不要用a、b、tmp满天飞除非是交换变量这种通用场景。像prev、curr、new_head、left、right这种有意义的命名一眼就能看出变量职责阅卷人看着省力气给分自然也痛快。代码块的排版也尽量工整一行一操作不要在一行里堆好几条语句。另外一个小技巧是编程题可以先把整体思路写在代码上方的注释里然后按注释逐步实现。比如写链表反转前先写注释// 迭代翻转链表 // 用prev保存已翻转部分curr指向当前节点next保存后文 // 每次将curr-next指向prev然后三个指针同时后移这种方式在纸质笔试时特别有用一方面帮助自己理清思路另一方面即使代码没写完阅卷人也能从注释里看到你的思路是正确的部分分就到手了。5.3 从笔试题目反推技术方向把这份卷子和近几年的同类试卷放在一起对比你还会发现一个趋势互联网公司的C基础类笔试越来越往“工程能力”靠拢。早年的卷子偏语法细节现在更偏向内存安全、代码健壮性、复杂度分析这些偏实际开发的内容。这意味着准备校招笔试时不能只背语法点要建立“写生产级代码”的意识。比如写任何涉及内存的操作都要想一想是不是可能越界是不是可能泄漏是不是存在未定义行为。这种意识不是面试前突击出来的是在平时练习中一点点积累的。我自己的感受是把C语言基础笔试当作一次代码审查训练来准备收获远不止一份offer那么简单它对后续的工作习惯也有潜移默化的影响。再说一个我个人的感受如果你现在正好在准备校招这份卷子里的每一道题都值得你亲手写一遍而不是在脑内过一遍。因为C语言是门“动手才能暴露问题”的语言很多错误你不实际编译运行根本不会意识到自己会错得那么离谱。当年我笔试前觉得自己什么都会结果真题模拟时在一个字符串拼接的坑里卡了半小时从那以后我就老实了该动手时绝不偷懒。这套卷子考的东西不多但每一个点都很准。能把这些基础题吃透你的C语言功底基本就站在了一个很稳的起点上后面的路也会好走很多。