今天是我做C课后习题训练的第85天。三个多月坚持下来最大的感受不是“我变强了”这种虚话而是面对一道题的时候脑子里会自然浮现出考点对应的实现路径哪怕第一遍写不对也知道该往哪个方向调。这个状态和第一天相比差得太远了。第85天记录的核心内容包括多维数组与指针的辨析、快速幂取模、单调栈模板题外加几道典型的入门练习题。这篇文章不打算写成一套完整的教程而是把这一天从读题、写码到提交、查错的全过程摊开聊聊哪些地方容易卡壳哪些坑值得后来人绕开。如果你也在学C、刷OJ或者准备笔试里的算法题这篇记录应该能给你一些参考。1. 训练第85天的规划与选题思路1.1 为什么到第85天还在做“课后习题”很多朋友听到“课后习题”四个字第一反应是这是新手才做的事。但我的看法不太一样。C这门语言语法细节多到离谱很多知识点上课听懂了、看书看懂了和真正在编译器里跑通完全是两回事。课后习题训练的价值就在于它会逼着你把“眼睛会了”变成“手会了”。到第85天这个阶段我做题的目标已经不再是为了学会某个语法而是为了验证对知识点的理解深度。比如多维数组和指针的关系书上一句话带过但真让你写一个函数去遍历二维数组并修改元素不少人会卡在参数类型上。这种细节光靠看是看不出来的只有亲手写过一遍、被编译器报过错才能真正记住。我的训练节奏也比较固定白天抽1小时做2到3道题晚上花20分钟看代码和编译报错周末做一次复盘。这个节奏不是一开始就有的是前30天试出来的。太猛了坚持不住太松了没有手感一天2到3题刚好能保持状态又不至于挤占正常生活。1.2 当天的题目清单与考点匹配Day85我挑了5道题题目来源是课程配套的OJ题库和几次笔试回忆题。选题思路很简单先扫一眼考点分布再挑自己最近薄弱的方向集中练。当天的题目和对应考点如下表所示题目核心考点对应热词二维数组按列求和多维数组、指针传参多维数组 c 指针快速幂取模二分思想、取模运算快速幂算法c每日温度变体单调栈、栈存储下标单调栈算法cn个整数的最小公倍数数论基础、gcdn个整数的最小公倍数怎么求c字符串数组初始化与输出字符串数组、边界处理c字符串数组初始化这5道题不是随便选的。二维数组那道题是为了巩固指针和数组的关系快速幂是为了强化二分思维单调栈是本周专项训练的重点最小公倍数那道题则是对数论基础的查漏补缺最后一道字符串题是我一直容易出小错的类型特意拿来练手感。2. 核心知识点拆解从Day85的题目反推C高频考点2.1 多维数组与指针被无数人绕晕的一组概念先说说二维数组按列求和那道题。这个题目本身不复杂一个二维数组让你对每一列求和输出结果。但问题是当这个数组作为函数参数传递的时候函数形参该怎么写新手容易写出这样的代码void sumByColumn(int** arr, int rows, int cols) { // ... }但传参的时候如果你传的是一个二维数组名编译器很可能会报错cannot convert int (*)[cols] to int**。原因在于二维数组名在表达式里会退化成指向第一个元素的指针而第一个元素是一个一维数组所以类型应该是“指向数组的指针”也就是int (*)[cols]而不是int**。我后来习惯用三种方式处理这种情况直接用固定列数void sumByColumn(int arr[][5], int rows)用指针数组void sumByColumn(int (*arr)[5], int rows)用一维指针加手动换算void sumByColumn(int* arr, int rows, int cols)然后通过arr[i * cols j]访问第三种方式最灵活适合列数不固定的情况。它的本质就是搞明白二维数组在内存中是按行连续存储的所以一个下标运算就能定位到任意元素。多维数组看似复杂拆到底就是“内存布局 指针运算”两个问题。注意使用int**来处理二维数组只有在传入的是指针数组即int* arr[]时才成立。严格来说它不是真正的二维数组而是数组的元素是指针。这两者容易混淆写代码前先想清楚你手上到底是哪种结构。2.2 快速幂取模递归与迭代两种写法对比快速幂那道题是当天最有价值的训练之一。题目大意是给定a、b、m求a的b次方对m取模的结果其中b的范围可以很大大到10^9级别。如果直接写循环累乘b次运算肯定超时。快速幂的核心思想是二分要求a^b可以分成两个a^(b/2)相乘。当b是偶数时a^b (a^(b/2))^2当b是奇数时a^b a * a^(b-1)。我最初写的是递归版本long long fastPow(long long a, long long b, long long m) { if (b 0) return 1 % m; long long half fastPow(a, b / 2, m); half half * half % m; if (b % 2 1) half half * a % m; return half; }这个写法思路很清晰但递归深度是O(log b)在b特别大的时候会有栈溢出风险所以我后来改用迭代版本long long fastPow(long long a, long long b, long long m) { long long res 1 % m; a % m; while (b 0) { if (b 1) res res * a % m; a a * a % m; b 1; } return res; }迭代版本的原理是把b写成二进制遍历每一位。如果当前位是1就把结果乘以当前的a每次循环把a平方对应二进制位权。时间复杂度同样是O(log b)但空间复杂度降到了O(1)实测大数据量下稳得多。关于取模一个关键结论是(a * b) % m ((a % m) * (b % m)) % m。所以可以在每一步乘完后都取一次模避免中间结果溢出。但注意如果m很大接近long long上限a * a这一步仍然可能溢出这时候需要用__int128临时存储或者用快速乘来规避。2.3 单调栈一类问题的统一解法单调栈这道题是“每日温度”的变体给定一个数组找每个元素右边第一个比它大的元素距离。直接暴力是O(n²)n超过10^5就吃力了。单调栈的思路是维护一个栈栈内元素保持单调递增或递减。以找右边第一个比当前元素大的距离为例我从右往左遍历数组维护一个从栈底到栈顶单调递减的栈存下标。每遇到一个新元素就把栈中所有比它小的元素弹出去栈顶元素就是右边第一个比它大的元素下标。vectorint dailyTemperatures(vectorint t) { int n t.size(); vectorint ans(n, 0); stackint st; for (int i n - 1; i 0; --i) { while (!st.empty() t[st.top()] t[i]) { st.pop(); } ans[i] st.empty() ? 0 : st.top() - i; st.push(i); } return ans; }第一次看这个模板可能会觉得抽象但它本质上就是一个“淘汰制”每个元素入栈之前检查栈顶元素是否比它小如果是说明这个栈顶元素对后续元素来说永远不可能是答案了可以放心丢掉。每个元素最多入栈一次、出栈一次总时间复杂度是O(n)。我做这道题的心得是不要死记模板而是先理解“为什么右边的更近元素会淘汰左边的更远元素”这个逻辑。理解了淘汰规则遇到变体题比如找左边第一个更大元素、滑动窗口最大值的时候才能灵活调整遍历方向和比较符号。3. 实操过程从读题到提交的完整记录3.1 环境与配置把vscode调成一个够用的C环境开始解题之前我先把环境确认了一遍。现在很多新人第一步就卡在环境配置上其实大可不必。我用的是vscode加MinGW-w64配合C插件跑课后习题完全够用。关键是配置文件要简洁别整一堆用不上的参数。我用的编译参数是g -stdc17 -O2 -Wall -o main.exe main.cpp在vscode里我直接在.vscode/tasks.json里配了一个编译任务快捷键CtrlShiftB触发生成main.exe然后再用终端手动运行。这样比每次敲一长串命令省事又不会像调试配置那样繁琐。{ version: 2.0.0, tasks: [ { label: build, type: shell, command: g, args: [-stdc17, -O2, -Wall, -o, main.exe, ${file}], group: { kind: build, isDefault: true } } ] }提示-Wall一定要加上它会把很多潜在问题变成警告提示出来。很多隐蔽bug在编译阶段就能暴露省去后面调试的时间。等代码写完要提交OJ时再把-Wall去掉就行。3.2 字符串输入与数组初始化的几个经典坑当天那道字符串相关的题目让我又把输入处理的坑踩了一遍。最典型的坑是cin s和getline(cin, s)的行为差异。cin s读到空白字符就停getline会一直读到换行符。如果两种方式混用中间残留的换行符会让getline读到一个空串。正确的做法是如果先用了cin n再想用getline读一整行中间要加一句cin.ignore()把缓冲区的换行符吃掉。这个问题几乎每届学生都会遇到写代码前先想清楚输入中到底有没有空格。字符串数组初始化也有讲究。常见的写法string s[10]; // 10个空字符串 string s[3] {a, bb, ccc}; char str[5] hello; // 错误长度不够hello需要6字节最后一行是我当年踩过的坑。C风格字符串的结尾要留一个位置给\0所以char str[5]根本装不下hello。这种错误编译器不一定报错运行时却可能悄悄越界反应到OJ上就是莫名其妙的runtime error。3.3 快读模板与运行时间优化技巧做快速幂那道题的时候我顺手把快读模板复习了一遍。虽然现在OJ的评测机配置普遍不差但遇到大数据量的输入cin和scanf的性能差距还是能体现出来的。最简单的优化是关掉C输入输出流和C标准库的同步ios::sync_with_stdio(false); cin.tie(0);加了这两行之后cin和cout的性能基本能追上scanf/printf。但要注意一旦关闭同步就不能再混用cin和stdin家族的输入函数了否则数据读取顺序可能出错。如果题目给的数据量特别夸张比如一行几百万个整数我会直接用手写快读int readInt() { int x 0, f 1; char c getchar(); while (c 0 || c 9) { if (c -) f -1; c getchar(); } while (c 0 c 9) { x x * 10 (c - 0); c getchar(); } return x * f; }这个模板的原理很朴素用getchar一个字符一个字符地读遇到数字就累加遇到非数字就继续跳过。它比cin快的原因在于省去了格式化解析的开销。但说实话90%的课后习题用不到快读掌握原理就够了别本末倒置。4. 当日踩坑与问题排查记录4.1 编译错误三连从报错信息里找线索Day85当天遇到了三个编译或运行错误我都记录下来了报错现象原因解决方案error: invalid conversion from int (*)[5] to int**函数形参类型不匹配改成int (*)[5]或手动做一维指针换算warning: unused variable定义了变量但没使用检查逻辑删掉多余变量segmentation fault数组越界或空指针解引用用gdb定位常见于动态规划数组开小了我的排查习惯是先读完整报错信息再定位到具体行号最后用输出中间变量的方式确认问题。很多人一看到segmentation fault就不知所措实际上只要在关键位置加几行cout reach here endl就能很快缩小问题范围。等代码稳定后再把调试输出删掉就行。4.2 一个隐蔽bug负数参与了求余运算最小公倍数那道题里我一开始写了个求最大公约数的函数然后通过a / gcd(a, b) * b来算最小公倍数。思路没问题但有一个隐藏风险如果输入里有负数%运算的结果在不同语言里的行为不一样。C中负数取模的结果符号和被除数一致比如-7 % 3的结果是-1而不是2。这个细节在很多场景下不是问题但一旦用错了地方就是选择题全对的代码、提交却全是WA的尴尬局面。我的处理方式很简单所有可能为负的变量在参与取模运算前先转到非负或者干脆在题目输入阶段就统一处理。做题前先看一眼数据范围如果题目保证非负就不需要额外操心如果没有保证一律先加偏移量转成正数。4.3 复盘环节为什么选择这些题目、如何沉淀每天训练结束我会花十分钟写一个简单的复盘内容包括每道题的思路摘要、写错的地方、最终优化方向。格式不需要复杂一个Markdown表格就够题目我的解法耗时错误点收获这个习惯坚持到第85天最大的变化是以前写错一道题过两周就忘得干干净净现在错过的点都会被记录在案隔一段时间翻出来看一眼下次再遇到类似问题第一反应就是“这个坑我见过”。错题集的价值不亚于刷题本身。5. 训练第85天后的调整方向5.1 从课后习题向实战方向过渡坚持到这个阶段课后习题的边际收益开始下降。语法和基础算法已经比较熟了但真要走向实际开发或更进阶的面试还需要补一些工程向的知识。我给自己规划的新方向是多线程编程理解std::thread、std::mutex的基本用法然后尝试写一个简单的线程池回调函数搞清楚回调在C里的实现方式包括函数指针、std::function和lambda表达式Linux下的UDP通信用socket写一个简单的收发消息程序理解网络编程的基本流程这些方向不是一时兴起。从面试角度看C岗的高频考点就是多线程、内存管理、网络编程这几块从项目实战角度看课后习题的题量积累能帮我快速读懂别人写的代码而不是对着开源项目一头雾水。5.2 给同样在坚持训练的朋友几点建议根据自己的实际经验我想分享三点第一训练记录不能只写“今天做了3道题”这种流水账要记录具体的技术点、错误和复现方法才有积累价值。第二不要排斥做“简单题”。简单题做多了基础会更扎实遇到复杂题的时候底层知识才不会掉链子。第三坚持比速度重要。第85天回头看真正让我进步的不是某一天爆刷10题而是每一天都保持编码的节奏。如果你也在这个阶段建议你先别急着追求难题怪题把当天做完的题目复盘清楚比多做一道新题有用得多。毕竟C课后的习题训练练的是把知识变成肌肉记忆的过程急不来。第85天真正教会我的一件事是课后习题训练练的不是“这道题我会不会”而是“当一道我不会的题摆在面前时我有没有一套稳定的拆解流程”。只要这个过程还在第185天和第385天就都值得继续写下去。