迅雷C++笔试B卷复盘:底层基础决定成败,从内存到STL
2026/8/30 7:08:00 网站建设 项目流程

2018年秋招那阵,我趁着金九银十投了一波互联网公司,其中就有迅雷。当时在线测评平台弹出来的是“2018迅雷校园招聘C++在线笔试B卷”,整场做完最大的感受是:这家公司是真的在用C++的底子筛人。不像某些厂一上来就是天马行空的智力题,迅雷这套题更多是把C++语言特性、内存布局、STL底层、网络和操作系统基础全部揉在一起,再加上两道不那么虐但很容易翻车的编程题。说白了,它考的不是你会不会写代码,而是你写的那行代码在机器里到底发生了什么。

这份复盘我当年就写在了自己的笔记里,后来又结合网上流传的版本和同届同学对题目的回忆,把考点重新梳理过一遍。现在回头看,这套B卷虽然出自2018,但其中的命题思路放到今天依然有很强的参考价值。无论你现在是准备2025届春招,还是未来打算投C++后端、客户端、音视频、存储这类岗位,这套题的复习方向都很值得对着过一遍。

在展开之前先给结论:迅雷这套B卷真正拉开差距的,不是偏题怪题,而是最基础的概念有没有吃透,以及编程题边界处理得干不干净。掌握了这一点,整个备考节奏就会清晰很多。

1. 笔试题型结构与整体思路

1.1 题型构成与时间分布

网上能翻到的原始题目不完整,不过结合我做B卷时的记忆和考后群里对题的情况,大致可以还原出这套卷的结构。整体分为选择题和编程题两大部分,选择题里又分单选和多选,排名不分先后,但分布上比较有规律。C++语言相关的内容大概占了一半,数据结构与算法占了两三成,剩下的是网络、操作系统和少量Linux基础。

我当时那场的时间安排大约是90到120分钟,题量不算小。选择题大概在20到25道左右,前面大部分是单选,中间混着几道多选,最后两道编程题,每题分值占比很高。编程题一道偏字符串和数学处理,一道偏数据结构和算法设计,整体难度在LeetCode中等偏下水平,但想要拿满分需要把边界情况想得很全。第一轮笔试刷人的主要依据就是编程题的通过率和选择题正确率,所以时间分配非常重要。

一个比较实用的策略是:拿到卷子先用两三分钟把所有题目扫一遍,特别是把编程题的两道题都先读了。读题的时间放在前面,心里有个底,后面做选择题的时候会不自觉地在脑子里跑编程题的思路,等真正写代码时已经比较有雏形了。选择题的时间控制在40分钟左右,不要在一道多选上死磕,实在拿不准先标记跳过,编程题留出充足时间才是重点。

1.2 命题风格:基础优先,但基础得很深

迅雷做下载引擎、分布式存储这类底层技术,代码量很大,而且大量逻辑是 C/C++ 直接操作内存和文件系统。这也直接反映在了笔试风格上:它不太像某些公司那样热衷于出偏门算法题,而是特别爱问“这个语法背后发生了什么”。比如空类为什么占一个字节、vector扩容的底层行为、栈对象和堆对象的析构时机区别、TCP连接释放时TIME_WAIT的作用,这类题不出则已,一出就是连续好几道。

这种风格可以类比成厨师考试,它不考你花式雕刻萝卜花,而是考你刀工是否扎实、颠勺是否熟练、火候是否拿捏得准。看似基础,但越基础的东西越能暴露一个人是不是真的写过很多C++代码,还是只在刷题网站里写过若干道算法题。我当时做完这套卷子的感受就是:如果只是背了八股没有实际内存和性能意识,选择题里至少有五六道会觉得模棱两可。

所以这套B卷的复习导向非常明确:把C++的核心机制当成一门需要“真正会用”的技能去学,而不是当作考点列表去背。后面的所有内容,也都是围绕这个思路展开的。

2. C++核心考点深度解析

2.1 指针、引用与内存管理:B卷的“题眼”

指针对C++工程师来说就像呼吸一样自然,但每次笔试都能考出新花样。B卷里关于指针和引用的题目大概占了三到四道,主要围绕这几个点。

第一是引用和指针的区别。引用必须在定义时初始化,之后不能再绑定到其他对象;而指针可以重新赋值。引用在底层实现上通常就是一个指针,但在语义层面它被当作原对象的别名来用。笔试题最喜欢问的形式是“下面哪种操作是非法的”,选项里给出各种引用和指针的组合,一不小心就会掉坑。

第二是堆和栈的差异。栈上对象随作用域自动构造和析构,堆对象必须手动释放。笔试中常见的扩展题是:在函数里new一个对象返回给调用方,谁负责delete?如果中间抛出异常怎么办?这背后其实引出了RAII和智能指针的重要性。B卷里还考过内存泄漏和野指针的场景判断,比如int* p; *p = 10;这种未初始化指针解引用的危险操作,一看就知道是常驻考点。

第三是裸指针和智能指针的选择。C++11之后,unique_ptr、shared_ptr、weak_ptr已经成了面试和笔试题里的常客。B卷里有道题问weak_ptr的主要用途,答案是解决shared_ptr相互引用导致的循环引用问题。这个考点虽然基础,但很多人只是背了结论,真到代码层面遇到两个对象互相持有shared_ptr时,未必能第一时间想到用weak_ptr来打破环。

从笔试角度看,指针和内存管理这块的复习核心,不是背概念,而是能在草稿纸上画出“一个对象从构造到析构的完整内存旅程”,包括栈指针怎么动、堆分配器怎么找空闲块、虚函数表指针存在哪。能把这张图画出来,相关题目基本就不会错。

2.2 面向对象:构造、析构与多态的“隐藏陷阱”

面向对象这块,B卷选择题的密集程度很高。我记得有一道题是问:有继承关系时,派生类对象创建和销毁的过程中,构造函数和析构函数的调用顺序是什么。答案是先基类构造,再成员对象构造,最后派生类构造自身;析构顺序正好完全反过来。但如果基类析构函数没有声明为virtual,那么通过基类指针delete派生类对象时,只会调用基类析构,派生类里可能持有的堆资源就不会被释放,这题我在考后对答案时印象很深。

另一个高频点是虚函数和纯虚函数。虚函数实现多态,底层通过vptr指向虚函数表来实现动态绑定;纯虚函数则让类变成抽象类,不能直接实例化。选择题里可能会给一个类声明了几个虚函数、几个纯虚函数,然后问这个类的大小是多少。这类题本质在考vptr在对象内存中的占用,一般机器上是一个指针大小(8字节)。

此外,static关键字在C++里也是个爱考的点。static局部变量生命周期是整个程序,static类成员属于类共享,static成员函数没有this指针。笔试中常见的形式是把static和const混在一起考,比如问“const static成员变量怎么初始化”,这就要区分C++11前后语法差异。

面向对象这块要特别注意:多态不仅是笔试题,更是后续面试手写代码时不自觉就会用到的能力。我在备考时会自己写一个小程序,用一个基类指针数组管理几种派生类对象,然后调用同一个虚函数观察行为差异,跑一遍比背十次结论都管用。B卷的命题风格恰恰说明了这一点,它考的就是你是否真的理解这个机制,而不是背过一句话。

2.3 STL底层原理:容器选择背后的性能账

STL是C++笔试的一个大板块,B卷里出现了好几道和底层实现相关的题。最经典的就是vector的扩容机制。vector底层是一块连续内存,当 size 达到 capacity 时再 push_back 就会触发扩容,常见做法是重新申请一块更大的内存,把旧元素拷贝或移动过去,再释放旧内存。扩容倍数通常是1.5倍或2倍,具体看编译器实现。这道题喜欢引申出“为什么是倍增而不是每次加1”,原因很简单,均摊时间复杂度保持在O(1),如果每次只加一个元素,连续插入n次会退化成O(n²)。

map和unordered_map的选择也是重头戏。map底层是红黑树,按键有序排列,操作复杂度O(log n);unordered_map底层是哈希表,平均O(1)但无序。笔试会问“如果需要按顺序遍历键值对,应该选哪个”,显然选map。反过来如果需要大量查找且不关心顺序,unordered_map通常更快。哈希表还有一个考点是哈希冲突的解决办法,开放定址法和链地址法都需要了解。

迭代器失效这块也特别容易失分。vector在插入或删除之后,指向被操作位置及之后元素的迭代器可能全部失效,因为底层内存可能重新分配了;deque的插入删除在某些位置也会让迭代器失效;list因为节点独立,插入删除不影响其他迭代器。B卷可能会给一段代码,问哪个位置上的迭代器在操作后还可用,这种题一旦对底层不熟就会懵。我建议把每个常用容器的迭代器失效规则整理成一张表,考前过一遍,笔试时可以省很多纠结时间。

另外,string和char数组的比较也值得留意。char数组是固定大小,容易越界;string自动管理内存,提供c_str()返回底层字符指针。笔试中可能考到const char* p = str.c_str()之后修改了str,p就失效的风险。这也是C++为什么推荐多用string的原因之一。

2.4 多线程、C++11新特性与设计模式:拉开差距的地方

B卷里出现多线程和C++11的题目时,我愣了一下,因为2018年不少公司的笔试题还停留在老C++标准。迅雷这套卷能把这些内容放进来,看得出是真的在招写底层服务的人。

多线程考点主要集中在锁和原子操作。std::mutex配合lock_guard或unique_lock使用,能避免忘记unlock的问题。笔试会问lock_guard和unique_lock的区别,lock_guard构造时加锁、析构时解锁,不支持手动控制;unique_lock更灵活,可以延迟加锁、手动解锁,开销稍大。此外还有死锁产生的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。死锁的经典问法是“怎么破坏其中一个条件”,这个需要会描述,不需要写完整代码。

原子操作这块,B卷虽然没直接那么深,但网络热词里出现了“ABA问题C++”,这里值得展开一下。ABA问题发生在无锁编程中的CAS(Compare-And-Swap)操作上。线程读取值为A,准备比较交换;另一个线程先把值从A改成B,又改回A;此时第一个线程CAS时发现值还是A,就误以为没人修改过,实际中间已经变过。解决思路通常是加一个版本号或引用计数,让每次修改都能被识别,比如std::atomic配合一个递增的tag。这类知识在实习面试中更容易被问到,但笔试选择题偶尔也会冒出来作为多选题。

C++11的新特性是另一个得分点,auto、decltype、constexpr、lambda、右值引用和移动语义都有可能被考到。笔试喜欢问constexpr和const的区别,constexpr强调编译期常量求值,可用于数组大小、模板非类型参数等;const则是运行期常量语义。lambda则是结合回调来考,问捕获方式:按值捕获还是按引用捕获,以及捕获的变量在lambda生命周期内的有效性。如果lambda在异步任务里被调用,捕获引用变量可能已经被析构,这就成了悬垂引用,非常危险。

设计模式在笔试中分值不高,但面试容易聊。最常考的是单例模式,特别是线程安全的写法。有两种推荐方案:一是C++11之后用函数内局部静态变量,初始化是线程安全的;二是使用std::call_once保证单次初始化。笔试多选题可能会给出几种单例写法,问哪些是线程安全的,这时候就得会分辨双重检查锁写法里的datal race风险。B卷之后系统的设计题如果展开,还会涉及观察者模式,但笔试阶段知道单例和观察者的基本概念就够了。

2.5 C++八股高频题速查

说到“C++八股”,很多同学不屑一顾,但在迅雷B卷这种在线笔试里,八股题反而是最快能抢分的题型。因为选择题时间有限,如果每个语法点都要现场推导,20多道题根本做不完。这里整理几个出现频率极高的问题,建议复习时直接背清楚。

问题标准答案要点
空类的大小是多少?通常是1字节,保证不同对象有不同地址
sizeof指针是多少?64位系统一般是8字节,和指向类型无关
什么是野指针?指向已释放或者未初始化内存的指针
new和malloc的区别?new自动调用构造/析构、按类型分配,malloc只管分配字节
函数指针怎么写?int (*fp)(int, int),或使用std::function包装
const和constexpr区别?const是运行期语义,constexpr强调编译期求值
为什么构造函数初始化列表更快?可以避免先默认构造再赋值,直接用参数初始化成员

这些“八股”不是用来死记硬背的,而是帮你把C++整个知识体系里最容易出选择填空的点先捞起来。我当时备考时把这些点做成一个excel表格,每天抽二十个选项自测,效果比反复看大段教程好很多。

3. 高频算法题实战拆解

3.1 字符串与数字:从ASCII处理到整行输入

B卷的编程题第一道大概率是字符串和数字结合的方向。网上这类的题很多,比如给一行字符串提取出所有整数、然后求最大最小值或最小公倍数之类。这类题不考复杂算法,考的是对输入输出和ASCII处理的熟练度。

先给一道经典的“字符串转整型数组”问题。输入是逗号分隔的一串数字,比如"12,345,67,8",需要转换为vector<int>。手写代码如下:

#include <iostream> #include <vector> #include <string> #include <cctype> using namespace std; vector<int> parseNumbers(const string& s) { vector<int> res; int num = 0; bool inNum = false; for (char c : s) { if (isdigit(c)) { num = num * 10 + (c - '0'); inNum = true; } else { if (inNum) { res.push_back(num); num = 0; inNum = false; } } } if (inNum) { res.push_back(num); } return res; }

这里有两个细节值得注意。第一是isdigit(c)需要包含<cctype>头文件;第二是循环结束后如果最后一个字符还是数字,inNum为true,要再 push 一次,否则最后一个数会丢失。这种边界就是笔试最常考的“隐藏分点”。

另一个字符串相关的高频坑是输入方式。在线笔试的输入经常是“多组测试数据”,处理器会写成:

int n; while (cin >> n) { // 处理一组数据 }

cin >> n在读不到数据时会返回false,流被置为错误状态,循环自动退出。这招在LeetCode上用不到,但在公司笔试里是保命技。还有getlinecin >>混用时,读完数字后如果有换行符残留,需要先调用一次getline把换行吃掉,否则后续读取会直接拿到空字符串。

这里我还想提一下热词里的“n个整数的最小公倍数怎么求c++”,这类题在笔试里经常作为编程题主体出现。最小公倍数可以通过最大公约数来算,先写一个欧几里得算法求gcd,然后两个数的最小公倍数就是a / gcd(a, b) * b,注意要先除后乘,防止中间溢出。多扩展到n个数就是两两累积,模板如下:

long long gcd(long long a, long long b) { return b == 0 ? a : gcd(b, a % b); } long long lcm(long long a, long long b) { return a / gcd(a, b) * b; } long long nLcm(const vector<long long>& nums) { long long res = 1; for (long long x : nums) { res = lcm(res, x); } return res; }

如果把范围扩展到n个整数,一个容易忽略的点是:如果数组中有0,最小公倍数定义会变复杂。笔试题一般不会这么出,但自己写的时候最好和面试官确认清楚边界。另外如果数字范围很大,long long都不够用,还需要考虑大数据、求质因数分解等扩展方案,不过在线笔试一般不会考到这个程度。

3.2 排序与查找:别只背快排,要看懂为什么快

排序算法几乎每次笔试都会沾边。B卷的选择题里很可能会出现关于排序稳定性和复杂度的判断题。快速排序平均O(n log n)但非稳定排序;归并排序稳定且同样O(n log n)但需要额外O(n)空间;堆排序O(n log n)但非稳定且常数较大;冒泡排序O(n²),但因为写起来直观,常被拿来当教材示例。

笔试编程题不太会直接要求“写一个快排”,更多是把排序作为解题的一个中间步骤。比如数据分析题,要求按某个字段排序然后取前k个,就可能用到std::sort配合自定义比较函数。自定义比较函数有个坑:必须满足“严格弱排序”,也就是不能同时返回a<b和b<a为真,否则sort行为未定义,可能崩溃。

二分查找也经常出现,特别是数组有序时找目标值或找边界。手写二分的时候最容易出错的地方是区间开闭和循环条件。推荐一个写法:

int lowerBound(vector<int>& nums, int target) { int left = 0, right = nums.size(); // [left, right) while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < target) left = mid + 1; else right = mid; } return left; }

这里用左闭右开区间,mid直接取left + (right - left) / 2,避免(left+right)/2可能溢出的问题。笔试时如果不想临时推边界,就把这个模板背熟,遇到“第一个不小于target的位置”直接套。

排序和查找之后,往往就是数据结构题。以B卷的题量,很少会专门考一个红黑树实现,但STL容器选择这类“数据结构应用”的题几乎必出。所以复习时了解每种数据结构的适用场景,比背源码更划算。

3.3 动态规划:笔试最常考的三类

动态规划在迅雷B卷里不一定出,但作为备选编程题,几乎每个大厂都有概率考到。我建议至少要掌握三类基础的DP模型,因为它们足够短小,又覆盖了最核心的DP思维。

第一是最大子段和。给定一个整数数组,求连续子数组的最大和。经典解法是线性扫描,维护当前子数组和cur,每来一个新元素,要么延续之前的段(cur + x),要么从这个元素重新开始(x),最终取最大值:

#include <climits> int maxSubArray(vector<int>& nums) { int cur = 0, best = INT_MIN; for (int x : nums) { cur = max(x, cur + x); best = max(best, cur); } return best; }

这个算法的思想是“不回头地延续最优前缀”,很容易扩展成二维最大子矩阵。

第二是找最长上升子序列(LIS)。O(n²)的dp定义是dp[i]表示以第i个元素结尾的LIS长度,转移时遍历前面所有小于nums[i]的元素更新。O(n log n)的进阶做法是维护一个单调数组tails,每个位置维护当前长度下最小的结尾值,然后通过二分更新。笔试如果考动态规划,能在O(n²)和O(n log n)两种方法里都给出代码,是一个很大的加分项。

第三是编辑距离。两个字符串分别删除、替换,使得它们相等的操作数,是经典的二维DP,状态转移方程网上随便一搜就有。这里我重点强调的是初始化:dp[0][j]和dp[i][0]分别表示空串时的操作次数,这个值一错,全盘皆输。

动态规划的复习不需要刷几百道题,把这三种模型吃透,再了解“背包问题”的基本状态定义,对笔试足够了。因为大多数在线笔试的DP题都倾向于某种经典模型的小变形,核心是看你能不能识别出“子问题”。

3.4 两个“模板级”算法:快速幂与单调栈

网络热词里的“快速幂算法c++”和“单调栈算法c++”都很有代表性,因为这两个算法是笔试编程题里的“性价比之王”:代码短、思路固定、出题频率高。快速幂常在不经意间出现在求大数模幂、矩阵幂等场景,单调栈则用于解决“下一个更大元素”“接雨水”“柱状图中最大矩形”这类题。

先看快速幂模板:

long long powerMod(long long a, long long b, long long p) { long long res = 1; while (b > 0) { if (b & 1) res = res * a % p; a = a * a % p; b >>= 1; } return res; }

核心思想是把指数b按二进制拆位,每次处理一位,底数平方,遇到二进制位为1就乘进来。时间复杂度从O(b)降到O(log b)。笔试题如果要求模一个很大的质数,取模运算千万不要漏,不然中间结果爆炸。另外注意指数为0时返回1,这已经由初始res=1覆盖了。

单调栈模板以“每个元素右侧第一个比它大的数”为例:

vector<int> nextGreater(vector<int>& nums) { int n = nums.size(); vector<int> ans(n, -1); stack<int> st; // 存下标 for (int i = 0; i < n; ++i) { while (!st.empty() && nums[st.top()] < nums[i]) { ans[st.top()] = nums[i]; st.pop(); } st.push(i); } return ans; }

单调栈的妙处在于,每个元素最多入栈一次、出栈一次,所以整体是O(n)。笔试时只要写出“维护一个单调递减栈”的思路,代码照着模板改就行。这个算法虽然短,但第一次接触可能很难理解,建议自己打印几个用例手推一遍栈的变化。

这两个算法都属于“背下来就能用”的范畴,又不至于像树套树那样冷门,很适合在B卷这类中等偏简单、但时间紧张的考试中作为快速攻坚手段。

4. 网络、操作系统与底层原理

4.1 TCP连接管理:三次握手与四次挥手

网络在B卷选择题里通常占两三道,重点集中在TCP。最经典的是三次握手为什么是三次而不是两次,因为客户端发送SYN、服务端回复SYN+ACK、客户端再发ACK,这能避免旧连接请求突然到达服务端后建立无用连接。选择题里会给一个场景,问第三次握手丢包了会发生什么,服务端会认为连接未建立,超时后重发SYN+ACK。

四次挥手则涉及TIME_WAIT。主动关闭方在发送最后一个ACK后进入TIME_WAIT状态,等待2MSL后才完全关闭。原因有两个:一是保证最后一个ACK能到达对方,如果丢失可以重发;二是让旧连接的报文在网络中自然消失,避免影响新连接。笔试容易把状态转换写成选择题,问主动关闭方在收到FIN后处于什么状态,答案一般是FIN_WAIT_2,最后进入TIME_WAIT。

对C++开发来说,网络编程还需要知道TCP和UDP的区别、阻塞和非阻塞、select/poll/epoll的基本概念。迅雷这类做下载和传输的公司,对网络IO模型明显更上心,笔试虽然没有直接让写epoll代码,但选择题可能会问epoll相比select的优势,答案是epoll使用事件驱动、没有fd数量限制、不用每次拷贝fd集合。

4.2 进程线程与虚拟内存布局

操作系统题里,进程和线程的区别是必考项。进程是资源分配的最小单位,线程是CPU调度的最小单位。同一进程内多个线程共享代码段、数据段、堆,但有独立的栈和寄存器上下文。笔试选择题喜欢问“下面哪个不是线程共享的”,选项通常是栈和寄存器,这两个是线程私有。

内存布局也是高频考点。一个C++进程的虚拟地址空间从低地址到高地址大致分为:代码段、已初始化数据段、未初始化数据段(BSS)、堆、共享库映射区、栈,以及内核区。栈向低地址增长,堆向高地址增长。笔试可能考“局部变量、全局变量、静态变量、malloc出来的变量分别存储在哪个区”,这就必须记清楚。全局变量和静态变量在数据段,malloc/new在堆,局部变量在栈,常量字符串一般在代码段或只读数据段。

这些知识在选择里占比不高,但如果你目标是C++后端岗,网络和OS就是绕不开的基础。笔试中网络和操作系统可以快速刷选,面试时往往要展开深聊,现在打好底不会亏。

4.3 一道经典的“系统设计”题:LRU缓存

迅雷B卷的编程题如果往“系统设计”方向靠,最可能的形式之一是实现一个LRU缓存。这类题既能考代码能力,又能考对常用数据结构的理解,非常适合作为在线笔试第二题。

LRU全称是Least Recently Used,最近最少使用淘汰策略。核心要求是get和put都尽量达到O(1)时间复杂度。做法是哈希表加双向链表,哈希表负责快速定位节点,双向链表负责维护访问顺序。每次get某个键,就把对应节点移到链表头部;每次put新键,如果容量满就删除链表尾部节点。

这里的关键点是:为什么不用vector而用双向链表?因为vector中间删除是O(n),而双向链表删除给定节点是O(1),配合哈希表直接拿到节点指针,就能在O(1)内完成移动和删除。

考虑到笔试时间紧张,完整实现双向链表需要写很多代码,我当时采取的策略是先用注释写清楚思路,再实现核心结构。但如果是要求完整可运行的代码,考前必须把这道经典题练熟,能把链表节点的prev和next指针关系一次性写对。

5. 编程题作答与代码优化技巧

5.1 在线笔试环境下的输入输出细节

迅雷B卷是在线笔试平台做的,这种平台的编程题往往比LeetCode更接近传统OJ风格:需要自己处理输入输出,而且输入格式可能不固定。LeetCode习惯了核心代码模式的人,第一次接触OJ式笔试很容易懵,因为main函数都要自己写。

第一个要攻克的是读取整行字符串。如果输入是一行数字,中间用空格分隔,用cin >>逐个读取就行。但如果一行内是逗号分隔、或者包含引号、括号之类的字符,就建议直接getline(cin, line)读整行,再按照分隔符解析。注意getline之前如果有cin >> n的操作,一定要先用getline(cin, tmp)把换行消费掉,否则读到的第一个字符串是空的。

第二个是循环处理多组数据,也就是while (cin >> n)。有些平台要求读到文件尾结束,此时不能死等某个特殊值,得用流状态判断。如果一直不结束还会导致超时,所以处理逻辑要保证输入结束时能自然退出循环。

第三个是输出格式。题目要求每个结果占一行,就不要多打空行;要求用空格分隔多个数,最后一个数后通常不能有尾随空格。这类细节罚分很冤,但每年都有一堆人挂在上面。

5.2 边界条件与数据范围:避免“能跑但不对”

编程题最常见的失分点不是思路不对,而是边界没考虑全。在线笔试的评测用例通常会包含很多边界数据:空数组、只有1个元素、全是负数、数据量达到上限等。B卷那两道编程题显然也在边界上布置了陷阱。

一个很典型的例子是求和类问题。如果不加思考用int存结果,遇到比较大的测试用例就会溢出,改成long long就对了。所以拿到编程题,先看数据范围,如果数值可能超过2^31-1,直接用long long,不要等发生溢出再回头改。

另一个边界是个数不足。比如求n个整数的最小公倍数,n可能为0或者1。n=1时结果就是那个数本身;n=0时怎么定义得看题目要求,通常可以输出0或按约定处理。写代码时先处理这些极端情况,能避免大面积超时和运行时错误。

数组越界也很常见。用下标访问的时候,要么正确判断边界,要么使用at()函数抛异常(但OJ里一般不推荐)。二分查找、滑动窗口这类算法最容易在left/right移动时写出死循环或越界访问。我养成的习惯是:写完一段循环,手动跑一遍长度为1和长度为2的用例,确认不会出错。

5.3 避免低级失误的三个经验

在线笔试不像本地IDE那样有智能提示,也没有编译器的连续报错提示,很多平时依赖IDE补全的习惯在这里会很吃亏。我总结三个经验,虽然不是算法本身,但能实打实减少失误。

第一是变量命名尽量短小但可读。笔试现场不追求工程级命名,nmsumcurans这类短名就够了,重点是不要让自己写着写着忘了哪个变量是干嘛的。如果命名是resresult混着用,到自己检查时容易乱。

第二是写完后自己再造几个测试用例。平台允许本地编译的话,先跑一遍题目给的样例,再自己想一个边界用例和一个大数据用例。如果平台没有本地运行功能,就人工在草稿纸上模拟一遍关键逻辑。这一步可以过滤掉绝大多数“想法正确、实现有误”的低级bug。

第三是先保证正确性,再考虑优化。在线笔试的编程题通常数据范围不会大到需要极致优化,暴力解法如果在时间限制内能过,就先用暴力写出来。快速幂、单调栈这种模板算法固然好,但如果记不熟,硬套反而容易出错。考试目标是通过,不是比拼代码最优。

6. 考后复盘与备考建议

6.1 我踩过的坑:从“都会”到“做错”

这套B卷我虽然最后顺利进入下一轮面试,但复盘时发现丢分的地方都不是真正的知识盲区,而是“会但没想清楚”的细节。最大的一个坑是选择题里关于vector扩容的问题,题目问扩容后原来的迭代器是否有效,我下意识选了“是”,因为认为旧内存里的数据会被移动走,新内存里的迭代器应该已经失效了——实际正是失效的,我选反了。这提醒我,STL的性能特性必须结合“内存重新分配”这一事实去推导,不能凭感觉。

编程题也暴露过问题。有一道字符串提取数字的题,我写的时候没有注意到输入可能包含负号,导致负数全部被拆成数字处理。在线笔试的评测用例不会给你明确指出这种疏漏,只会悄悄报WA。之后再遇到这类题,我会在读入时增加对正负号和分隔符的判断。

还有一个经验是关于时间的。B卷选择题部分我做得比较快,但多选里有一道跨进程通信的题让我犹豫了很久,占用了编程题的部分时间。后来吸取教训:做选择题时遇到不确定的多选,先标记跳过去,不要在一道题上消耗超过三分钟,编程题拿满分比选择题多拿几分更重要。

6.2 针对这套B卷的高效复习路线

如果你正在准备类似迅雷B卷这种C++开发岗笔试,我建议复习路线按优先级分成三层。

第一层是C++语言本身。把指针和引用、虚函数和多态、内存布局、STL容器底层、C++11常用新特性全部过一遍,同时配合高频八股题速查。这一层是选择题的基础,优先级最高,差不多占去复习时间的四成。

第二层是算法和数据结构。不需要追求ACM难度,把数组、链表、栈、队列、哈希表、二叉树这些基础结构学扎实,掌握常见的排序、二分、双指针、滑动窗口、单调栈、动态规划入门模板即可。刷题量控制在LeetCode Hot 100这个规模,重点是每道题能讲清楚思路。这一层占三成时间。

第三层是网络和操作系统。复习TCP三次握手和四次挥手、进程线程区别、内存布局、IO模型。这一层占两成时间,因为笔试占比没有前两层大,但面试几乎一定会聊到。剩下的一成时间用来做项目复盘,准备自我介绍和项目细节。

B卷的特殊之处在于,它的选择题考得非常“底层”,所以刷题之外,一定要写一些小实验验证自己的理解。自己写一个继承体系打印构造析构顺序、自己写一个shared_ptr循环引用的demo然后用weak_ptr解决,这些动手实验比单纯背结论更能抗住笔试的多种变形。

6.3 给下一届同学的办法:一张自查表

最后分享一个我在多次笔试中逐渐完善出来的临场自查方法。它不是什么高深技巧,却能在大约五分钟内帮我把粗心错误率降到最低。

比如C++笔试里,代码写完后我会按这几点快速检查:

  • 头文件是否齐全,特别是<vector><string><algorithm><stack>这些常用头;
  • 是否所有变量都有初始值,尤其是定义的指针和局部数组;
  • 数组越界风险,循环边界是<还是<=,字符串是否以'\0'结尾;
  • 输入输出是否和样例格式完全一致,有没有多打印空格或换行;
  • 全部结果是否用long long存储,会不会溢出;
  • 是否存在多组输入,循环是否能在文件结束时正确退出。

这些点单独看都很基础,但考试压力下真的容易漏。把它们固化成一份checklist,每次提交前快速过一遍,效果比我盯着屏幕发愣要好得多。笔试考察的不仅是知识储备,更是稳定输出能力。C++岗尤其如此,因为这门语言给了程序员太多控制权,相应地也给了太多犯错的角落。能把细节控制好的人,往往就是公司想找的人。

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

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

立即咨询