ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Day108 C++基础题复盘:覆盖隐藏、二分边界与构建环境避坑

Day108 C++基础题复盘:覆盖隐藏、二分边界与构建环境避坑 1. 今日习题怎么选为什么还在死磕基础题1.1 Day108意味着什么说实话能把一份C课后习题训练坚持到第108天这本身就已经淘汰了大多数三分钟热度的学习者。Day108这个节点既不是万事开头难的第一周也不是考前突击的临时抱佛脚而是一个已经形成惯性、进入稳定输出的阶段。到了这个天数一天不敲几道题、不踩两个编译报错反而会觉得手痒。今天这批题并不是什么高深莫测的算法难题而是围绕C语法细节、经典排序查找、边界条件处理的基础题组合。可能有人会问都学到Day108了怎么还在做这些基础题我的想法很简单C这门语言基础题永远不是在学而是在查漏。很多你以为早就掌握的知识点真正落到代码里往往会在最不起眼的地方卡你一下。比如今天我就被一个字符串数组初始化的问题绊了十分钟这种问题放在面试题里就是送分题但手写的时候就是会出错。今天的题量不算大一共五道一道冒泡排序的变体题一道二分查找的边界题一道质数判断的优化题一道覆盖与隐藏的辨析题还有一道字符串处理的综合题。这五道题基本覆盖了热词里大家最常搜的那几类排序方式、二分查找、判断质数、C覆盖隐藏、字符串数组初始化。做完之后我最大的感受是这些题单独拎出来都不难但把它们放在同一天做你会开始注意到一些共通的思维习惯——边界条件的敏感度、对底层内存布局的感知、以及编译错误信息的解读能力。1.2 今天选这五道题的理由选题不是随手抓的我通常遵循三个原则第一覆盖本周学过的语法点第二包含至少一道容易踩坑的边界题第三必须有能立刻应用到项目里的实战代码。今天这五道题正好对应了这三条。冒泡排序变体题考察的是对排序稳定性和交换次数的理解这道题表面考排序实际上考的是循环边界的控制力。二分查找那道题更典型是一个在旋转有序数组里找目标值的问题这比普通二分查找多了一层条件判断特别适合检验自己是不是真的理解了区间不变量的概念。质数判断则考的是数学优化从朴素判断到开根号再到6的倍数规律每层优化的背后都是一次对计算量的具体估算。覆盖与隐藏那道题是一道典型的概念辨析题C的覆盖override、隐藏hide、重载overload这三个概念几乎每次面试都会有人被问倒。我专门把这道题放在中间位置就是想让自己在纯算法题之间穿插一道语法题让大脑换个频道。最后一道字符串综合题涉及字符串数组初始化、流式拼接、以及C11之后常用的to_string转换属于考完就能直接用在项目里的类型。2. 覆盖、隐藏与重载一道课后题扯出的语法坑2.1 三个概念一次性讲透今天这道辨析题是这样出的基类Base里有一个virtual void print()派生类Derived里分别写了一个void print()、一个void print(int x)、还有一个void print() const问哪些构成覆盖哪些构成隐藏哪些是重载。很多初学者看到这道题就懵了因为三个概念长得太像。我用自己的理解把它们拆开讲一下。覆盖override的前提是基类函数是虚函数派生类函数签名完全一致而且基类函数的返回值类型相同或是协变类型。覆盖的意义在于实现多态也就是通过基类指针或引用调用时实际执行的是派生类的版本。这是C面向对象三大特性里多态的核心机制。隐藏hide则霸道得多只要派生类中出现了同名函数不管参数列表是否相同基类中的同名函数都会被藏起来。哪怕你只是想调用基类的void print()但只要派生类里有一个print(int x)编译器的名字查找规则就会直接屏蔽掉基类的所有同名函数。很多人第一次写继承代码时遇到的明明基类有这个函数却调用不到就是这个原因。重载overload则是在同一个作用域内函数名相同、参数列表不同的多个版本。注意关键词同一个作用域。一旦跨了类的作用域重载就不成立了取而代之的是隐藏规则。回到那道题Derived的void print()因为基类是虚函数、签名一致构成覆盖void print(int x)与基类print()参数不同不构成覆盖但因为同名构成隐藏void print() const则更有意思它因为const限定符不同签名也不一样不构成覆盖但同样因为同名而构成隐藏。我在做这道题时顺手补了一个小实验如果在派生类中使用using Base::print;就可以把基类的同名函数重新引入可见范围从而同时保留两个版本的重载关系。这个技巧在重写接口的时候特别实用比如你只想覆盖一个版本还想让其他版本继续可用。2.2 字符串数组初始化的隐藏陷阱字符串处理题里有一个很基础但错误率极高的问题怎么初始化一个字符串数组。我见过太多人在这个点上翻车包括今天我自己也差点写错。常见的写法有这几种// 方式一C风格字符数组 char str1[] hello; // 方式二string类的数组 std::string arr[3] {apple, banana, cherry}; // 方式三vectorstring std::vectorstd::string vec {apple, banana, cherry}; // 方式四指针数组 const char* ptrArr[3] {apple, banana, cherry};这些写法看起来都对但它们的内存布局和灵活程度差别很大。char str1[]是栈上分配定长数组大小由编译器推导实际占6字节结尾还有\0。std::string arr[3]是string对象数组每个元素内部管理自己的堆内存字符串大小可以动态变化。const char* ptrArr[3]存的是三个常量字符串的地址适合只读场景。今天这道题的陷阱在于题目要求把输入的字符串按逗号拆分后存到一个数组里然后统一输出。如果一上来就用const char* ptrArr[3]去接就麻烦了因为拆分出来的子串需要动态创建直接赋值给const char*会导致悬空指针。正确做法是用std::string arr[3]或者更推荐的std::vector std::string 。我刚开始没注意直接用char tokens[3][100]去接结果字符串一长就数组越界编译不报错运行才崩溃查了半天。这类问题在C里特别典型编译期越界基本不查运行期越界不一定会崩等到崩的时候你已经忘了是哪里越的界。所以我的习惯是涉及不确定长度的字符串处理一律用std::vector或std::array而非原始数组。这不是矫情是在帮自己省调试时间。3. 冒泡、二分、质数、快速幂今天手敲的四个经典实现3.1 冒泡排序变体题的优化边界今天的排序题不是单纯的冒泡排序而是在冒泡的基础上要求统计交换次数并且当某一轮没有发生任何交换时提前终止。这个变体考察的是对冒泡排序时间复杂度的理解最坏情况O(n²)最好情况O(n)前提就是提前终止。我写的第一版代码是这样的int bubbleSortCount(vectorint nums) { int n nums.size(); int swapCount 0; for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - i - 1; j) { if (nums[j] nums[j 1]) { swap(nums[j], nums[j 1]); swapped true; swapCount; } } if (!swapped) break; } return swapCount; }关键在于内层循环的边界n - i - 1。每一轮结束后最大的元素已经沉到末尾所以内层不需要再访问已经排序好的位置。这个边界我第一次写的时候写成了n - 1虽然结果也能算对但白白多做了很多次无意义的比较。对于100个元素可能无所谓但一旦数据量到10万级别差距就出来了。顺手补充一个热词里很多人问的c sort 引入库的问题工程实践中根本不需要手写冒泡排序直接用#include 里的std::sort就行。但为什么课后题还要练冒泡因为面试官问的往往不是怎么排序而是排序的稳定性是什么最坏情况时间复杂度是多少如何优化。基础排序算法是理解高级排序算法的阶梯不能跳。3.2 二分查找的边界处理技巧二分查找这道题让我认真想了一会。题目是在一个按升序排列但在某个未知位置旋转过的数组中查找目标值比如[4,5,6,7,0,1,2]中找1要求时间复杂度O(log n)。这道题的核心思路是把数组分成两段左递增段和右递增段。每次取mid之后先用nums[left]与nums[mid]比较判断mid落在哪一段然后再根据目标值与边界的关系收缩区间。我习惯用闭区间的写法因为左闭右闭区间的边界条件最直观。int search(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; if (nums[left] nums[mid]) { if (target nums[left] target nums[mid]) { right mid - 1; } else { left mid 1; } } else { if (target nums[mid] target nums[right]) { left mid 1; } else { right mid - 1; } } } return -1; }这里有几个容易错的地方。第一mid的写法要用left (right - left) / 2而不是(left right) / 2因为前者可以避免left和right都是很大的整数时相加溢出。第二判断条件nums[left] nums[mid]时这个等号必须带上否则当数组只有两个元素时会漏掉一种情况。第三每次收缩区间时已经比较过的mid位置要排除掉所以right mid - 1或left mid 1。我在调试这道题时特意打印了每一轮的left、right、mid值用一个只有4个元素的小数组手动演算了一遍。这种手算小数据的方法虽然是笨办法但特别有用能直观看到边界条件是怎么一步步收敛的。如果你也经常在二分查找上卡壳我强烈建议你把这个习惯捡起来。3.3 快速幂与质数判断的优化思路质数判断这道题是热词里判断质数c优化的核心应用场景。朴素写法是从2循环到n-1时间复杂度O(n)。稍微优化一点是循环到sqrt(n)也就是bool isPrime(int n) { if (n 2) return false; for (int i 2; i * i n; i) { if (n % i 0) return false; } return true; }注意这里的循环条件是i * i n不是i sqrt(n)。因为sqrt函数有浮点精度问题而ii是整数运算更可靠还省去了每次循环都调用sqrt的开销。不过i * i也有溢出的风险当n接近int上限时ii可能超过int范围。稳妥的写法是把i声明为long long或者用i n / i来代替。进阶版的6的倍数规律是这样的除了2和3以外所有质数都分布在6的倍数两侧也就是n % 6 1或n % 6 5。可以先排除掉其他情况再对6的倍数两侧的因子做判断。这个优化可以把循环次数减少到原来的三分之一适合需要判断大量数字是否质数的场景。快速幂算法今天也顺手复习了一遍它是热词榜里的常客。核心思想是把指数拆成二进制表示例如计算3的13次方13的二进制是1101也就是3^(841)。每次迭代时底数自乘指数右移long long fastPow(long long base, long long exp, long long mod) { long long result 1 % mod; base % mod; while (exp 0) { if (exp 1) { result result * base % mod; } base base * base % mod; exp 1; } return result; }快速幂的实用性极强尤其是配合取模使用比如在密码学、大数计算、组合数学求逆元的时候几乎处处可见。它的时间复杂度是O(log n)比朴素循环的O(n)快出好几个数量级。我在做质数相关题目时经常用费马小定理做素性测试底数幂的计算就需要快速幂打底。4. VS Code配置与MSVC报错今天的构建环境实录4.1 VS Code配置C/C环境的实操记录今天做字符串处理题时需要调试代码我把VS Code的C/C环境又重新配了一遍。这个话题在热词榜里常年霸榜说明新手卡在这一步的非常多。我分享一下我现在的配置流程照着做基本一次就能跑通。首先说结论VS Code本身不是一个IDE它只是一个编辑器编译和调试都依赖外部工具链。Windows下最省心的方案是编译器用MinGW-w64g调试器用GDB构建任务用tasks.json调试配置用launch.json。具体步骤大概是下载MinGW-w64解压后把bin目录加到系统环境变量Path里运行g --version验证。VS Code里安装C/C扩展Microsoft官方出的那个扩展ID是ms-vscode.cpptools。在工作区.vscode文件夹下创建tasks.json配置编译任务{ version: 2.0.0, tasks: [ { label: C Compile, type: cppbuild, command: g, args: [ -fdiagnostics-coloralways, -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: build, problemMatcher: [$gcc] } ] }创建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: true, MIMode: gdb, miDebuggerPath: gdb, setupCommands: [ { description: Enable pretty-printing for gdb, text: -enable-pretty-printing, ignoreFailures: true } ], preLaunchTask: C Compile } ] }这两份配置的核心逻辑是CtrlShiftB触发编译任务F5触发调试调试前自动执行preLaunchTask完成编译。externalConsole设为true的意思是让程序运行在外部控制台窗口这样可以在程序里用cin读入数据不会因为VS Code内置终端交互不便而困扰。4.2 那些年让我们头疼的MSVC报错热词里有个报错非常典型error: microsoft visual c 14.0 or greater is required. Get it with Microsoft C Build Tools。这个报错我早期遇到过很多次主要出现在用pip安装某些Python包时因为部分第三方库的Python扩展是用C写的安装时需要编译源码于是就得调用MSVC编译器。如果你机器上没装Visual Studio或Build Tools就会直接报这个错误。解决办法很简单去微软官网下载Microsoft C Build Tools安装时勾选使用C的桌面开发工作负载。装完之后重开终端再执行一次pip install一般就能顺利编译了。这里有个小坑安装完Build Tools后原来已经打开的终端不会自动刷新环境变量必须新开一个终端窗口否则仍然会报找不到cl.exe之类的错。另一个高频报错是C4996也就是使用strcpy、scanf等被认为不安全的函数时MSVC会提示你换用带_s后缀的安全版本。这类报错本身不影响编译但会中断构建过程。解决办法是代码最上方加#define _CRT_SECURE_NO_WARNINGS或者不推荐偷懒但在刷题时很实用的一种写法是把编译警告等级调低。我自己的态度是课后练习阶段无所谓能用就行但如果是写生产代码还是老老实实用安全函数毕竟编译器提示都是有理由的。还有一类报错是LNK2019无法解析的外部符号这个在写多文件项目时特别常见。根本原因通常是声明了函数但没有定义或者定义的函数与声明的签名不一致。排查思路就一个CtrlShiftF全工程搜函数名确认定义和声明是否完全匹配包括形参类型、const限定、命名空间。5. 排错记录与刷题避坑清单5.1 常见编译错误速查表到了Day108这个阶段我明显感觉到自己读编译报错的能力比第10天时强了不止一个档次。很多人怕编译报错觉得那一大坨红字是打击自信心用的。其实编译器的每条提示都是在帮你它精确指出了行号甚至列号就差直接告诉你怎么改了。我把今天遇到的、以及C刷题群里高频出现的编译错误整理成了一个速查表报错关键字常见原因排查路径error C2146 / syntax error语法错误通常缺分号、括号不匹配先看报错行再往上看三行问题往往在上面的声明里C2065 未声明的标识符变量或类型没声明就使用检查是否漏include头文件、拼写是否一致C2664 无法将参数转换函数参数类型不匹配检查实参和形参类型必要时加static_castC4996 使用了不安全的函数调用了strcpy/scanf等旧函数加#define _CRT_SECURE_NO_WARNINGS或改安全版本LNK2019 无法解析的外部符号声明了没有定义确认链接的源文件是否加入编译undefined reference toxxxg编译时未链接对应库链接数学库加-lm线程库加-lpthread这张表是我从上百条报错记录里提炼出来的基本覆盖了新手阶段80%的编译问题。遇到不认识的新报错我一般会先把报错信息完整复制到搜索引擎里搜同时把出错的代码片段缩小到最简单独编译测试。这个最小化复现的方法是程序员的基本功。5.2 给刷题新手的几条实在建议刷了108天题最想分享的不是某个算法怎么解而是一套适合自己的复盘方法。第一题不在多在精。我一天的题量控制在五道左右但每道做完都会重新看一遍自己的代码思考三个问题还能不能优化时间/空间复杂度、边界条件有没有覆盖全、如果写成可复用的函数该怎么抽象。这三个问题比单纯刷数量有价值得多。第二纸上推演比直接编码更快。遇到复杂一点的逻辑先拿纸笔画一画数组的下标变化想清楚再动手写。我统计过自己写代码的时间分布直接上手写的方案平均要调试十五分钟先在纸上演算的方案平均三分钟就能一次通过。第三学会读报错信息而不是慌了神。编译器的报错信息里藏着黄金。每次报错先看行号再看错误类型最后才复制去搜索。逐步建立自己的报错经验库后面遇到类似的错误一眼就能定位。第四C的坑大多来自内存管理。无论是数组越界、悬空指针还是迭代器失效归根结底都是对内存布局和对象生命周期理解不够。建议每学一个容器都去了解它的底层数据结构vector是连续内存、list是链表节点、unordered_map是哈希桶这些底子迟早会派上用场。今天还顺手梳理了C和Java在类继承上的区别、C和Python的构建差异这些对比类的知识在面试里很容易被问到。它们的共同逻辑是语言特性背后都是设计取舍理解了为什么这么设计八股文根本不需要背。Day108的收尾我想说一个自己的观察坚持刷题到100天以上的同学百分之九十都会经历一个心态变化从最初的焦虑学不完到中期的急功近利只想刷难题再到现在的坦然接受基础就是王道。我今天做的五道题没有一道是竞赛级别的难题但每一道都让我有新的收获。这种状态我觉得才是刷题最健康的样子。
返回列表