ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

100天C++课后习题实录:从环境配置到语法细节与算法面试

100天C++课后习题实录:从环境配置到语法细节与算法面试 1. 100天前定的规矩我为什么用“课后习题”而不是“项目驱动”来练C先交代一下背景。这个“Day100”不是什么编程训练营的打卡任务是我自己给自己定的规矩每天必须完成至少一道C课后习题并且把完整的思考过程、踩坑记录、优化对比写下来。到今天刚好满100天翻了一遍这100天的记录发现很多东西和当初预想的不一样。很多人学C喜欢直接上项目觉得课后习题太简单、太课本化不如做个贪吃蛇、写个学生管理系统来得有成就感。但我的体感恰恰相反——课后习题才是把语法细节磨到肌肉记忆的最短路径。项目里你遇到一个bug可能花了半天才发现是生命周期问题而课后习题的设计目的就是精准打击某一个知识点你每做一道就等于把这个知识点的所有边界条件、常见误解全部搓了一遍。举个具体的例子。热搜词里有个“c 4%2”看起来像是个极其弱智的问题但等你真去查“C里负数的取模运算到底是向零取整还是向负无穷取整”你会发现C标准里从C11开始才把整数除法的截断方向明确为向零取整而%的结果符号跟随被除数。这种细节写项目一年都不一定碰得到但课后习题能让你在第三天就遇到。另一个原因是反馈周期短。项目写到一半卡住挫败感极强但课后习题一个小时之内就能有明确的正误反馈这种高频的“完成感”是坚持100天最重要的燃料。如果你也是一个人自学、没有外部督促我强烈建议你试一下这个笨办法——每天一道题连续30天你会明显感觉到写代码时的手感不一样了。2. 环境折腾是第一天的大坑VS Code、MinGW-w64和那堆Redistributable到底怎么配Day1我啥也没干成光配环境就折腾了一晚上。这里把踩过的坑集中说一下主要是为了让你别在第一天就放弃。2.1 编译器选型MinGW-w64还是MSVC还是Clang我最后选的是MinGW-w64 VS Code。原因很简单轻量、免费、跨平台而且和VS Code的配合最成熟。MSVC虽然调试器强大但Visual Studio那个体量对课后习题来说实在太重了Clang的报错信息是最友好的但Windows下配置稍麻烦等你Linux玩熟了再去拥抱Clang不迟。但MinGW-w64有个坑装的时候一定要选对架构和线程模型。我一开始装的是32位版本结果编译出来的程序在64位系统上跑着没问题但一旦涉及指针和size_t的混用各种隐式转换警告能把你烦死。建议直接选x86_64架构、posix线程模型、seh异常处理模型的版本。2.2 VS Code的tasks.json和launch.json到底配什么VS Code只是个编辑器编译和调试都靠配置文件。很多教程会让你装一堆插件但真正必需的只有两个C/C扩展微软官方的那个和Code Runner可选。剩下的什么C Intellisense、CMake Tools对单纯刷课后习题来说都是锦上添花前期装多了反而干扰。tasks.json是干编译用的。给一个最简配置{ version: 2.0.0, tasks: [ { label: C Compile, type: cppbuild, command: g, args: [ -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true }, presentation: { panel: dedicated, clear: true } } ] }注意${file}是当前文件路径这意味着你写多个小题时每个.cpp文件是独立编译的互不干扰——对课后习题这种“一次一个程序”的场景非常合适。不要一上来就搞CMake那是为多文件项目准备的提前上重武器只会增加认知负担。launch.json是干调试用的{ version: 0.2.0, configurations: [ { name: C Debug, type: cppdbg, request: launch, program: ${fileDirname}/${fileBasenameNoExtension}.exe, args: [], stopAtEntry: false, cwd: ${fileDirname}, environment: [], externalConsole: false, MIMode: gdb, miDebuggerPath: 你的MinGW/bin/gdb.exe路径, preLaunchTask: C Compile } ] }这段配置里preLaunchTask的值要和tasks.json里的label保持一致否则点F5会报“找不到任务”。我见过太多人卡在这了。2.3 Visual C Redistributable那件事热搜词里那个“已检测到匹配的 visual c redistributable跳过安装 解压缩: c:\users\administ...”是很多人在装软件时弹出的窗口。这不是代码问题是Visual C运行库的安装器提示。你装某些Windows软件时它要求系统里先有对应版本的VC运行库比如2015-2022 x64而检测到你已装过匹配版本就会跳过。具体来说VS Code的C/C插件本身不依赖VC Redistributable但如果你用MSVC编译器或者你运行的程序依赖动态链接到msvcp140.dll、vcruntime140.dll这些库那系统就得装对应运行库。MinGW-w64编译出的程序走的是libstdc-6.dll不依赖VC运行库所以用MinGW的人基本不用管这个。但如果哪天你去跑别人编译好的Windows程序弹窗报缺VCRUNTIME140.dll那就去微软官网下“Visual C Redistributable for Visual Studio 2015-2022”装一遍x64和x86两个版本都装因为有的程序是32位编译的需要x86版本运行库。一次装好基本长期不折腾。把这个事写进来是想说环境问题里很大一部分不是你的代码问题而是运行库、编译器、链接器之间版本匹配的问题。先在脑子里建立这个意识后面遇到诡异问题会淡定很多。3. 语法细节才是真正的分水岭覆盖/隐藏、字符串数组初始化这些Day100才敢说“懂”的知识语法这块我按“踩过的坑”来写这里面的每一条都是热搜词里真实出现的、也是我这100天里真实迷糊过的点。3.1 覆盖override、隐藏hide、重载overload三者别再混了这是C新手最绕的一点热搜词里“c 覆盖 隐藏”直接上榜就很说明问题。我用一个最小复现来厘清class Base { public: virtual void func(int x) { cout Base::func(int) endl; } void func(double x) { cout Base::func(double) endl; } }; class Derived : public Base { public: void func(int x) override { cout Derived::func(int) endl; } void func(char x) { cout Derived::func(char) endl; } };这三种情况的本质区别重载同一个类里函数名相同、参数列表不同。比如Base里的func(int)和func(double)这俩是重载关系调用时靠实参类型决定匹配哪个。覆盖派生类重写基类的虚函数函数签名完全一致且基类函数有virtual。签名不一致不构成覆盖比如Derived里的func(char)和基类的func(int)参数类型都不同覆盖不了。隐藏派生类里定义了同名函数不管参数是否相同都会把基类里的同名函数“藏”起来。注意隐藏不需要virtual也不需要参数一致。上面Derived里只要有func基类所有名为func的函数在派生类对象里都是不可见的。所以Derived对象如果想调base的func(int)必须显式加作用域d.Base::func(1)否则编译报错。这三者搞混的直接后果就是你以为自己重写了虚函数实际却定义了一个新函数导致多态失效。排查方法是给打算覆盖的析构函数和成员函数加上override关键字——如果签名不匹配编译器会直接红牌警告根本不给你错误发生的机会。这条建议是我这100天里觉得性价比最高的一条。3.2 字符串数组初始化char[]和string谁更坑“c字符串数组初始化”也是高频搜索词。说实话如果新手期末作业里出现char arr[10] hello那是经典写法但到了Day50以后还在用char[]做字符串处理就该被骂了。C标准库的std::string处理动态长度字符串省心得多面试手写题里char[]只在特定的嵌入式或底层内存布局场景出现。常见的坑是这么几个char str1[] hello; // 长度6结尾自动加\0 char str2[5] hello; // 编译报错放不下6个字符含\0 const char* p hello; // 字符串字面量存放在只读区不能修改另一种面试爱问的场景是char* p hello; p[0] H;——这就直接未定义行为因为字面量在只读区。用std::string就不会有这种问题它管理好自己的堆内存赋值、拼接、取子串都有现成方法。我的建议很直接课后习题里凡是能用std::string的就不要写char[]。等你哪天真的遇到只能用char[]的场景比如处理二进制数据、极低内存环境再回来补C风格字符串的细节不迟。学习顺序很重要先学会用工具再理解工具背后的机制。3.3 模板类链表为什么你会觉得STL已经够了还要手写“c模板类链表”这个热搜词让我想起自己写的一个模板单链表。我当时以为自己能写出来就是会了结果被三个问题打回原形拷贝构造函数没写导致浅拷贝析构函数没把每个节点都delete导致内存泄漏还有模板的声明和实现不能分离到.h和.cpp——模板是在编译期实例化的编译器需要在看到模板定义的地方就能看到完整实现所以通常把实现也放在头文件里或者用*.tpp文件单独放实现再在头文件尾部#include。手写一遍链表的价值在于你能真实感受到“析构函数为什么需要遍历删除”“为什么拷贝构造需要深拷贝”“为什么unique_ptr能帮你省这些事”——你只有自己踩过内存泄漏的坑才能真正理解RAII的意义。STL是别人的实现手写链表是自己对内存和指针的掌控力训练。但这里有个更重要的判断到了Day100你不能还在手写链表来证明自己。面试手写链表是为了考验你是否理解指针和内存管理而工程中用STL是天经地义的。要把课后习题和工程实践分开看待。4. 算法训练不是刷题量游戏从冒泡排序到二分查找再到快速幂和单调栈4.1 冒泡排序新手的第一个“算法思维”分水岭热搜词里“冒泡排序算法c”“c 冒泡排序”都在前列说明这是绝大多数人入门算法的第一道坎。我Day3的习题就是手写冒泡排序当时还觉得“这也算算法不就是两层循环套一下吗”。但等你被问到“怎么优化冒泡排序”的时候才意识到这题没那么简单。最基础的写法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]) { swap(arr[j], arr[j 1]); } } } }优化点其实藏在两个地方如果某一趟遍历中没有任何交换发生说明序列已经有序可以提前退出而每一趟结束后最后一个元素一定是当前最大的所以内层循环边界可以逐步缩小。再加上一个flag标记是否发生交换代码变成了void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; } }面试官问冒泡排序更多是想看你能否说清楚它的时间复杂度和空间复杂度最好情况O(n)、最坏情况O(n²)、平均O(n²)空间复杂度O(1)稳定排序。这几个点你五分钟之内能脱口而出算法基础这一关基本就算过了。4.2 二分查找边界条件是最大的坑“c 二分查找”能上热搜太正常了因为二分查找的代码量极少但边界条件是出了名的难写对——尤其对左闭右开还是双闭区间没想清楚的时候写着写着就把循环条件写反了。我总结了一个稳如老狗的模板以双闭区间[left, right]为例int binarySearch(vectorint nums, int target) { int left 0, right nums.size() - 1; while (left right) { int mid left (right - left) / 2; if (nums[mid] target) return mid; else if (nums[mid] target) left mid 1; else right mid - 1; } return -1; }这里left right对应双闭区间mid 1和mid - 1是为了缩小范围时不重不漏。如果你改用左闭右开[left, right)那循环条件就是while (left right)相应的更新逻辑也要变。关键是一次只记住一套然后把所有二分相关题目都用这一套模板去套形成固定套路。到Day60以后你又会被问到一个进阶问题“如果数组里有重复元素怎么找到第一个target的位置lower_bound”这时候再把上面的模板微调一下返回条件改成nums[mid] target你会发现理解起来比直接看STL源码容易得多。4.3 快速幂、单调栈这些“听着吓人”的算法其实有固定套路“快速幂算法c”和“单调栈算法c”都属于课后习题里“进阶通关”类的代表。快速幂解决的是a^n mod m这种问题。如果直接循环乘n次复杂度是O(n)n一上亿就崩了。核心思想是把指数拆成二进制利用a^(2^i)之间平方翻倍的关系把复杂度降到O(log n)。模板是这样long long modpow(long long a, long long n, long long m) { long long result 1; while (n 0) { if (n 1) result result * a % m; a a * a % m; n 1; } return result; }单调栈解决的是“找每个元素右边第一个比它大的数”这类问题。原理是维护一个栈保证栈内元素单调递增或递减每次新元素入栈时把破坏单调性的栈顶元素弹掉弹掉的那个位置对应的结果就确定了。这里想说的是这两个算法都属于“背模板 理解原理 三倍练习”就能攻克的类型。不要被名字吓到它们比动态规划简单了至少一个量级。5. C八股不是背出来的Day100整理面试题的正确姿势搜到“c八股”这个词的人可能已经在准备校招或跳槽面试了。八股文确实烦人但在国内技术面试的语境里它就是一种“知识体检表”。我的态度是不反对背但反对死背——每一道八股题背后都对应着一个真实的语言机制或工程场景。5.1sort函数的底层逻辑为什么面试爱考它“c sort 引入库”也是一个热搜项但真正的面试问题是“std::sort的实现原理”。大部分人会答“快速排序”但实际上std::sort是内省排序intro sort它结合了快速排序、堆排序和插入排序。递归深度过深时切换到堆排序避免最坏O(n²)当待排序区间长度小于某个阈值通常是16或20时改用插入排序因为小规模数据插入排序的常数极小。sort需要引入哪几个库algorithm。底层比较器默认用operator但你可以传入lambda或函数对象这也衍生出一个高频考题“sort的cmp函数里为什么不能用必须用”因为std::sort要求比较器满足严格弱序strict weak ordering会破坏这个性质导致未定义行为。5.2 回调函数和线程RAII和生命周期才是本质“c回调函数例子”和“c两个线程分别读写一个大数组”这两个热搜词其实可以放在一起看。回调函数本身不难难的是回调里捕获的上下文生命周期管理——捕获了this指针结果对象已经析构一调用就是空指针。线程间共享一个大数组也是同样的道理读写顺序、同步机制、数据竞争每个问题背后都是对对象生命周期和数据安全的考量。Day60的时候我写过一道“两个线程分别读写一个大数组”的习题当时用的是std::mutex加锁保护和std::atomicint做计数器。核心是别把两个线程想得多高深本质上就是并发访问共享数据时要保证同步最简单的方式就是让它们读写不同的区域或者加锁访问。关于回调函数我给新手的建议是把回调函数换成std::function捕获列表并且一定确认回调执行时所有捕获的对象都还活着否则就上weak_ptr和shared_ptr做好生命周期管理。5.3 面试题整理建立自己的“错误集锦”到Day100我把刷过的面试题分成了四类语言机制类、数据结构算法类、操作系统/内存类、项目场景设计类。每一道错题都记录了三段错解是什么、正确是什么、错误背后的认知偏差是什么。音频这种整理方式比任何时候都值钱——它不是靠背诵而是让你在看题瞬间就能定位到“这道题在考哪个机制”。比如“C里内联函数和宏定义的区别”“const成员函数能不能修改成员变量”“移动构造函数为什么通常标记为noexcept”——这些题虽然分别属于不同知识点但核心都在考察对“编译期行为、运行期行为、异常安全”这三个层面的理解。一旦你用这个框架去整理八股就不再是一个一个孤立的知识点而是一张有结构化联系的知识网。6. 给还没到Day100的人踩坑清单和心态建议最后一部分分享几个这100天里最痛的经验以及如果我从头开始会在一开始就注意什么。6.1 代码格式和命名规范要从第一天就养成“c竞赛码风”这个热搜词说明有人想了解竞赛选手的代码写法。但以我的经验竞赛码风和工程码风是天壤之别竞赛追求缩短、减少打字和常数优化变量名可以是a、b、c但工程代码要让任何人读起来都像在读一篇文章。Day1到Day40我在命名上花了大量心思——每个变量名都试图传达“它是什么 它为什么存在”。Day40之后回头改bug的效率明显提升。这个习惯绝对是复利效果最强的投资。6.2 遇到bug别急着问人先学会“读错误信息”很多初学者碰到编译错误第一反应是把完整报错截图丢到群里问人。但我100天里最大的体会是编译器的报错已经告诉了你90%的答案你只需要静下心把前三条错误信息完整读一遍。最常见的情况是“未声明的标识符”“未定义的引用”“越界访问”这些都能从报错信息里直接定位到具体文件和行号。真正需要问人的复杂bug通常都能用“最小复现”缩小到十行代码以内那时候再去求助别人也更愿意帮你。6.3 遇到“看不懂的过不去”的题时怎么办判断质数那道“c判断质数优化”的题是我Day10做的当时只会最朴素的从2到sqrt(n)枚举。后来才知道还能用埃氏筛、欧拉筛预处理素数表配合大量重复查询的场景。这里有个学习策略如果一道题做不出来先看一眼题解看懂思路之后合上题解自己重写一遍再隔一天重新写一遍。三遍之后的印象深度远超死磕两小时不放弃的效果。6.4 100天之后的路线选择如果已经坚持到了Day100说明你已经建立了足够的代码手感。接下来的路线可以这样规划如果目标是竞赛就把精力转向系统性的算法训练和复杂度分析如果目标是找工作就尽早开始啃操作系统、网络、数据库这些面试硬通货C这时候只是个工具你的重点是理解其底层运行机制如果目标是做项目那课后习题可以功成身退了去读一个真实开源项目的源码从中挑一个模块自己重写一遍。最后分享一个我自己的体会C这门语言学一百天可能刚好让你觉得自己开始入门了。它太庞大了——模板元编程、概念、协程、移动语义、内存序、异常安全——每一个分支都值得再花一百天。但请相信Day100那天你回头看Day1的代码那种“我居然能看出自己当初代码里的味道不对”的感觉是整个坚持过程里最爽的一刻。
返回列表