
1. 这份练习卷的定位与自测方法去年年底我参与一个部门的社招面试发现好几个简历写得挺漂亮的候选人在让手写字符串逆序这种基础题的时候卡了半天。你说他不会吧他张口就是strlen、strcpy、vector、map你说他会吧指针一绕就晕const加在哪都拿不准。回来之后我就想与其每次面试都临时出题不如把 C/C 工程师需要练的东西沉淀成一份综合练习卷。这份卷子不是从网上抄的题集而是我从笔试面试场景、社区高频提问、还有实际工程项目里踩过的坑反推出来的。1.1 卷子为什么按“能力模块”组织而不是按知识点很多练习题集是这么组织的第一章指针第二章数组第三章类与对象。这种组织方式的问题在于它和真实的工作场景是脱节的。你写一个文件读写功能的时候不会先想“这是指针题还是文件题”你只会想“我要把数据从磁盘读进来排序再写回去”——这中间指针、数组、文件操作、算法全部混在一起。所以我这份练习卷按四个能力模块来组织语言基础与内存模型、算法与复杂度、现代 C 特性、工程与环境工具链。每一个模块的题目都是从真实业务场景里抽象出来的高频动作。比如“字符串逆序输出”不是让你背一个算法而是考察你能否在不越界、不内存泄漏、不写出未定义行为的前提下完成一个最简单的数据变换。1.2 怎么用这份卷子自测我建议按这个方式使用每个模块限时完成时间到了就停笔不要“再给我五分钟我马上写出来”。真实笔试不会给你额外五分钟你平时对自己越宽松考场上就越吃亏。模块建议做题时长通过标准语言基础与内存模型40 分钟能独立写出全部代码且编译无警告算法与复杂度60 分钟能说明每个算法的时间复杂度和适用边界现代 C 特性30 分钟能回答为什么用这个特性而不是只会用工程与环境工具链20 分钟能不看文档完成环境配置和问题排查如果某个模块超时超过一半说明这个模块是你的短板。这没什么不好意思的C/C 的知识面太宽了没人能全精通。但你要清楚自己的短板在哪然后针对性补。还有一点很重要做完之后不要只看“能不能跑”要看“有没有隐患”。卷子里有不少题是故意埋了坑的比如没有判断文件打开失败、没有处理空字符串、排序算法写得对但忽略了稳定性。这些坑恰恰是实际工程项目里最常见的 bug 来源。2. 语言基础与内存模型字符串和数组的那些高频考点这个模块我出题的时候参考了最近社区里反复出现的热搜词字符串逆序输出、字符串数组初始化、指定顺序输出、字符串转数组。这些题目看着基础但恰恰是笔试面试里淘汰率最高的部分。2.1 字符串逆序的三层写法先看第一题给定一个 C 风格字符串char s[] hello要求原地逆序输出。这道题考的是指针操作和边界条件的处理核心实现并不复杂#include cstring void reverse(char s[]) { if (!s) return; int left 0; int right static_castint(strlen(s)) - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; --right; } }这里有三个关键点。第一strlen返回的是size_t是无符号类型如果字符串长度为 0strlen(s) - 1会变成一个非常大的正数所以要先判空。第二交换的时候用临时变量tmp不能直接s[left] s[right]否则会丢失数据。第三while (left right)这个条件保证了字符串长度为奇偶时都能正确处理。这是最基础的双指针写法。如果你用的是 C 标准库那有更简洁的版本#include algorithm #include string std::string s hello; std::reverse(s.begin(), s.end());这道题我还见过面试官追问递归版本它的意义在于考察你对递归栈的理解void reverse_recursive(char* s, int left, int right) { if (left right) return; std::swap(s[left], s[right]); reverse_recursive(s, left 1, right - 1); }这个递归版本在字符串很长的时候会栈溢出笔试里你只要说出这个缺点面试官就会觉得你是真懂而不是背答案。2.2 字符串数组初始化一张表说清所有写法第二题是关于字符串数组的初始化。很多人在这个上面栽跟头就是因为不清楚char数组、const char*指针、std::string这三者之间的区别。我整理了一张表基本覆盖了所有笔试会出现的情况写法存储位置是否可修改需要注意的问题char s[] hello栈上数组内容可修改数组大小自动推导为 6含\0char* s hello字符串字面量通常位于只读区修改是未定义行为应写成const char*const char* s hello只读区不可修改最安全但不能s[0] Hstd::string s hello堆上小字符串优化时在栈上可修改推荐但要注意c_str()返回的指针在修改后失效char s[6] hello栈上数组可修改数组大小必须大于等于 6否则编译报错char s[] {h,e,l,l,o}栈上数组可修改没有\0当字符串用会越界这里最经典的一个坑是char* s hello这种写法。在 C 标准里字符串字面量的类型是const char[N]你用一个char*去指向它相当于丢掉了一个const限定。虽然很多旧编译器只给警告不给错误但如果你尝试s[0] H在大多数平台上会直接运行时报错因为字面量所在的内存页是只读的。写这段的时候我特别建议你动手敲一遍把每种写法的sizeof(s)和strlen(s)打出来看。sizeof是包括\0的数组完整大小strlen是到\0之前的字符数这个区别几乎每年都有笔试会考。2.3 指定顺序输出从排序规则到 lambda第三题和“指定顺序输出”这个热搜词相关。灵活的人会立刻意识到这个题目不是考排序算法本身而是考“你能否按自定义规则排序”。最常见的情况是你有一个结构体数组需要按某个字段排序#include vector #include algorithm #include string struct Student { std::string name; int score; }; int main() { std::vectorStudent stu {{Alice, 90}, {Bob, 85}, {Cathy, 95}}; // 按分数从高到低排序 std::sort(stu.begin(), stu.end(), [](const Student a, const Student b) { return a.score b.score; }); return 0; }std::sort的第三个参数可以是函数指针、函数对象、lambda 表达式。笔试里用 lambda 是最简洁的但你要能说清楚如果 lambda 捕获了外部变量它本质上是一个匿名函数对象不是函数指针。这里还有一个常见的衍生题如果分数相同按姓名字典序升序怎么写比较器[](const Student a, const Student b) { if (a.score ! b.score) return a.score b.score; return a.name b.name; }这道题的另一个考察点是稳定性。std::sort是不稳定的排序std::stable_sort是稳定的。如果你有“相同分数的学生要保持输入顺序”这种需求必须用stable_sort。这个细节在实际业务里经常被忽略我见过线上跑着跑着数据顺序不对了排查半天发现是排序稳定性导致的。2.4 指针与引用的辨析这个模块我还会出一道看似送分实际送命的题const T*、T* const和const T* const的区别。很多工作两三年的朋友碰到这个题也要停顿一下。const T* p指向常量的指针*p不能改p可以指向别处。T* const p常量指针p不能指向别处*p可以改。const T* const p指向常量的常量指针两者都不能改。记忆技巧是从右往左读const修饰它左边的东西。const T*里const在最左边等价于T const*修饰的是T也就是指针指向的对象。这个解法和笔试里大量关于“函数参数什么时候加 const”“什么时候用引用”的题都相关。3. 算法与复杂度从排序到快速幂的推演算法模块我挑了三个最有代表性的题目冒泡排序、选择排序、快速幂。前两个是面试里最常问的基础排序第三个是算法竞赛和工程里都高频出现的优化技巧。热搜词里这几个都出现过确实说明是大家真正关心的问题。3.1 冒泡排序背下来之前先理解为什么冒泡排序的代码几乎人人都会背但大部分人不知道怎样才算“理解”了它。我要求练习的人必须能回答三个问题为什么外层循环要循环n-1次为什么内层循环比较到n-1-i就停了什么情况下冒泡排序的时间复杂度会变成 O(n)基础写法是这样的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]) { std::swap(arr[j], arr[j 1]); } } } }外层循环每轮结束最大的元素就会被“冒泡”到它最终的位置所以下一轮不需要再比较它。这就是为什么内层循环的上限是n-1-i。但如果数组本来就有序这个写法仍然要做n*(n-1)/2次比较。优化版用一个标志位提前退出void bubble_sort_optimized(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]) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; // 本轮没有任何交换说明已经有序 } }优化之后最好情况的时间复杂度确实变成了 O(n)。笔试如果只写到基础版面试官一般会让你“再想想能不能优化”这个优化思路就是加分项。3.2 选择排序稳定性的坑选择排序的思路更符合人的直觉每一轮找到剩余元素中的最小值放到当前轮次的位置。void selection_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int min_idx i; for (int j i 1; j n; j) { if (arr[j] arr[min_idx]) { min_idx j; } } if (min_idx ! i) { std::swap(arr[i], arr[min_idx]); } } }这里最值得记住的知识点是选择排序是不稳定排序。举个例子数组[5, 5, 3]第一轮找到最小值3和第一个5交换结果是[3, 5, 5]。原来第一个5在第二个5前面交换之后第一个5跑到第二个5后面了两个相同元素的相对顺序变了。冒泡排序是稳定的选择排序不是。这个差异在笔试里经常被单独拎出来问如果题目要求“按分数排序同分数保持原顺序”你就不能用选择排序。3.3 快速幂O(log n) 的优雅之处很多练习者看到快速幂觉得这是竞赛题工程里用不到。但实际上任何需要重复批量计算a^n的场景快速幂都有它的用处比如模运算加密、矩阵快速幂、斐波那契数列的高效求解。快速幂的核心思想很简单把指数看成二进制。比如3^10 3^(1010)_2 3^8 * 3^2你只需要做三次乘法而不是十次。迭代写法是这样的long long fast_pow(long long base, long long exp, long long mod) { long long result 1; base % mod; while (exp 0) { if (exp 1) { // 当前二进制位是 1 result result * base % mod; } base base * base % mod; // base 翻倍 exp 1; // 指数右移一位 } return result; }为什么base要不断base * base因为二进制下每右移一位意味着这一位的权重要平方一次。exp 1判断当前最低位是否为 1如果是说明这一位对最终结果有贡献就把当前的base乘进去。这个写法我还想强调一个坑如果mod大于2^31 - 1result * base会溢出 int所以参数类型必须用long long。笔试里经常有人在这里翻车代码看着没问题一提交就是 Wrong Answer。3.4 算法练习的核心方法在纸上推演最后一个关于算法的建议动手在纸上推演不要上来就敲代码。我练算法有个习惯拿到一道题先在草稿纸上画一个小的输入用例手动跑一遍算法把每一次交换、每一次递归调用都写下来。这个过程能逼着你理解算法的每一步在做什么而不是靠记忆把代码默写出来。面试的时候如果面试官让你写快速幂你能当场在白板上画一个3^10的二进制拆解过程这比直接写出代码的印象分高很多。平时练习花 80% 时间在推演上20% 时间在敲代码上效果远好于直接刷 100 道题。4. 现代 C 特性与实际工程落点如果只会写 C with Classes那距离“C/C 工程师”这个称号还有一段距离。现代 CC11 及以后引入的特性在工程里的使用频率非常高。这一章我从热搜词里选了几个最容易被问到的点constexpr、回调函数、ABA 问题、设计模式。4.1 constexpr从哪个版本引入各版本有什么能力热搜词里有一条“constexpr 哪个 c 版本引入的”这其实是面试里非常典型的“语法版本”问题。constexpr 是在C11引入的但它早期的能力非常有限。C 版本constexpr 能力代表限制C11只能修饰简单的返回语句和 constexpr 构造函数函数体只能有一条 return 语句C14放宽函数体内可以有局部变量、循环、if、switch大部分普通函数的写法都支持了C17constexpr 函数隐式成为 inline可以在 if 初始化中使用对 lambda 的支持增强C20constexpr 虚函数、constexpr try、在编译期使用动态内存实际上已经是“可在编译期执行的完整子集”实际工作中我最常用的场景是让一些常量在编译期就算好而不是运行时每次调用函数再算。比如constexpr long long factorial(int n) { long long result 1; for (int i 2; i n; i) { result * i; } return result; } constexpr long long val factorial(10); // 编译期就算好了注意在 C11 里这个写法是编译不过的因为循环不允许。所以如果你在旧代码里看到 constexpr 函数写得像“只有一条 return 的半残函数”不要惊讶那是版本限制。面试加分点说清楚constexpr和const的区别。const是“不允许修改”constexpr是“必须在编译期求值”。一个const变量可能是运行时初始化的但constexpr变量必须在编译期初始化。用constexpr修饰函数时它既可以被编译期调用也可以被运行时调用取决于参数是不是编译期常量。4.2 回调函数从函数指针到 lambda回调函数在工程里的使用太频繁了定时器超时回调、网络事件回调、排序比较器、UI 事件处理。热搜词“c 回调函数例子”说明这块确实是很多人实操时卡住的点。C 里实现回调有三种主流方式我建议你全部掌握方式一函数指针。#include iostream void on_event(int code) { std::cout event code: code std::endl; } void register_callback(void (*cb)(int)) { cb(42); // 模拟事件触发 } int main() { register_callback(on_event); return 0; }函数指针语法丑但效率最高适合对性能极其敏感的底层代码。方式二std::function。#include functional void register_callback(std::functionvoid(int) cb) { cb(42); }std::function可以保存任何可调用对象包括函数指针、lambda、函数对象。代价是有一定的性能损耗和内存分配。工程里用它的收益远大于损耗推荐日常开发使用。方式三lambda 表达式C11 引入。int threshold 10; register_callback([threshold](int code) { if (code threshold) { std::cout exceed threshold std::endl; } });lambda 的[threshold]是按值捕获外部变量[threshold]是按引用捕获。笔试里常问如果 lambda 要修改捕获的变量需要写mutable如果要在回调函数内部改变外部对象用引用捕获。我在实际工程里的建议是优先用 lambda std::function 的组合。既保持代码可读性又保持了灵活性。函数指针在需要跨越 C ABI 边界的时候用比如调用 C 库注册回调。4.3 ABA 问题并发编程的经典陷阱“ABA 问题”这个热搜词出现在 C/C 相关里说明很多人是在学习无锁编程或者并发容器的时候碰到了这个名词。简单说ABA 问题指的是线程 A 读取到内存值为 A线程 B 把值改成 B又改回 A线程 A CAS 时发现值还是 A就认为“数据没变过”继续操作但数据其实已经被改过了。用一个生活化的例子你出门前看了一下钱包在桌上离开后室友拿走钱包又放回原位你回来看到钱包还在就觉得“没人动过我的钱包”——但实际上钱包里的钱可能已经被拿走了。在 C 中解决 ABA 问题的经典方法是加版本号#include atomic struct Node { int data; Node* next; }; // 简化示意用指针版本号的方式做 CAS std::atomicuint64_t atomic_ptr_with_version; // 高 32 位存版本号低 32 位存指针 // 每次修改版本号 1CAS 时同时比较指针和版本号这个技巧在实际工程里的核心思想是不要只比较值要比较“值 变化次数”。如果变化次数也一致才能认为数据确实没被修改过。C20 里有std::atomic_ref等更高级的工具但版本号这个思路仍然是最通用、最好理解的。面试时你只要能把 ABA 问题的场景讲清楚再说明版本号方案基本就能过关。如果面试官继续追问你可以补充无锁编程在工程中只是少数极端场景下的选择大部分时候用互斥锁更安全、更容易维护。4.4 设计模式在 C 里的真实使用频率C 设计模式这个话题网上教程多如牛毛但很多人学完只记得“单例要 double-checked locking”到写代码时根本用不上。我的看法是设计模式要学但要用自己的话讲清楚“它解决了什么需求”而不是背 UML 类图。拿策略模式举例。假设你要写一个排序工具支持按不同规则排序。最笨的写法是枚举加 ifenum class SortType { BY_SCORE, BY_NAME, BY_AGE }; void sort_students(std::vectorStudent stu, SortType type) { switch (type) { case SortType::BY_SCORE: std::sort(stu.begin(), stu.end(), [](const Student a, const Student b) { return a.score b.score; }); break; case SortType::BY_NAME: // ... break; } }每增加一种排序规则就要改这个函数违背了开闭原则。策略模式的思路是把每种规则封装成一个独立的策略对象用std::function或虚函数接口来抽象using SortStrategy std::functionbool(const Student, const Student); void sort_students(std::vectorStudent stu, SortStrategy strategy) { std::sort(stu.begin(), stu.end(), strategy); } // 调用方自己决定用什么策略 sort_students(stu, [](const Student a, const Student b) { return a.score b.score; });你看用std::function实现策略模式比传统的虚函数接口要简洁得多。这也是现代 C 的风格能用高阶函数解决的问题就不必抽象出多余的类层次。设计模式是工具箱不是金科玉律。5. 综合实践把知识变成能跑的小项目语言特性、算法、设计模式如果只是单点练习很容易过两天就忘。我设计这套练习卷的时候特意留了一道综合题要求把学到的东西串起来。这道综合题和一些热搜词相关C 小游戏、爱心代码、C 语言文件读写操作。5.1 爱心代码从数学公式到控制台动画“爱心代码”在热搜词里出现频率很高确实是个适合新手练手的项目。它的核心不是 C 语法而是用代码表达一个数学图形。爱心曲线的参数方程是x 16 * sin^3(t) y 13 * cos(t) - 5 * cos(2t) - 2 * cos(3t) - cos(4t)其中t从 0 到 2π 变化。你可以用这个公式计算出心形轮廓上的点映射到控制台坐标系里用字符填充#include cmath #include iostream int main() { for (int y 15; y -15; --y) { for (int x -30; x 30; x) { // 将坐标映射到参数方程的参数区间 double t std::atan2(y / 15.0, x / 30.0); double px 16 * pow(sin(t), 3); double py 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t); // 判断当前点是否在爱心边界附近 double distance sqrt(pow((x / 30.0) - px / 16, 2) pow((y / 15.0) - py / 13, 2)); if (distance 0.2) { std::cout *; } else { std::cout ; } } std::cout \n; } return 0; }这个代码的难点在于坐标系的映射。你要把控制台有限的字符网格当成画布把数学公式计算出的坐标缩放到画布范围内。很多初学者卡在“为什么我算出来的点不在屏幕上”本质是坐标系没对齐。进阶玩法是加动画用std::this_thread::sleep_for控制帧率清屏后重新绘制就可以做一个会跳动的爱心。这个项目虽然没有什么实际业务价值但很适合用来熟悉标准库的时间和线程接口。5.2 C 语言文件读写基础但坑最多“c 语言文件读写操作代码”这个热搜词说明文件操作是很多人的知识盲区。文件读写看起来简单坑全在细节上。先看最基础的文件写入#include stdio.h int main() { FILE* fp fopen(data.txt, w); if (fp NULL) { perror(fopen failed); return 1; } fprintf(fp, hello, file\n); fclose(fp); return 0; }重点fopen返回的指针必须判断是否为NULL。文件可能因为权限不足、磁盘已满、路径不存在等各种原因打不开。我见过太多人在笔试里不判断NULL直接fprintf这在面试官看来就是防御性编程意识不足。fopen 的模式参数也经常考模式含义文件不存在时文件存在时r只读失败正常打开w只写创建清空原内容a追加创建在末尾追加r读写失败可读可写w读写创建清空原内容a读追加创建可读可追加如果你要用二进制模式写数据模式要加b比如wb。Windows 平台下文本模式和二进制模式有区别换行符转换Linux 下没有区别这也是一个经典的跨平台坑。文件读写还有一个高频考点二进制文件的写入和读取。以fwrite和fread为例typedef struct { int id; char name[32]; } Student; Student stu {1, Alice}; FILE* fp fopen(stu.bin, wb); fwrite(stu, sizeof(Student), 1, fp); fclose(fp); Student load; fp fopen(stu.bin, rb); fread(load, sizeof(Student), 1, fp); fclose(fp);这里要注意的是直接把结构体写入二进制文件会包含内存对齐引入的填充字节。如果结构体里含有指针那么写进去的是地址值下次运行指针指向的内存早就失效了。所以在设计磁盘文件格式时不能直接写结构体需要把每个字段单独序列化。这个细节是文件读写从“会写”到“能用于生产”的分水岭。5.3 综合练习一个最简单的学生成绩管理系统练习卷的最后一道综合题我建议做一个小工具用 C 语言实现一个学生成绩管理系统功能包含从文件读取学生信息、添加学生、按分数排序、输出结果、保存回文件。这道题把所有核心考点都串起来了文件操作读取和保存数据到磁盘结构体和数组存储学生信息排序算法按分数排序字符串函数处理姓名防御性编程判断文件打开是否成功、判断输入是否合法我见过很多练习者单独做题都会连在一起就手忙脚乱。原因在于他们平时都是“题目说什么就做什么”没有自己设计数据流和模块边界的经验。做这个综合题时建议你先把流程画出来程序启动读文件、进入主循环等待命令、每次操作后是否立即保存、退出时如何清理资源。把这些问题想清楚代码写起来反而快。6. 开发环境与工具链从 VSCode 配置到 C 盘清理最后一个模块和语法无关但对一个 C/C 工程师来说环境配置和问题排查能力同样是硬要求。热搜词里一大半都是环境类的问题vscode 配置 c/c 环境、npm 无法加载文件、C 盘满了怎么清理、Visual C Redistributable。6.1 VSCode 配置 C/C 环境最常见的三个错误现在很多人用 VSCode 写 C/C但在配置阶段就会卡住。核心流程其实只有三步装编译器、配置编译任务tasks.json、配置调试器launch.json。先说编译器选型。Windows 上最简单的方案是安装 MinGW-w64它自带 GCC 和 GDB开箱即用。如果你用 Visual Studio 的 MSVC 工具链需要额外配置环境变量而且命令行的使用方式和 GCC 有细微差别。我建议新手先走 MinGW-w64 这条路。配置 tasks.json 时最常见的错误是把args里的文件名写死{ version: 2.0.0, tasks: [ { label: build, type: cppbuild, command: g, args: [ ${fileDirname}/*.cpp, -o, ${fileDirname}/output.exe ], group: build } ] }${fileDirname}/*.cpp会编译当前文件所在目录下的所有 cpp 文件。如果你改成${file}那就只会编译当前打开的文件一旦文件之间有互相引用就会链接失败。配置 launch.json 时最常见的错误是miDebuggerPath写错位置。要填 gdb 可执行文件的完整路径比如C:/mingw64/bin/gdb.exe。注意用正斜杠Windows 的反斜杠在 JSON 里需要转义成\\。还有一个非常隐蔽的坑VSCode 的externalConsole设置为true时程序会弹出单独的终端窗口在调试时输入输出会更正常设置为false时程序在 VSCode 内置终端运行但某些控制台程序对输入的处理会异常。如果你调试时发现程序卡住不输出可以先检查这个配置项。6.2 npm.ps1 脚本执行权限问题热搜词里有条很具体的报错信息npm : 无法加载文件 c:\program files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这个问题不是 npm 的锅是 PowerShell 执行策略默认禁止运行脚本文件。解决方式有两种。第一种是临时在当前会话放开权限Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned这种方式只对当前 PowerShell 窗口有效关掉就恢复。第二种是永久修改当前用户的执行策略Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedRemoteSigned的含义是本地创建的脚本可以运行从网络下载的脚本必须有数字签名。这个安全性足够日常使用了。如果你不想和 PowerShell 纠缠直接打开 cmd 运行npm.cmd也行.cmd文件不受执行策略限制。这个技巧在 CI 环境里特别有用因为很多 CI 脚本默认是 cmd 执行的。6.3 开发者的 C 盘为什么总是不够用“C 盘满了怎么清理”“c 盘红了怎么清理 c 盘空间”这些热搜词背后是一个几乎每个开发者都遇到过的现实问题。C 盘空间不足很多开发工具会莫名其妙地报错比如编译器无法生成临时文件、npm 无法安装依赖。对于 C/C 开发者来说C 盘空间的大户通常有这么几个Visual Studio 安装目录动辄十几 GB 起步、Windows SDK、MinGW 或者 LLVM 工具链、npm 缓存、以及各种各样的临时文件。我建议先从最安全的地方开始清理# 清理临时文件目录 # 可以在资源管理器里直接删除 %TEMP% 下的内容也可以执行 del /q/f/s %TEMP%\*# 清理 npm 缓存 npm cache clean --force# 清理 Visual Studio 的旧版本缓存 # 使用磁盘清理工具cleanmgr.exe勾选Windows 更新清理和临时文件 cleanmgr这里有一个重要的安全提醒不要为了清理空间而去删除系统的WinSxS目录、System32目录或者任何你不确定用途的文件夹。我之前见过有人把C:\Windows\Temp里的文件全删了结果正常运行的程序也跟着出问题。最稳妥的方式是用 Windows 自带的“磁盘清理”和“存储感知”功能它们会扫描出可以安全删除的内容。另外有一个小技巧把开发工具的缓存目录迁移到其他盘。比如 npm 的全局缓存目录可以通过.npmrc文件指定cacheD:\npm-cache这样以后npm install产生的缓存就不会再占用 C 盘空间了。VSCode 的扩展缓存、MinGW 的下载包也都支持类似的自定义路径。凡是能迁到非系统盘的尽量迁出去C 盘只留给系统本身和必须装在系统盘的工具。6.4 Visual C Redistributable为什么总是装来装去很多 C/C 开发者会对“microsoft visual c redistributable”这个关键词有印象因为装各种软件时它经常被要求安装。它到底是什么简单说它是 VC 程序运行所需的运行时库集合包含了 C 运行时CRT、标准库、MFC/ATL 等 DLL。你用 Visual Studio 编译出的程序如果链接了动态运行时目标机器上就必须有对应版本的 Redistributable 才能跑。这也是为什么很多软件安装包会附带这个组件。实践建议机器上有最新版的 Redistributable 就够了它向后兼容绝大多数旧版本程序。如果你的程序部署到客户机器上发现缺少某个 DLL比如msvcp140.dll大概率是 Redistributable 版本太旧装个最新的补上就行。真正的 C/C 工程师也应该知道两种根除方式一是安装对应的 Redistributable二是让自己的程序静态链接 CRT这样目标机器就不需要额外装运行时了。静态链接的代价是可执行文件变大但对分发场景来说更省心。7. 最后几条经验这份练习卷整理出来之后我让团队里的几位同事也试了试。结果挺有意思工作五年的人未必比工作一年的做得更好因为练习卷里考的很多东西工作中不常用就会忘。但这恰恰说明了一个问题C/C 的知识体系是需要定期复习和巩固的不能指望吃老本。我个人对这份练习卷的使用建议是每周抽两个晚上每次一个模块做题加复盘分两周过完一整轮。过完一遍之后把你写错的题、卡壳的题单独标出来下一轮只做这些标记过的题。这样反复三轮基础就能打得比较扎实了。有一个现象我要特别说一句很多人在练习的时候写得很高兴但要他去读别人代码就难受要他去调试一个段错误更是如坐针毡。如果你也是这种情况建议把练习卷的某几道题故意改出几个 bug再自己去调试。见过足够多的段错误和内存泄漏之后你对指针和内存模型的理解会上一个台阶。我自己当年在练习字符串逆序这道题的时候第一批代码在strlen(s) - 1这个地方翻了车没有判空输入导致空字符串直接崩了。后来我给所有涉及字符串的题目都养成了一个习惯永远先检查边界条件。现在看代码 review我第一眼也是看边界分支有没有考虑全。这个习惯值回票价。这份综合练习卷不是终点它只是一个自测工具。真正的 C/C 功夫还是要靠日常写代码、读代码、调试代码慢慢磨。但如果你能把这套卷子从头到尾独立写完每一个坑都知道为什么是坑那至少说明你在基础层面已经超越了相当一部分求职者。剩下的就是去真实的工程里历练了。