秋招倒计时早就过了那种“还有时间慢慢补”的阶段,我作为一个零基础起步的菜鸟,硬着头皮自学C语言已经进入第五天。前四天我把变量、类型、运算符、条件判断、循环过了两遍,每次写到switch都快把case分支漏光,但昨天我终于把循环里嵌套的break和continue搞明白了,那种“原来如此”的顿悟感,比刷手机爽太多。今天这个第五天,对我来说是一个分水岭:继续顺着教程往后点下一页当然容易,但我想换一个更功利、也更有效率的角度——秋招的C语言题到底在考什么?我该怎么用手里仅有的五天知识去接住那些题?这篇文章就是我自己第五天的心路历程加实操笔记,写给同样在咬牙自学的伙伴们,也写给那些还不太清楚“学了C语言到底能不能用来找工作”的观望者。
1. 第五天,我决定把“学过的”变成“能用的”
自学最容易遇到的一个问题就是:视频看完了,书翻完了,一合上本子,脑子里什么都没留下。前四天我一直在跟语法打交道,printf、scanf、if、else、while、for,我自认为掌握得七七八八,可真让我独立写一个能跑的、有点逻辑的程序,我就会卡在“下一步该写什么”上。第五天我给自己定的目标是:把前四天积累的东西揉进一个稍微完整一点的程序里,然后开始啃两个大块头——数组和函数。这两个东西,一个是数据结构的入场券,一个是模块化编程的起点,也是秋招机试和笔试题里绕不开的基础。
1.1 前四天我干了什么,为什么第五天要换打法
第一天到第二天,我把基础语法过了一遍。说“过了一遍”其实不太准确,应该是“反复看加反复敲”。变量声明、整型和浮点型的区别、取模运算的坑、scanf里忘了写&导致程序崩溃这种经典错误,我全踩了一遍。第三天开始接触分支和循环,if的嵌套我还能硬写,一到for循环的边界条件就开始犯迷糊,经常少循环一轮或者多循环一轮。第四天我把循环里的break、continue、goto(其实goto很忌讳,但我还是好奇试了一把)理了一遍,与此同时开始了第一个“像样”的小程序:从控制台读五个数,求平均值。
但这个写法非常笨。比如我写的是声明五个变量,然后挨个scanf进去,再挨个相加。这代码又臭又长,连我自己看着都难受。第五天我醒悟了:我得用数组来干这事。而当我想把“求平均值”这种逻辑抽出来反复用,我就得用函数。所以第五天的核心任务很清晰:数组和函数,外加一点指针的“预习”——因为函数参数传递和数组名本身都牵扯到指针,逃不掉。
1.2 秋招视角:C语言笔试到底在筛什么
我去翻了近几年的秋招笔试题型回忆帖,发现一个规律:真正的压轴难题往往考算法和数据结构,但“能不能进面试”的敲门砖,反而是最基础的知识点。变量的内存分布、数组下标的边界、函数参数的传值还是传址、字符串的结尾标志干、指针的悬挂和野指针,这些都是选择题和填空题的高频考点。机试更直接,要么让你手写一个排序,要么让你实现一个字符串处理工具,再不就是让你用递归解决某个问题。这些东西,靠前四天是打不下来的,但从第五天的数组和函数开始,我算是正式踏上了这条正轨。
2. 数组:一切数据结构的地基
数组在很多教材里是放在挺后面的,但对急着秋招的人来说,数组反而应该提前啃下来。因为你后面学排序、学查找、学链表,都是用数组来搭台子的。理解了数组,你才真正开始和“内存里的连续空间”打交道。
2.1 一维数组:从定义到边界
数组的定义很简单,类型 + 数组名 + 方括号里的元素个数。比如int nums[5];意味着你向内存申请了五个连续的整型空间。这里我想特别强调两件事。
第一,下标从0开始。第五天我第一次开开心心写了for (i = 1; i <= 5; i++)然后去访问nums[5],结果程序崩溃了。原因就是:数组nums[5]的合法下标是0到4,访问nums[5]属于越界,行为是未定义的。编译器不一定报错,但运行时就可能踩到内存里不该踩的地方,具体表现可以是值莫名其妙变成垃圾值,也可以是直接段错误退出。
第二,初始化。C语言的数组不会自动清零,声明完如果不初始化,里面是什么东西,完全取决于那片内存之前装过什么。我第五天犯了个低级错误:声明了数组就直接求和,结果每次运行出来的数都不一样,查了半天才意识到没清零。稳妥做法是显式清零:int nums[5] = {0};或者用循环赋初值。这一点在机试里尤其致命,因为评测跑出来的错误结果会让你怀疑人生。
写一个最简单的遍历示例,把数组从0到4打印出来:
#include <stdio.h> int main(void) { int nums[5] = {10, 20, 30, 40, 50}; for (int i = 0; i < 5; i++) { printf("nums[%d] = %d\n", i, nums[i]); } return 0; }这段代码本身不难,难的是你要能条件反射般说出“为什么是i < 5而不是i <= 5”。秋招面试官最喜欢沿着你的代码问边界条件,你答不上来,印象分会大打折扣。
2.2 二维数组:内存连续性的威力
二维数组看起来像一个有行和列的表格,比如int matrix[3][4];有3行4列。但在内存里,它其实仍然是一段连续的空间,按行优先存放:第0行的4个元素排完,才开始第1行。你可以把它理解成一个“数组的数组”:matrix[i]本身是第i行的一维数组,matrix[i][j]才是具体的元素。
这种连续性带来一个很有意思的写法:你可以用一个一重循环遍历整个二维数组,只要把行和列的关系映射成线性下标就行。我第五天练习时写了这样一个程序,把3行4列的元素赋值成行列号的组合,再打印出来:
#include <stdio.h> int main(void) { int matrix[3][4]; // 按行优先填充 for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { matrix[i][j] = i * 4 + j; } } // 打印二维数组 for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { printf("%2d ", matrix[i][j]); } putchar('\n'); } return 0; }运行结果会是一个3行4列的矩阵,每个元素刚好就是它在内存里的相对序号。这个过程帮我理解了“多维数组其实是一维数组的包装”,这个理解在后面学指针、学动态内存分配时特别管用。
2.3 数组最容易丢分的三个点
第一,数组作为函数参数的时候会“退化”成指针。你在子函数里用sizeof(arr) / sizeof(arr[0])想算数组长度,算出来的往往是错的值,因为此时arr已经是指针而不是数组了。第二个坑是数组初始化时不给大小,比如char str[] = "hello";这种写法会由编译器根据字符串字面量自动确定长度,但长度是6,不是5,因为末尾还有一个隐藏的\0。第三个坑是下标越界读,越界读可能不会立刻报错,而是返回一个随机值,这种情况在笔试选择题里出现最多,专门用来考察你对边界条件的敏感度。
3. 函数:从写代码到写“建筑”
没有函数的代码,就像把所有家具堆在一个大房间里,看着也还行,但完全没有章法。函数的本质是“把一个重复出现的逻辑封装成一个积木块”,你可以反复调用,也可以在别人写的代码里直接调用,不用关心内部细节。这个思想贯穿整个软件开发流程,秋招笔试里考函数也不是考你能不能写出来,而是考你能不能合理地拆。
3.1 函数声明、定义、调用,三者的分工
很多初学者会懵:为什么明明写了个函数,却还要在前面声明一遍?原因很简单:编译器是按顺序读代码的,它在遇到调用处时,必须已经知道这个函数的签名(返回类型、参数类型、参数个数),否则不知道如何传递参数和接收返回值。如果你把函数定义写在调用之后,那么就应该在调用之前加一道声明,声明像“预告片”,定义像“正片”。
我第五天写了一个两数相加的函数:
#include <stdio.h> // 函数声明 int add(int a, int b); int main(void) { int result = add(3, 5); printf("3 + 5 = %d\n", result); return 0; } // 函数定义 int add(int a, int b) { return a + b; }无论输出还是逻辑都很好理解。但我在实际练习里遇到的真正问题是变量作用域。函数内部定义的变量是局部变量,函数结束就被回收;函数外部定义的变量是全局变量,整个程序都可见。秋招机试里经常让你不用全局变量完成一个功能,考的就是你有没有“数据该从哪里来、结果该往哪里去”的模块化意识。
3.2 值传递与地址传递:第五天必须跳过去的坎
这是秋招的高频考点,也是第五天的重头戏。先看一段经典的错误代码:
#include <stdio.h> void swap_wrong(int a, int b) { int temp = a; a = b; b = temp; } int main(void) { int x = 3, y = 5; swap_wrong(x, y); printf("x = %d, y = %d\n", x, y); return 0; }运行完你会发现 x 和 y 根本没变,还是3和5。原因是:C语言的函数参数默认是“值传递”,你把 x 和 y 的值复制了一份交给函数形参 a 和 b,函数内部交换的只是副本,原件纹丝不动。想真正交换主调函数里的变量,必须传入它们的地址,也就是用指针接收:
void swap_right(int *a, int *b) { int temp = *a; *a = *b; *b = temp; }然后在主调函数里写swap_right(&x, &y);。这个细节我第五天反复验证了三遍才彻底想明白。值传递和地址传递的区别,本质上就是“我把你的名字抄在一张纸条上”和“我直接走到你本人面前”的区别。很多笔试题会让你判断程序输出,考的就是这个点。
3.3 递归:先会用,再深挖
递归这个概念,初看特别绕,但第五天我已经开始接触了。一个函数调用自己,只要注意两件事:必须有递归出口,否则会无限循环直到栈溢出;问题规模要在逐步缩小,否则递归毫无意义。
拿经典斐波那契数列举例:
int fib(int n) { if (n <= 1) { return n; } return fib(n - 1) + fib(n - 2); }这个写法相当简洁,但要理解它的执行过程并不简单。我第五天逼着自己手动在草稿纸上画了一遍fib(5)的调用树,画完以后发现递归的核心其实就是“把大问题拆成两个小问题,直到小问题可以直接回答”。秋招机试里经常出现“用递归实现字符串反转”“用递归计算最大公约数”,它们都不难,但需要你有这个拆解思路。
4. 指针初探:第五天晚上我啃了这块硬骨头
第五天晚上,我终于翻到了指针这一章。坦白说,指针被很多人渲染成C语言里最吓人的东西,但它本质上就是把“地址”这个值存下来,然后告诉编译器“你去这个地址找我要的数据”。我在第二天学scanf时就已经和地址打交道了:scanf("%d", &num)里的&num就是变量num在内存中的地址。所以指针并没有那么玄乎,它是“存放地址的变量”。
4.1 指针到底是个什么东西
声明一个指针变量,要在类型后面加个星号:int *p;,意思是“p是一个指针,它指向一个整型变量”。取地址用&,根据地址取内容用*。我第五天用一个极简单的程序验证这种关系:
#include <stdio.h> int main(void) { int num = 42; int *p = # printf("num 的值: %d\n", num); printf("num 的地址: %p\n", (void *)&num); printf("p 存储的地址: %p\n", (void *)p); printf("p 解引用得到的值: %d\n", *p); return 0; }如果你把*p想成“p指向的那个东西”,那么*p = 100;就等价于直接修改num的值为100。明白了这个,前面3.2节的swap函数就彻底通了:形参接收的是地址,函数内部通过*a和*b直接操作主调函数里的原变量。
4.2 指针和数组之间的那点关系
数组名在绝大多数表达式中会被当作指向首元素的指针。比如int arr[5] = {1,2,3,4,5}; int *p = arr;这里的arr等价于&arr[0]。所以访问数组元素除了arr[i],还能写成*(p + i)。后者看起来麻烦,但它体现了指针运算的本质:p加1,不是地址值加1,而是地址值加sizeof(int)个字节,跳到下一个元素。
我在第五天练习了用指针遍历数组:
#include <stdio.h> int main(void) { int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; for (int i = 0; i < 5; i++) { printf("arr[%d] = %d, *(p + %d) = %d\n", i, arr[i], i, *(p + i)); } return 0; }这里的坑在于,sizeof(arr)在main里是20(4字节乘5个元素),但一旦你把arr作为实参传给函数,在函数内部sizeof(arr)会变成8(64位系统指针大小),因为数组名已经退化成指针了。这个性质我在前面2.3节提过,但它和指针是同一根线上的两个点,要放一起理解。
4.3 指针初学的避坑指南
指针空悬和野指针是初学者最容易犯的错误。声明了指针却不初始化,它的值是个随机地址,解引用它非常危险。稳妥习惯是:声明时就给int *p = NULL;并养成判断if (p != NULL)的习惯。另一个坑是函数返回局部变量的地址,比如你写一个函数返回int *,却返回了函数内部局部变量的地址,这个局部变量在函数返回时就失效了,地址还在,内容却已经变成垃圾值。
第五天我还踩了一个很隐蔽的坑:把char *str = "hello";和char str[] = "hello";搞混。前者是一个字符串字面量,指向常量区,不能修改内容;后者是字符数组,存放在栈上,可以逐字符修改。坑就坑在两者大多情况下都能用,但一旦对前者执行str[0] = 'H';,轻则崩溃,重则未定义行为。秋招选择题里经常给你一段类似代码,问输出或者问是否报错,我建议你把这两种声明的内存区别记得滚瓜烂熟。
5. 秋招笔试专用:把知识点刷成题型
学知识是输入,做题是输出。第五天我意识到:光看教程不刷题,等于健身只吃饭不撸铁。秋招的笔试题型其实很固定,我按“用第五天能掌握的知识”就能做的目标,给自己筛了三个方向。
5.1 高频基础题:排序与遍历
排序题几乎是机试标配。我第五天能写出来的最经典的就是冒泡排序。它并不高级,但胜在实现简单、逻辑清晰,适合作为第一个“真正完整的算法”。我给自己的要求是:不看课本,手写出来并解释每一行代码在干嘛。
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 temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } }为什么内层循环要写成j < n - 1 - i?因为第 i 趟排序结束后,末尾的 i 个元素已经有序,不需要再比较。如果我把这个判断条件写错,数组依然能排,但会多做很多无用功。秋招面试官如果问“这段代码的复杂度”,我得能说出平均和最坏都是 O(n²) 的平方级复杂度。
5.2 高频算法题:用第五天的知识能做的
字符串逆序、求最大公约数、判断素数、求斐波那契数列第n项,这些都能用数组、函数和递归解决。我第五天给自己安排了三道题:
第一题,字符串逆序。用两个下标,一个从头走,一个从尾走,交换字符直到相遇。题目本身不难,但要注意字符串末尾的\0不能参与交换,否则你就把结束符换到前面去了,程序输出会变得乱七八糟。
第二题,最大公约数,用辗转相除法:gcd(a, b) = gcd(b, a % b),直到余数为0。这个写法天然适合递归,而且只需要一句核心逻辑。
第三题,约瑟夫环或者二分查找。二分查找是数组与指针的绝佳练习,因为它必须操作下标或者指针游走,而且极其考验边界条件。我给自己立了一个规矩:以后每学一个知识点,就找一道能用到它的题去做,拒绝“学过就完”。
5.3 机试环境适应要点
说了这么多知识点,最后还得落到环境上。我第五天专门花时间适应了牛客和某平台的在线编译器环境,因为手写代码和在线编辑器差别很大,最典型的就是头文件要不要自己加、main函数返回值要不要写、变量声明要不要放在块开头。我在本地编译器写好能跑的代码,贴到在线评测里却发现编译报错,多半原因是把#include漏了,或者用的编译标准不一致。建议你从第五天开始,把所有练习都放到模拟机试的环境里进行,特别是要习惯“没有任何教学提示、编译信息全靠自己看”的状态。
6. 菜鸟踩坑实录:第5天我的错误清单
第五天我写了大概二十个小程序,编译错和运行错加起来,比前四天还多。但这些错恰恰是我进步最快的时刻。我把几个代表性的错误列出来,希望能帮大家少走弯路。
6.1 编译运行阶段
我犯了三个很典型的编译阶段错误。第一个是函数声明和定义不匹配,参数类型写错,导致调用时报“函数冲突”或者隐式声明警告。第二个是少写了头文件,比如使用strlen时忘了#include <string.h>,编译器就给你一个隐式声明警告,运行结果直接乱套。第三个是变量名拼写不一致,这个特别容易发生在复制粘贴代码后,比如前面定义nums,后面写成了num,编译报错,但肉眼就是很难看出来。
我现在的习惯是:编译报错先看第一行提示,不要急着改代码。找到出错文件和行号,再往上看看是不是变量未定义、类型不匹配,还是漏了分号。如果编译器警告里出现implicit declaration,八成就是头文件的问题。
6.2 逻辑错误
比起编译错误,逻辑错误更隐蔽。第五天我写了一个统计“数组里有多少个正数”的程序,运行结果总是差1。后来我定位到原因:我用了for (i = 0; i <= n; i++),多循环了一次,把数组外面的垃圾值也算进去了。这个错误和2.1节的越界本质上是一回事。
再有就是数组作为参数退化成指针后,我在子函数里直接用sizeof(arr)当长度用,结果长度变成8。要解决可以传入长度参数,或者干脆在子函数里用指针遍历,但无论如何,我都得牢牢记住:数组长度要在定义它的作用域里算好,而不是靠被子函数里的sizeof侥幸。
6.3 心态和节奏
第五天我还想分享一个技术之外的东西:节奏。我几次因为一个小错误卡住一个多小时,疯狂改代码,越改越烦躁。后来我发现,卡住的时候最有效的做法不是硬刚,而是先把问题写下来,用自然语言描述“我期望程序做什么”和“程序实际做了什么”,然后一行一行读代码,或者先放一放去写另一个小功能,让脑子切换上下文。这个策略听上去很“软”,但在第五天这种知识点快速累积的阶段,非常解压且高效。
另外,秋招是一个长线冲刺,第五天只是万里长征的起点。我看到很多前辈的经验贴里都会强调“不要追求一天学完所有东西”,我自己也体会到,每天把两到三个核心知识点真正弄懂,比一口气翻十页书有用得多。第五天我学到的最重要的东西,其实不是数组和函数本身,而是我发现自己开始有了“用程序解决问题的意识”——看到一堆数字,会想怎么用数组装;看到重复的逻辑,会想怎么抽成函数;看到交换两个数,会想值传递到底够不够用。
我给自己定的计划是:后面每个晚上,集中做三道易错题,整理一段笔记,再用十分钟把自己今天写的代码重新读一遍。再过几天,我打算开始刷一套完整的模拟真题,看看第五天的积累到底能不能扛住真实秋招题目的冲击。如果你也在自学的路上,别太着急,C语言的知识点是有网状的,你等第五天打下的这些结,会替你系住后面更多的知识线头。