ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++课后习题训练115天复盘:从语法巩固到调试与算法实战

C++课后习题训练115天复盘:从语法巩固到调试与算法实战 半年多前我给自己定了一个规矩C的课后习题不能只看答案必须每天亲手敲、亲手调、亲手记录踩坑过程。昨天刚做完第115天的训练任务回看这115天的记录说实话收获比我想象中大得多。最初我只是想巩固课堂上学过的语法现在反倒被这些习题推着把算法、内存、调试、工程配置这些边边角角的东西都补了一遍。这篇记录不是什么系统教程就是我在Day115这个节点上对近期训练内容的一次复盘包括几个反复踩的坑以及我实际摸索出来的应对方式给同样在刷C习题的朋友一个参考。1. 为什么把课后习题训练坚持到第115天1.1 训练计划是怎么搭起来的我的训练方法其实很笨每天固定抽出45到90分钟只做课后习题里那些“看起来会做、但一写就错”的题目。第一天我给自己定了几条规则不跳题哪怕题目再简单也要写完整代码并跑通。每题至少保留两种写法优先比较它们的差异和效率。无论AC通过测试与否都要把出错信息和解决过程记录到日志里。每周末挑一道本周错误最多的题目重新独立完成一遍。第115天回过头看这套规则最大的价值不是让我刷掉了多少题而是逼我养成了“先想清楚再动手”的习惯。以前我做习题喜欢直接打开编辑器边写边试经常写到一半发现逻辑不对推倒重来。现在我会先在草稿纸上列出输入范围、边界条件、时间复杂度要求再确定数据结构和算法方向。这一个习惯的转变就让我写题的准确率从刚开始的不到一半提升到现在绝大多数题目都能一次通过编译和样例测试。1.2 习题记录的价值不在“做出来”而在“为什么这么做”很多人刷习题追求的是“我AC了”但我这115天的记录里真正值钱的反而是那些出错的过程。比如有几天我连续在字符串相关的题目上栽跟头把出错信息一对照才发现问题全出在我对字符数组和string的行为差异理解不够透彻。这种“树状记录”的方式让我很快定位到自己的薄弱环节然后集中火力补课。我每天记录的内容包括题目的核心考点、我的初始思路、最终的实现方案、出错时的堆栈或控制台输出、以及我当时踩坑的心理活动。把“心理活动”写进去听起来很怪但它的作用很大。因为我发现很多错误并非知识盲区纯粹是惯性思维导致比如总觉得循环边界可以“差不多”或者总觉得编译器会帮我处理掉未初始化的问题。把这些错误想法记录下来再对比实际运行结果比单纯刷二十道题都管用。2. 近期重点题型与核心知识点拆解2.1 随机数题目别再把 rand() 当万能钥匙随机数是近期习题里出现频率比较高的考点但我发现很多教材和网课讲得都太浅了直接说“用rand()加取模就行”结果一到实际场景就翻车。先聊最基础的#include cstdlib #include ctime int main() { std::srand(static_castunsigned(std::time(nullptr))); int x std::rand() % 100; // 0~99 return 0; }这段代码在入门阶段确实够用但如果你真拿它去做抽奖、随机洗牌或者蒙特卡洛模拟这类需要均匀分布的习题就会被坑得很惨。原因很简单rand()的默认实现多数是线性同余生成器低位随机性比较差直接用%截断会让分布产生偏差。我在习题里被教育过一次后给自己定了几个更可靠的做法用 C11 之后标准库提供的random优先选择std::mt19937。分布器用std::uniform_int_distribution或std::uniform_real_distribution不要自己拿%硬切。随机种子来源习题场景用std::random_device{}()就够不要动不动就time(nullptr)同一秒内多次运行会得到相同序列。下面是我现在写的随机数代码模板#include random std::mt19937 rng{std::random_device{}()}; int roll(int minVal, int maxVal) { std::uniform_int_distributionint dist(minVal, maxVal); return dist(rng); }这个做法的好处有两个第一mt19937的周期很长生成的序列在统计上均匀得多第二把“随机数引擎”和“分布”分开换分布只是一行代码的事做题时非常方便。不过有一个细节要提醒如果你在多线程程序里用随机数要考虑引擎不是线程安全的更好的是每个线程一个thread_local引擎或者使用std::mutex保护否则同一个线程里出错很难排查。2.2 字符串数组初始化的几道经典“送命题”C里的字符串数组初始化看着简单每年都有大量新手在它上面挂掉。我随便列几种常见写法你试试能不能一眼看出问题char str1[] hello; // 正确长度自动为6含\0 char str2[5] hello; // 错误放不下末尾的\0 char str3[6] hello; // 正确刚好容纳 char str4[10] hello; // 正确多余部分初始化为\0有次习题就是让学员找出上面哪行会编译错误结果班里一堆人没看出来。问题就出在str2上你看着hello是5个字符觉得数组长度5就够了但实际上C风格字符串必须以\0结尾所以长度至少得是6。如果硬写成5编译器通常不会直接报错而是在运行时把\0写到数组外这种“不报错但是致命”的行为最坑人因为它会悄悄破坏相邻内存的数据。再看string和字符数组的差异。我近期记录了一道题要求把两个字符串拼接起来分别用C风格和C风格实现。C风格我一开始漏了目标数组长度的问题目标数组开小了拼接之后直接越界。C风格用std::string就省心很多#include string std::string a Hello, ; std::string b World!; std::string c a b; // 自动管理内存这个题给我的教训是如果在现代C习题中允许使用标准库优先考虑std::string只有当题目明确要求考察C风格字符串或内存操作时才手动处理字符数组。不要为了“显得底层”而故意去用手动管理内存的方式实现一个完全可以用string优雅解决的问题。还有个小细节std::string的c_str()返回的指针在字符串修改后会失效。我把这个知识点记录在了Day110当时测试代码时把一个std::string的c_str()指针存了下来然后继续往原字符串里追加内容最后这个指针变成野指针程序开始随机崩溃。解决方案很简单每次要用c_str()就去重新调用不要缓存。2.3 const、static、final 的关键区别别到用的时候才查近期习题里有一类纯概念题专门考察修饰符的用法比如“const成员函数里能不能修改成员变量”“static成员变量该怎么定义”“final类到底想干什么”。我第115天的记录里专门给它们做了一张对比表方便以后复习修饰符作用对象核心作用常见坑const变量、成员函数、指针定义只读语义编译期检查const成员函数里修改成员变量会编译失败除非用mutablestatic变量、函数、成员定义与对象无关的存储或行为静态成员变量需要在类外定义忘了定义直接链接错误final类、虚函数C11起禁止继承或禁止进一步重写类被标记final后尝试继承会编译失败先放一段我自己整理的代码片段把这三个修饰符放在一起看class Base { public: static int counter; // 声明需类外定义 virtual void show() const; // const成员函数内部不可修改非mutable成员 }; int Base::counter 0; // 类外定义否则链接期报错 class Derived final : public Base { // final类不能再被继承 public: void show() const override; // 合法重写虚函数 }; // class Derived2 : public Derived {}; // 编译错误Derived是final的我踩过最典型的坑是写了static int counter;但在类外忘了写int Base::counter 0;结果编译通过、链接时报一堆未定义引用。当时我还一脸懵以为代码没问题后来一查才发现静态成员变量和普通全局变量不一样它虽然声明在类内但存储空间必须单独定义。关于const还有一个容易弄混的点const放在成员函数后面void func() const意味着这个函数不会修改对象的非静态成员但如果你有一个成员变量被声明为mutable那就例外可以在 const 函数里改它。这个机制通常用于缓存、互斥锁计数等场景习题中偶尔会考到不要只背结论花十分钟写个demo感受一下比较好。final的坑相对少但如果题目的类层次比较深加上final后无法再扩展会让以后重构变得困难。我在做设计类习题时会先想清楚这个类是否真的不会再被继承再决定要不要加final。2.4 结构体链表的“基本盘”写不对就是白学C课程进行到数据结构阶段链表几乎是必练的内容。结构体链表的基本语法并不难但我在记录里发现很多错误出在“内存管理”上而不是“语法”上。先看一个基础的结构体定义和单个节点创建struct Node { int data; Node* next; Node(int x) : data(x), next(nullptr) {} }; Node* createNode(int x) { return new Node(x); }有次习题要求创建一个链表并且在函数结束后清空内存。许多人的第一版代码只做了new没有delete导致程序运行后内存泄漏。我当时在日志中写了一句提醒只要用了new就要问一下自己谁来delete这不是强迫症而是防止内存泄漏的基本素养。链表的删除节点操作也有很多细节。比如删除某个值对应的节点时如果只改前驱节点的next忘记释放目标节点的内存就会造成泄漏如果直接delete目标节点但没修正前驱的指针再次遍历时就会访问野指针程序大概率崩溃。正确写法一般是这样单链表删除第一次出现的值为key的节点void deleteNode(Node* head, int key) { if (!head) return; if (head-data key) { Node* tmp head; head head-next; delete tmp; return; } Node* prev head; Node* cur head-next; while (cur) { if (cur-data key) { prev-next cur-next; delete cur; break; } prev cur; cur cur-next; } }这里有一个很重要的细节删除头节点时必须修改head本身所以参数用Node*。如果只传Node*修改的是参数副本外面拿到的head还是指向已释放的内存一用就崩。这个坑我在Day57时踩过当时排查了很久才反应过来。链表这类题的特点就是这样逻辑看起来好像都对但一旦指针传参方式错误运行结果就完全不可预期。我还碰过“借用临时变量保存指针”的漏网情况比如在遍历时直接delete cur然后又用cur-next继续往前走这就是典型的悬垂指针使用属于课后习题的高频雷区。正确的做法是先把next保存下来再释放Node* tmp cur; cur cur-next; delete tmp;3. 算法题从冒泡排序到单调栈和快速幂3.1 冒泡排序人人都能写但不是人人能写对冒泡排序是C课后习题的常客很多同学觉得它简单但这道题恰恰是最容易暴露出“对边界和指针不敏感”的问题。比如下面这段经典算法void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - i - 1; j) { if (arr[j] arr[j 1]) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; // 优化本轮没有交换则说明已有序 } }我在记录中认真标过这个算法的两个关键点外层循环次数只需n - 1不是n。因为最后一个元素在前几轮比较后已经就位再跑一轮纯属浪费。内层循环的右边界是n - i - 1。每完成一轮最大元素相当于被“冒泡”到了末尾所以下一轮无须再和它比较。很多人刷题时只记住了“排序交换”却忽略了这个优化版的swapped标志。实际上这个标志能让接近有序的数组在O(n)时间内完成排序而不是永远O(n^2)。如果题目里给了“数组大部分有序”这样的大数据量输入这个小小的优化可能就是能否通过时间限制的关键。我还有一种心得写排序类题目尤其是课后习题里的中小规模排序不要一上来就引入快排或者归并。先用冒泡理解最基础的交换思想再逐步优化才能真正理解“为什么快排快”“为什么归并需要额外空间”。如果你现在还在靠死背代码应付排序题建议打开调试器一步步看冒泡排序的数组状态变化我敢说你会有种“原来之前都是假装会了”的顿悟。3.2 单调栈解“下一个更大元素”类题目的利器第115天前后我集中练了一批单调栈相关的题目。这类题目在课后习题里不算简单但在笔试面试中很有代表意义。拿最典型的“下一个更大元素”举例给定一个数组对于每个元素找到右边第一个比它大的元素不存在则返回 -1。暴力做法是二重循环时间复杂度O(n^2)。数据规模一大就会被卡。单调栈的写法把复杂度降到接近O(n)。核心思路其实就一句话维护一个栈让栈内元素保持单调不增。遍历到新元素时只要它比栈顶元素大就把栈顶元素弹出同时记录栈顶元素的下一个更大元素就是当前值。我写过很多版本最终稳定成如下模板vectorint nextGreaterElement(const vectorint nums) { vectorint res(nums.size(), -1); stackint st; // 存储下标 for (int i 0; i (int)nums.size(); i) { while (!st.empty() nums[i] nums[st.top()]) { res[st.top()] nums[i]; st.pop(); } st.push(i); } return res; }重点解释一下为什么栈里存下标而不是直接存值因为当我们要更新结果数组时知道下标才能定位到对应位置否则还得用一个额外数据结构去映射值到位置没必要。while循环保证只要当前元素比栈顶元素大就能把栈里所有比它小的元素处理完。处理完后当前下标入栈为后续元素做铺垫。这个模板我建议直接背熟但更重要的是理解“单调”的含义。单调栈的变形很多比如“每日温度”这道题要求的是“多少天后升温”其实就是下一个更大元素的下标差模板整体不变只把返回值改成i - st.top()。别怕变种抓住“单调”这个核心什么变种都逃不过你的手心。3.3 快速幂递归和迭代都要会快速幂是算法入门阶段一个很重要的“思维提升点”。它用分治思想把幂运算从O(n)降到O(log n)在需要判大数模运算的题目里非常常见。我第一次写快速幂时用了递归long long fastPow(long long a, long long n, long long mod) { if (n 0) return 1 % mod; long long half fastPow(a, n / 2, mod); half half * half % mod; if (n % 2 1) half half * a % mod; return half; }思路是a^n (a^(n/2))^2如果 n 是奇数再额外乘一个a。这个思路一点都不难难点在什么时候取模、什么时候用long long。如果中间运算不加% mod可能在第几次递归后就直接溢出结果惨不忍睹。后来我为了练迭代写法又整理了非递归版本long long fastPowIter(long long a, long long n, long long mod) { long long res 1 % mod; a % mod; while (n 0) { if (n 1) res res * a % mod; a a * a % mod; n 1; } return res; }这两种写法各有用处。递归理解起来直观但递归深度在极端情况下可能成为问题所以对很大的n迭代更稳。迭代版本的核心是把n看作二进制位遇到1的位就乘上对应的幂每轮最底层的幂自己翻倍。这一步大家可能一眼看不懂建议拿a2, n10手动模拟一遍很快就能体会出“二进制 累乘”的精髓。我当时还顺手把“矩阵快速幂”也一并练了是用来解决线性递推问题的。如果你已经能把上面的整数快速幂吃透矩阵快速幂无非是把long long的乘法换成矩阵乘法核心思想完全一样。但这个是后话Day115的记录里我还没有系统整理它先不展开。4. 环境与工具VSCode配置C/C的实用笔记4.1 别再让调试环境成为刷题阻力很多新手在刷题时最崩溃的不是题目本身而是代码写完却运行不起来。我在第115天的记录里专门整理了VSCode配置C/C环境的完整版本因为我发现身边不少同学根本不敢碰调试器碰到问题只会“输出中间变量”来回改效率很低。我用的是经典的 MinGW-w64 编译器方案VSCode 里装 C/C 扩展然后配置.vscode/tasks.json和.vscode/launch.json。先说最基础的编译任务配置{ version: 2.0.0, tasks: [ { label: build, type: shell, command: g, args: [ -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true } } ] }这里有个非常关键的参数是-g它会在可执行文件里生成调试信息如果缺少它断点就断不下来调试功能形同虚设。我见过不少人直接粘贴网上配置结果把-g删了只留-o和源文件名编译是能通过但要调试的时候才发现各种变量信息都看不到只能打回重来。接下来是调试配置launch.json{ version: 0.2.0, configurations: [ { name: debug, type: cppdbg, request: launch, program: ${fileDirname}/${fileBasenameNoExtension}.exe, args: [], stopAtEntry: false, cwd: ${fileDirname}, environment: [], externalConsole: false, MIMode: gdb, miDebuggerPath: gdb, preLaunchTask: build } ] }preLaunchTask的作用是每次按F5调试之前先自动编译这样你不需要手动切到终端执行g命令。externalConsole设成false是为了让输出显示在VSCode内置终端里对比日志更方便。如果你想看经典的黑框控制台可以设成true但那样的话截图和记录输出就比较麻烦。还有一个经常被忽略的点如果你的系统同时装了多个编译器比如VS的MSVC和MinGW要确保 VSCode 的miDebuggerPath指向正确的gdb.exe。我遇到过装着多个版本导致调试器启动失败的情况最后是把MinGW的bin目录放到环境变量最前面才解决。4.2 Microsoft Visual C Redistributable 这个“隐形依赖”搜索热词里有一条关于 Microsoft Visual C Redistributable 的问题老实说很多刷题的同学第一次遇到它并不是在自己的电脑而是在考试或评测机上拷出程序运行结果发现目标机器缺了运行时库程序直接报错。C程序有两种运行库链接方式静态链接与动态链接。MinGW下如果用-static-libgcc之类的参数会在生成的可执行文件中静态打包一些库那么目标机器上不需要装额外的运行库也能跑。但你如果用MSVC编译工具链并且目标是动态链接那目标机器上就需要安装对应版本的“Microsoft Visual C Redistributable”。我在自己刷题电脑上装的是Visual C Redistributable for Visual Studio 2015-2022这个版本能兼容大部分现代C程序。如果你是用VS或某些依赖MSVC的第三方库装上它是必须的否则常见的提示就是“VCRUNTIME140.dll丢失”或者“找不到MSVCP140.dll”。当时我把这个点写进日志是因为我在一台新电脑上运行自己编译的测试程序突然就弹出了缺少运行库的提示一度以为是编译器装坏了。后来才发现只是这台新电脑没装Redistributable。这个坑不算算法题本身但在训练环境下确实很影响心情提早记录一下能省很多折腾时间。4.3 “野指针”或库位冲突导致的 Access Violation 排查热词里的access violation c0000005我第115天前就遇到过好几次。这个错误的中文表现一般是“0xC0000005 写入位置xxx时发生访问冲突”它背后最典型的原因是空指针解引用、野指针、栈内存被破坏或者跨DLL边界传递了不安全的指针。我在训练记录里整理过一个排查顺序先看崩溃时在哪个函数、哪一行。检查该行是否有数组越界、空指针解引用或使用了已释放内存。检查是否有memset、memcpy长度超出目标缓冲区。如果和第三方库相关注意是否跨模块new/delete比如在一个DLL里new在EXE里delete很可能因为堆管理器不一致而崩溃。有一次正好是在回调函数里出了这个错误。回调函数本身是某个库调用我们的入口传进来的指针生命周期有限。我习以为常地把它保存到全局变量里等用完再访问时指针指向的内存已经被释放了于是直接 Access Violation。这提醒我对于外部传入的指针不要轻易延长其生命周期能拷贝数据就拷贝数据不要只拷贝指针。这一条放到任何依赖第三方SDK的项目里都适用不只是课后习题。5. 常见报错与问题排查实录5.1 “捕获到标准C异常”并不等于你知道哪里错了有时候程序在debug模式下弹出一个很笼统的提示“捕获到标准C异常。有关详细信息请参见系统日志文件”。我当年第一次看到这行提示时也很懵因为它不像编译报错会指到具体行号更像一个“元信息”。我在Day115的记录里专门把这类问题的排查思路写下来优先切换输出窗口看是不是有具体异常文本被吞掉了。用调试器在“异常设置”里勾选“第一机会异常”让程序在抛出异常的那一刻就断下来而不是在顶层捕获后才提示。看系统事件日志里对应的应用程序错误时间拿时间戳和代码日志比对缩小范围。最常见的原因其实是std::out_of_range或std::bad_alloc前者往往是不小心访问了越界下标后者往往是无限循环导致内存爆炸。尤其是第一机会异常这个功能我强烈建议每个人配置好。它就像在警察到现场之前先让报警器响让你能站在第一现场看问题而不是事后通过日志猜。设置方法很简单VSCode的调试控制台里运行breakOnException之类的命令或者在launch.json的type: cppdbg配置中加入stopAtEntry: true结合异常设置逐个击破。5.2 编译通过运行崩溃的几种典型场景我在115天里遇到过最多的运行时崩溃通常发生在以下四种场景场景典型报错自查路径数组越界无报错或栈内存保护触发检查所有下标尤其是循环里用了空指针调用0xC0000005加上判空或在创建对象后立刻断言重复释放0x000000D9检查是否在多个分支中意外复制了指针格式化字符串随机崩溃检查printf/sprintf的占位符是否匹配其中“数组越界”真是最隐蔽的。尤其是用for (int i 0; i n; i)去访问长度为n的数组编译器通常不会给任何提示程序要么正常运行要么在某些特定输入下才崩这种“间歇性崩溃”最难查。我现在遇到这种题第一反应就是把所有循环条件里的改成并且顺手用静态检查工具扫一遍。另一个高频场景是重复释放。我在一次习题里用链表存了一组节点删除函数里调用delete node但是调用方又保存了同一份指针二次调用时会再次delete。这种情况在刷题环境里看不出来但一到长期运行的实验室或引擎项目里就是随机偶发的崩溃连内存转储都不一定有用。所以我在日志里反复强调谁拥有这块内存谁负责释放不要跨模块共享所有权。可以将所有权显示转移但是一定要有一个唯一的“责任方”。5.3 调试器断点不生效的“低级但致命”原因刷题过程中我一度以为调试器坏了断点打上了运行后却直接跳过边看边懵。后来排查发现是编译时没有加-g调试信息。这个我在前面VSCode配置部分也提到过但这里想单独强调一遍因为它太容易被忽略。还有一种情况是代码优化等级开得过高。比如-O2甚至-O3编译器许多指令重排和函数内联会让调试器无法精确定位到某一行断点位置会发生偏移甚至某些局部变量被优化没了。如果你要调试建议在Debug配置下不要开高优化或者至少把-O0放到编译命令里。此外跨语言的场景比如C#调用C DLL也容易出调试问题。热词里有一条“C#调用C出现access violation c0000005”这个我在之前的项目里也踩过。C#侧通过DllImport调C导出函数函数的签名如果不对比如int*和ref int的对应关系搞错运行时就会报 Access Violation。这个场景与纯C习题有一定距离但如果你在课后作业或毕设中做混合编程肯定用得上。排查方法是先确认调用约定对不对C侧是不是extern C再看参数封送有没有用到安全的MarshalAs。这个出错时先不要急着怀疑C代码先把C#侧封送和生命周期搞清楚一半的崩溃立刻就能消失。6. Day115这个节点上我对C练习路径的几个新判断6.1 学了语法不等于能做题但做题也不能替代系统学习115天训练让我最有感触的一点C语法和算法是两套东西但它们互相成就。如果只学语法不刷题你根本不知道vector扩容时迭代器可能失效意味着什么。但如果只刷题不啃语法很多代码你抄都抄不顺还容易在const和static这类坑里反复摔。我现在回忆最初二十天的训练那时候每天写个排序、链表都觉得自己要成大神了非常浮躁。后来做结构体和运算符优先级相关的题目时才发现自己对C表达式的求值顺序其实一知半解。C不像Java那样有很多设计上的“安全网”它把很多选择权交给开发者代价就是你必须清楚自己在干什么。这也是为什么C课后习题训练记录值得坚持的原因——每天都能逼你面对“原来我并不懂”的瞬间。6.2 记录每道题的“失败模式”比记录正确答案更有长期价值现在翻看这115天的日志我发现最常犯的错误不是“不会做”而是“会做但写错一点”。比如排序时边界写错、链表中忘记释放内存、字符串拼接时忽略\0的位置、快速幂取模没乘系数导致溢出。这些失败模式在正确答案里完全看不出来却在我自己的代码史上反复出现。所以我现在的做题模板多了一个“赛后总结”环节完成AC后再花十分钟写三个点——这道题我容易在哪里错、我通过什么手段发现它错了、下次如何提前预防。这个环节会逼我主动思考失败模式而不是“代码跑通了就万事大吉”。Day115时我明显感觉到同样的题目以前可能需要半小时反复试错现在往往一次就能写对就是因为很多失败模式已经被我总结成了一张“错误指纹表”。6.3 环境问题也要纳入训练范围不要只埋头刷题训练第70天左右我突然意识到一个问题如果我不熟悉VSCode配置、不熟悉运行库依赖、不熟悉调试器怎么设置那么即使算法写得再漂亮交付起来也是举步维艰。那天我特意花时间把开发环境整体整理了一遍后来那些access violation、缺运行库、断点不生效的问题都变成了我日志里的“环境篇”反而在以后帮了大忙。所以我给正在刷C习题的朋友一个建议不要只盯着题解花点时间搞清楚你的工具链搞清楚编译器的报错信息是什么意思搞清楚调试器在崩溃时给你的提示。把“环境问题”也当作练习题来做等你后面进入真实项目开发就会感谢现在的自己。6.4 即时反馈的持续性比一次性的高强度更可靠最后再说一点个人体会。Day115这个数字其实没有任何魔法它最大的意义是“让即时反馈保持足够长的生命周期”。如果我把115天的量浓缩成一周做完那我只会得到一堆浮光掠影的印象什么坑都记不住。但如果每天只写一两道题并做记录那些错误就能在脑海中发酵变成肌肉记忆。我建议你如果也想做类似训练不必追求每天刷很多题重点是把每一天都写进日志今天的代码哪里跑不通为什么跑不通我如何调通的下次遇到同类问题能不能更快定位。哪怕每天只记录一小段文字30天之后你也会拥有一本完全属于自己的“排错手册”它比任何教程都有价值。训练到现在我从一个写链表都会泄漏内存的新手变成了能从容面对调试器、敢于用单调栈和快速幂解决问题的人。第115天并不是终点我期待第200天、第300天时回看今天的记录能更清晰地看到自己是怎么一步步走过来的。如果你也正走在这条路上不妨也给自己留一个“Day N”的记录等攒到一百多天再回头看我相信你会有同样的感慨。
返回列表