Day108 C++基础题复盘:覆盖隐藏、二分边界与构建环境避坑
2026/9/15 9:09:24 网站建设 项目流程

1. 今日习题怎么选,为什么还在死磕基础题

1.1 Day108意味着什么

说实话,能把一份C++课后习题训练坚持到第108天,这本身就已经淘汰了大多数三分钟热度的学习者。Day108这个节点,既不是万事开头难的第一周,也不是考前突击的临时抱佛脚,而是一个已经形成惯性、进入稳定输出的阶段。到了这个天数,一天不敲几道题、不踩两个编译报错,反而会觉得手痒。

今天这批题并不是什么高深莫测的算法难题,而是围绕C++语法细节、经典排序查找、边界条件处理的基础题组合。可能有人会问,都学到Day108了,怎么还在做这些基础题?我的想法很简单:C++这门语言,基础题永远不是在"学",而是在"查漏"。很多你以为早就掌握的知识点,真正落到代码里,往往会在最不起眼的地方卡你一下。比如今天我就被一个字符串数组初始化的问题绊了十分钟,这种问题放在面试题里就是送分题,但手写的时候就是会出错。

今天的题量不算大,一共五道:一道冒泡排序的变体题,一道二分查找的边界题,一道质数判断的优化题,一道覆盖与隐藏的辨析题,还有一道字符串处理的综合题。这五道题基本覆盖了热词里大家最常搜的那几类:排序方式、二分查找、判断质数、C++覆盖隐藏、字符串数组初始化。做完之后我最大的感受是:这些题单独拎出来都不难,但把它们放在同一天做,你会开始注意到一些共通的思维习惯——边界条件的敏感度、对底层内存布局的感知、以及编译错误信息的解读能力。

1.2 今天选这五道题的理由

选题不是随手抓的,我通常遵循三个原则:第一,覆盖本周学过的语法点;第二,包含至少一道容易踩坑的边界题;第三,必须有能立刻应用到项目里的实战代码。今天这五道题正好对应了这三条。

冒泡排序变体题考察的是对排序稳定性和交换次数的理解,这道题表面考排序,实际上考的是循环边界的控制力。二分查找那道题更典型,是一个在旋转有序数组里找目标值的问题,这比普通二分查找多了一层条件判断,特别适合检验自己是不是真的理解了区间不变量的概念。质数判断则考的是数学优化,从朴素判断到开根号再到6的倍数规律,每层优化的背后都是一次对计算量的具体估算。

覆盖与隐藏那道题是一道典型的概念辨析题,C++的覆盖(override)、隐藏(hide)、重载(overload)这三个概念,几乎每次面试都会有人被问倒。我专门把这道题放在中间位置,就是想让自己在纯算法题之间穿插一道语法题,让大脑换个频道。最后一道字符串综合题,涉及字符串数组初始化、流式拼接、以及C++11之后常用的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"}; // 方式三:vector<string> std::vector<std::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(vector<int>& 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(vector<int>& 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^(8+4+1)。每次迭代时底数自乘,指数右移:

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-w64(g++),调试器用GDB,构建任务用tasks.json,调试配置用launch.json。

具体步骤大概是:

  1. 下载MinGW-w64,解压后把bin目录加到系统环境变量Path里,运行g++ --version验证。
  2. VS Code里安装C/C++扩展(Microsoft官方出的那个,扩展ID是ms-vscode.cpptools)。
  3. 在工作区.vscode文件夹下创建tasks.json,配置编译任务:
{ "version": "2.0.0", "tasks": [ { "label": "C++ Compile", "type": "cppbuild", "command": "g++", "args": [ "-fdiagnostics-color=always", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ], "group": "build", "problemMatcher": ["$gcc"] } ] }
  1. 创建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" } ] }

这两份配置的核心逻辑是:Ctrl+Shift+B触发编译任务,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无法解析的外部符号,这个在写多文件项目时特别常见。根本原因通常是声明了函数但没有定义,或者定义的函数与声明的签名不一致。排查思路就一个:Ctrl+Shift+F全工程搜函数名,确认定义和声明是否完全匹配,包括形参类型、const限定、命名空间。

5. 排错记录与刷题避坑清单

5.1 常见编译错误速查表

到了Day108这个阶段,我明显感觉到自己读编译报错的能力比第10天时强了不止一个档次。很多人怕编译报错,觉得那一大坨红字是打击自信心用的。其实编译器的每条提示都是在帮你,它精确指出了行号甚至列号,就差直接告诉你怎么改了。我把今天遇到的、以及C++刷题群里高频出现的编译错误整理成了一个速查表:

报错关键字常见原因排查路径
error C2146 / syntax error语法错误,通常缺分号、括号不匹配先看报错行,再往上看三行,问题往往在上面的声明里
C2065 未声明的标识符变量或类型没声明就使用检查是否漏include头文件、拼写是否一致
C2664 无法将参数转换函数参数类型不匹配检查实参和形参类型,必要时加static_cast
C4996 使用了不安全的函数调用了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天以上的同学,百分之九十都会经历一个心态变化,从最初的焦虑学不完,到中期的急功近利只想刷难题,再到现在的坦然接受"基础就是王道"。我今天做的五道题没有一道是竞赛级别的难题,但每一道都让我有新的收获。这种状态,我觉得才是刷题最健康的样子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询