这段时间一直在做两件事:刷C语言基础练习,以及每天固定啃一点计算机英语。目前C语言练习做到第18期,英语词汇也到了第12天,两个进度叠在一起,反而让我发现了一些单刷任何一个都体会不到的东西——写代码卡壳的地方,回头看英文原版定义往往一下就通了;而背单词时遇到的术语,敲代码时又成了天然的“记忆锚点”。
这篇就把这两条线放在一起聊一聊。C语言部分,我会挑这期练习里最典型的几道题,说说思路、对比写法、填上那些教科书里不讲但实际敲代码才会踩的坑;计算机英语部分,则分享一下这12天里我筛选出来的高频术语,以及怎么把它们真正用起来,而不是停在“眼熟”层面。适合正在系统学C语言、同时又想提升英文文档阅读能力的初学者参考,也欢迎已经入行的朋友一起纠正补充。
1. 内容整体设计与思路拆解
1.1 为什么把编程练习和英语学习放在同一条线
说白了,编程语言和自然语言都是“语言”。写C语言代码是在跟编译器沟通,读英文文档是在跟全世界的开发者沟通,两者共用同一套底层能力:语法识别、语义理解、模式匹配。我自己的体会是,代码写多了以后,再看英文文档不会先“翻译成中文再理解”,而是直接以英文的逻辑去理解那个概念,速度提升非常明显。
具体到C语言,它的正统教材、标准文档、开源项目注释基本都以英文为主。比如你查malloc,中文资料总喜欢翻译成“动态内存分配”,听起来高大上,但英文原意是“memory allocation”,memory是内存,allocate是分配,拆开一看就特别直白。再比如while这个词本身就是“当……时”的意思,do-while就是“先做,当……时继续”,把英文含义搞清楚了,两者的区别就再也忘不掉。
所以这一阶段的规划是:每天刷3到5道C语言基础题,同时每天积累10到15个计算机英语词汇。做题时遇到的关键字、函数名、报错信息,我会顺手抄进英语本里;背单词时遇到的概念,我会要求自己立刻回忆起对应的C语言用法。两条线互相喂,效率比单线高很多。
1.2 第18期练习的选题逻辑
这一期的练习范围集中在几个方向:while与do-while的对比使用、字符串处理、数组排序、简单文件读写、以及一些涉及日期运算的数学逻辑题。这些内容在C语言知识体系里属于“承上启下”的位置——前面是变量、运算符、分支结构,后面是指针、结构体、动态内存。如果这一层的地基不稳,后面的链表、树、文件系统接触起来会非常吃力。
为什么要专门挑这些题?因为它们考察的从来不是“会不会背语法”,而是“能不能把语法用对场景”。举一个这期练习里的例子:计算某年某月某日是当年的第几天。看起来只是switch-case的堆叠,但真正优化起来,可以用数组预处理每月天数、用函数封装闰年判断,还牵扯到static数组和const修饰符的选择。说实话,能把这道小题写出三种以上版本的人,数组和函数这两个章节基本没问题了。
另外,这期练习里我也刻意加入了文件读写的内容,因为很多初学者学到文件操作就卡住。原因倒不难理解——文件操作涉及FILE*指针、打开模式、缓冲区刷新等一堆新概念,抽象程度比printf高一个台阶。再加上文本文件在Windows和Linux下的换行符不同(\r\n和\n的问题),光是把“读一行字符串”跑通,就够研究半天。这些我都会在后文详细拆。
2. 核心细节解析与实操要点
2.1 while 与 do-while:一个先判断,一个先执行
这个知识点几乎是每套C语言基础题里必出现的,但真正能说明白的人不多。简单说:
while先判断条件,条件为真才执行循环体,所以循环体可能一次都不执行。do-while先执行一次循环体,再判断条件,所以循环体至少执行一次。
听上去很简单,但实际写代码时,很多人习惯性地都用while,结果在“至少执行一次”的场景里被迫在循环前重复写一遍逻辑。比如要把用户输入的数字累加,直到输入0结束,用while就得先读一次再进循环,而do-while就自然得多:
#include <stdio.h> int main(void) { int num, sum = 0; do { printf("请输入一个数字(输入0结束): "); scanf("%d", &num); sum += num; } while (num != 0); printf("总和 = %d\n", sum); return 0; }这段代码天然契合“先做一次再判断”的业务逻辑。反之,如果需要“当条件满足时才执行”,比如遍历链表直到指针为空,那while才是正确的选择。选哪种循环,本质上不是在背语法,而是在理解业务逻辑的执行时机。
有一个细节很多人忽略了:do-while的while(...)后面必须加分号。这个分号非常容易漏,一漏就是编译错误。初学者经常花十分钟找bug,结果发现就是少写一个分号。我自己的习惯是写完do-while立刻检查一遍分号——这个习惯救过我很多次。
2.2 指针、取地址符与内存管理的基础认知
这一节是C语言的“劝退点”,也是“分水岭”。第18期练习里已经开始涉及简单的指针操作,比如用指针遍历数组、用指针交换两个变量的值。很多初学者在这里最大的困惑是:&和*到底是啥关系?
换算成生活场景来理解:假设内存是一条巨大的街道,每个房间(内存单元)都有门牌号(地址)。变量的名字就是贴在房间上的便签,而&变量名相当于查这个房间的门牌号,*指针变量则是拿着门牌号找到房间、打开门看看里面放了什么。所以:
int a = 5;相当于在某个房间放了数字5int *p = &a;相当于记下了a房间的门牌号*p = 10;相当于按门牌号找到房间,把里面的5改成10
指针的难点不在语法,而在“间接”这个思维方式。平时我们直接操作变量,指针则是在变量外面又包了一层“门牌号”。初学者最容易犯的错是混淆p、&a、*p三者的含义:p存的是门牌号,&a是取门牌号,*p是门牌号对应房间里的值。这三者的类型也完全不一样,用错就是编译警告甚至程序崩溃。
关于内存管理,这一期暂时只要求理解栈区和全局区的基本概念,先不深入malloc和free。但我建议有这个意识:全局变量和static局部变量放在静态存储区,程序启动时就分配好了;普通局部变量放在栈区,函数调用结束就自动释放。后面学到动态内存分配时,这个概念会反复用上。
2.3 字符串函数与文件读写:高频工具函数盘点
C语言没有原生的“字符串”类型,只有字符数组加\0结尾。这个设计导致字符串处理比高级语言麻烦,但也因此更透明。这期练习里我重点用了几个函数:
strlen—— 返回字符串长度,不含\0strcmp—— 比较两个字符串,返回0表示相等strcpy—— 把一个字符串复制到另一个数组strcat—— 把一个字符串拼接到另一个后面
这四个函数在使用时有个通病:都不检查目标缓冲区大小。strcpy和strcat如果目标数组不够大,就会越界写入,轻则数据错乱,重则程序崩溃。安全一点的写法是用strncpy和strncat,指定最大复制长度,但这两个函数又有“不自动补\0”的坑。C语言就是这么朴实无华——每个工具都有代价,理解代价比记住函数名重要。
文件读写部分,这期练习我写了一个“统计文本文件行数”的小程序。核心就三步:fopen打开文件、循环读字符数换行、fclose关闭文件。但中间有个细节让我踩了坑:Windows下文本文件换行是两个字符\r\n,直接按\n统计会把\r漏掉,某些情况下统计结果会偏差。解决方法是把文件以文本模式打开(默认就是),C标准库会把\r\n转成\n处理,但如果你用二进制模式rb打开,就要自己处理换行符。这个问题在Linux下不存在,但跨平台时一定要留意。
3. 实操过程与核心环节实现
3.1 字符串逆序:从思路到代码
这道题是这期练习里比较经典的一道:给定一个字符串,把它的字符顺序颠倒过来输出。有不少新手第一反应是“从后往前遍历打印”,这确实是解法之一:
#include <stdio.h> #include <string.h> int main(void) { char str[100]; printf("请输入一个字符串: "); fgets(str, sizeof(str), stdin); str[strcspn(str, "\n")] = '\0'; // 去掉fgets带入的换行符 int len = strlen(str); for (int i = len - 1; i >= 0; i--) { putchar(str[i]); } putchar('\n'); return 0; }如果要求“原地逆序”,也就是不借助额外数组,那就用双指针法:一个指针指向开头,一个指向结尾,交换两个位置的字符,然后向中间移动。这里要注意的是fgets会读取换行符,直接strlen会把它也算进去,所以用strcspn把换行符替换成\0是很实用的小技巧。这个细节在很多网上的题解里都不会讲,但实际敲代码时一定会碰到。
原地逆序的代码长这样:
#include <stdio.h> #include <string.h> void reverse(char *s) { int left = 0; int right = strlen(s) - 1; while (left < right) { char tmp = s[left]; s[left] = s[right]; s[right] = tmp; left++; right--; } } int main(void) { char str[100]; printf("请输入一个字符串: "); fgets(str, sizeof(str), stdin); str[strcspn(str, "\n")] = '\0'; reverse(str); printf("逆序结果: %s\n", str); return 0; }这里就体现了一个英语词汇的重要性:reverse本身就是“反转、颠倒”的意思,看到函数名就懂函数功能。很多教材里的示例代码喜欢用f、g这类无意义命名,实际开发中如果都用reverse、swap、merge这类“动词化”命名,代码会好读很多。
3.2 日期计算:两种优化方法对比
题目:输入年、月、日,输出这是当年的第几天。最“老实”的写法是switch-case累加每个月的天数,再根据闰年调整二月。但既然学了数组和函数,完全可以写得更漂亮。
方法一:用数组预处理每月天数。
#include <stdio.h> int isLeap(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } int main(void) { int year, month, day; printf("请输入年 月 日: "); scanf("%d %d %d", &year, &month, &day); int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(year)) { days[1] = 29; } int total = 0; for (int i = 0; i < month - 1; i++) { total += days[i]; } total += day; printf("%d年%d月%d日是当年的第%d天\n", year, month, day, total); return 0; }方法二:用static数组避免每次调用都重新初始化。如果这段逻辑封装成函数,每次调用都重新定义并填充days数组,虽然开销不大,但写成static const int days[]可以明确告诉编译器这是只读数据、放在静态存储区,既清晰又高效。
日期题的核心难点不在语法,而在“闰年判断”的边界条件。判断闰年的规则是“能被4整除但不能被100整除,或者能被400整除”,这个规则很容易记错。有个记忆技巧:400年是一个大周期,所以能被400整除的一定是闰年;能被100整除但不能被400整除的不是闰年;其余能被4整除的就是闰年。以后写任何与日期相关的工具,这个判断务必抄对。
3.3 冒泡排序:交换次数与边界控制
冒泡排序是算法入门第一课。这个排序之所以叫“冒泡”,是因为每轮比较都会把当前最大的数“冒泡”到数组末尾,像水里的气泡往上升一样。写下最基础的版本不难,难的是理解每一轮循环的边界是怎么推出来的。
#include <stdio.h> void bubbleSort(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; } } } } int main(void) { int arr[] = {5, 2, 9, 1, 5, 6}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, n); for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }很多初学者会疑惑:为什么外层循环是n - 1?为什么内层循环是n - 1 - i?原因很简单:n个数最多需要n - 1轮排序,因为每一轮至少能确定一个数的最终位置,排完n-1个,剩下的自然就位;而每轮结束后,末尾的i个数已经是排好的,无需再参与比较,所以内层比较次数要减去i。
进阶一点,可以在每轮开始前加一个“是否发生交换”的标记,如果一轮下来没有任何交换,说明序列已经有序,直接跳出循环。这个优化叫“提前终止”,在处理已经接近有序的数据时能省不少时间。这类优化思想比背排序代码更具迁移价值——以后写任何循环,都应该问一句“有没有可能提前结束?能不能减少无效计算?”
4. 计算机英语Day12:把术语变成肌肉记忆
4.1 这12天里最有价值的几组术语
每天记十几个计算机英语词汇,坚持12天差不多积累了150个左右。这150个里,我筛选出几组真正高价值的,它们几乎每天写代码都会遇到:
declare/definition:声明 / 定义。C语言里,声明只是告诉编译器“有这么个东西”,定义才是真正分配内存。这两个词搞不清,理解头文件机制会一直吃力。allocate/release:分配 / 释放。对应malloc/free,也对应内存的两个状态,allocate是拿到,release是还回去。iterate/recursion:迭代 / 递归。循环是迭代,函数调用自己是递归。两个词在算法讨论里高频出现。buffer/overflow:缓冲区 / 溢出。buffer是临时存放数据的区域,overflow是数据放不下溢出来了。缓冲区溢出是C语言安全问题的头号来源。compile/link:编译 / 链接。C语言从源码到可执行文件需要经过编译和链接两个阶段,undefined reference这类错误就是链接阶段的经典报错。pointer/reference:指针 / 引用。C语言只有指针,C++才有引用,但如果英文资料里出现dereference(解引用),对应的就是*p这个操作。
这些词有个共同特点:它们描述的是动作和状态,而不是名词本身。比如iterate是“迭代”,动词,强调的是“重复执行”的过程;overflow是“溢出”,名词/动词兼有,强调的是“超过边界”的状态。把这类词当成动词记,再去读代码里的注释和报错信息,会顺畅很多。
4.2 怎么把英语文档反哺到C语言理解
背单词不是目的,能读文档能排查报错才是目的。我给自己定了个规矩:遇到不懂的C语言概念,先查英文资料,再对照中文资料理解。英文写得不复杂,但很多细节表述得非常精确。比如说undefined behavior——中文经常翻译成“未定义行为”,听起来很学术,英文原文是“the behavior is not defined in the standard”,意思是“标准没规定会发生什么”,所以编译器想干嘛就干嘛。这个英文表述直接揭示了为什么不要依赖未定义行为——因为你根本不知道程序会怎样表现。
另一个例子是sequence point(序列点)。C语言里i++ + i++是危险的,就是因为中间没有序列点。中文资料讲这个知识点总是云里雾里,但英文里 “sequence point is a point in the execution where all previous evaluations are complete and all side effects are done” 直接说明了“一个确保前面所有操作都完成、副作用都生效的时间点”,一下就清楚了。
所以我的建议是:每天背的词汇,不要孤立地背拼写和中文意思,一定要跟C语言的语法点绑定。比如背到static这个词时,同时回顾它在C语言里的三个作用:修饰局部变量使其生命周期延长、修饰全局变量使其仅在本文件可见、修饰函数同理。这样每背一个词,等于复习了C语言的一个知识点,一天10个词就是10个知识点,一个月下来积累量会很可观。
5. 环境配置与常见问题排坑实录
5.1 开发环境的选择:VSCode搭配C/C++插件的细节
很多初学C语言的人第一个问题就是“用什么软件写代码”。目前主流的方案有Visual Studio、Dev-C++、Code::Blocks、VSCode等。我个人更推荐VSCode,原因有三个:轻量、插件生态强、跨平台一致性好。但VSCode本身只是一个编辑器,要让它能编译运行C语言,还需要安装C/C++扩展和一个编译器(Windows下推荐MinGW-w64,macOS用自带的clang,Linux用gcc)。
我在配置过程中踩过的最大坑是:装完插件后依然找不到编译器路径。原因通常是MinGW的bin目录没有加到系统环境变量PATH里,或者VSCode没有重启识别新环境变量。解决方法是手动打开终端输入gcc --version,如果能输出版本信息说明 PATH 没问题,再重启VSCode即可。
还有一个小坑是任务配置文件tasks.json的写法。如果直接从Run菜单选Run Without Debugging,VSCode会默认使用上一次的任务配置,首次使用需要手动生成tasks.json,指定args里至少要包含源文件名和-o输出文件名。很多教程会写固定的文件名,比如main.c,但实际项目里我经常同时打开多个源文件,所以更推荐在args里用${file}和${fileDirname}/${fileBasenameNoExtension}.exe这样的变量,自适应当前文件。
5.2 新手最容易踩的5个编译/运行问题
这期练习期间,我在学习群里帮人排查了不少问题,挑几个频率最高的整理成表格:
| 问题现象 | 根本原因 | 快速排查方法 |
|---|---|---|
编译报错fatal error: xxx.h: No such file or directory | 头文件路径不对,或者头文件本身不存在 | 检查#include里的文件名和项目目录是否一致 |
编译报错undefined reference to 'xxx' | 编译和链接分离,缺少对应函数实现 | 确认是否把所有.c文件一起编译 |
| 程序运行闪退,什么也不输出 | 可能是数组越界、指针未初始化导致段错误 | 用printf在关键位置打印调试信息,定位崩溃点 |
用fgets读字符串时,程序跳过了后续输入 | fgets把换行符也读进去了,影响后续scanf | 在fgets后加str[strcspn(str, "\n")] = '\0'; |
double类型取余报错 | C语言的%只能用于整数,浮点取余要用fmod | 使用<math.h>里的fmod,编译时加-lm |
顺手展开说两点。double取余这个问题,我在网上看到不少新手问“为什么5.2 % 2报错”,其实是C语言规范里%运算符只接受整数操作数,浮点数取余需要fmod函数,而fmod的返回值和整数取余的符号规则还不太一样,使用时需要额外留意。另一个是 C语言里&符号的双重身份,一个作为“取地址”运算符,比如int *p = &a;,另一个作为“按位与”运算符,比如5 & 3结果是1。这两个用法初学者容易混,口诀是“左边有类型就是取地址,左右都是值就是按位与”。
还有一个非常隐蔽的坑:scanf的返回值。很多人写完scanf("%d", &num);之后就默认读成功了,但实际上如果用户输入了非数字内容,scanf会返回0,num的值保持不变。如果程序后续用这个num做运算,结果就是错的。正确处理方式是检查返回值,或者在循环里把输入清理掉再重新读。这一点在刷题网站的多组输入场景里尤其重要,用例一多,输入不规范是常态,你不检查返回值就等着 “Wrong Answer” 吧。
6. 关于持续练习的一点心得
第18期练习和第12天英语,听起来都只是漫长学习曲线里不起眼的一小段,但真正走下来你会发现,每天固定学习两小时比周末猛学十小时有效得多。C语言的知识点很多都是环环相扣的,比如这期练习里“字符串逆序”用到的双指针思想,后面学链表反转、快排划分时还会遇到;而“日期计算”里对闰年的判断,几乎是所有时间处理库的基础。
我个人在实际操作中的体会是:练习不止是把题做对,还要留意“有没有更优的写法”、“有没有更安全的写法”、“这个函数名到底怎么读”。把这些习惯固化下来,收获会比刷一百道题还大。至于计算机英语,不用一开始就死磕原版大部头,从每天的报错信息开始读起,再慢慢扩展到man手册、官方文档,一步步来,等到某天你发现自己不再依赖翻译工具时,就已经进入新的境界了。
最后再分享一个小技巧:每次做完一道题,花30秒在代码旁边用英文注释写一句“这道题的关键点”。比如// This loop stops when left and right meet.这样既复习了英语写作,又把代码逻辑重新梳理了一遍,一举两得。这个习惯我还在坚持,目前看来效果不错,你可以试试看。