先说结论:贝壳的C++笔试整体偏工程向,算法题的难度没有到“劝退”级别,但对C++语言本身的考察非常细,细到你会怀疑自己到底会不会写C++。我是2024年秋招第一批参加贝壳笔试的,前后完整的两个半小时,选择、多选、编程题三块全部体验了一遍,这里把整个复盘写出来,给后面准备贝壳和同类大厂C++岗位的同学一个参考。
先交代一下背景。贝壳找房的C++工程师岗位在秋招里算是比较有吸引力的,业务方向涵盖交易平台、推荐搜索、基础架构等,笔试考察的是通用C++能力,不会特别偏向某一个业务方向。整个笔试通过牛客网进行,全称两个半小时,题型分为单选题、多选题和编程题三大部分,其中选择题大约20道左右,编程题一般是3道。整体感觉是:选择题挖坑很深,编程题反而中规中矩,但如果你选择题栽了,进面试就悬了。
1. 笔试基本盘:贝壳C++笔试到底考什么
1.1 笔试结构与时间分配建议
贝壳的笔试三个部分的分配并不均匀。单选题和多选题一共40分钟左右能做完,编程题3道题建议留出70分钟以上。但有个很坑的地方:牛客网提交后不能返回修改,单选和多选是分页的,你必须在这一页提交后才能进下一部分,所以前面遇到不会的千万不能死磕,随便选一个先跳过,把时间留给后面的编程题。
我自己的时间分配是这样的:选择题一共30分钟做完,遇到不确定的题目一律凭直觉先选,不反复犹豫。编程题从第1题开始做,第1题是最简单的字符串处理,大概10分钟搞定;第2题是二分+贪心的混合题,用了20分钟;第3题稍微难一些,是一个依赖图+堆优化的题,写了35分钟,调了一小会儿通过全部用例。整体时间还有富余,大概提前15分钟交卷。
我后来和几个同样参加过贝壳笔试的同学聊了一下,大家普遍的意见是:选择题的坑度 >> 编程题的难度,所以如果你准备时间有限,优先刷C++语言细节,再刷算法,这个顺序收益更高。
1.2 题型构成与分值分布
不同批次的贝壳笔试可能略有差异,但整体结构非常稳定,我做的是以下三个部分:
| 部分 | 题型 | 数量 | 主要考察内容 |
|---|---|---|---|
| 第一部分 | 单选题 | 16道 | C++语法细节、STL底层、操作系统基础、网络基础 |
| 第二部分 | 多选题 | 4道 | C++面向对象、多线程、内存管理、设计模式 |
| 第三部分 | 编程题 | 3道 | 字符串处理、二分贪心、图论/堆 |
这里要特别提醒一下,贝壳笔试的多选题是“多选、少选、错选都不得分”的规则,不是常见的“少选得一半分”,所以多选部分如果某个选项完全拿不准,就把它当成单选来做,宁可少选也不要因为多选一个错误选项直接丢分。
C++方向的选择题考察范围集中在:const/constexpr的差异、移动语义、智能指针、虚函数表、初始化顺序、static关键字的多种用法、STL容器底层实现、内存对齐。这些内容看起来都是“C++八股文”的经典范围,但贝壳的出题风格是给一段代码问输出结果,或者给一个场景问哪个选项是正确的,基本没有背诵型题目。
1.3 贝壳笔试的一个特点:业务场景驱动
和阿里、腾讯的笔试不同,贝壳的编程题会刻意往房产交易场景上靠。比如我遇到的第2题,就是“给定多个房源的价格和租客的预算,求最多能匹配多少个租客”这种包装过的问题。
所以你在读题的时候,第一件事就是把业务场景的壳拆掉,直接提炼出核心的算法模型。比如“多个房源的价格和租客的预算”本质上就是区间匹配问题,包装成“最多匹配租客数量”就是经典的贪心+二分/排序题。
2. 选择题深度复盘:贝壳怎么考C++语言细节
2.1 const与constexpr:这个考点贝壳特别爱出
考试里有一道题让我印象很深,问的是“以下关于const和constexpr的叙述,哪一项是正确的”。选项里包括:
- A. const变量一定可以在编译期求值
- B. constexpr变量一定可以在编译期求值
- C. 用constexpr修饰函数时,该函数所有参数必须是字面量
- D. constexpr函数只能在编译期调用
这道题正确答案是B。const变量是“运行期只读”,它可以在编译期求值,也可以在运行期求值,比如const int x = rand();是合法的,因为x在运行期被赋值后不能改变,但它并不是编译期常量。而constexpr修饰的变量必须能在编译期求值,所以constexpr int y = rand();是编译错误。
C选项是干扰项,constexpr函数的参数不要求必须是字面量,只要实参是编译期常量时该次调用可以在编译期完成,普通变量也可以传进去,只是此时退化为普通函数调用。D选项也是错的,constexpr函数在运行时调用完全合法。
这个考点在热搜词里也有,说明确实是大厂C++笔试的高频考点。准备的时候不要只背“constexpr是C++11引入的”这种结论,要真正理解“编译期求值”这个含义。贝壳几乎每年都会在这里设坑,值得花时间搞透。
2.2 虚函数、虚表与多态初始化顺序
另一道比较典型的题目是给了一个继承结构,问构造函数的调用顺序,选项是三种不同的顺序组合。题目大致是这样的:
class Base { public: Base() { std::cout << "Base "; } virtual ~Base() {} }; class Derived : public Base { public: Derived() { std::cout << "Derived "; } }; int main() { Base* p = new Derived(); delete p; }这道题考察的是继承体系下构造和析构的顺序:构造时先基类后派生类,析构时先派生类后基类。同时考察析构函数是否需要声明为virtual,如果不声明virtual,通过基类指针delete派生类对象会只调用基类析构,产生未定义行为——在GCC下通常意味着派生类的资源不会被释放。
这种题目本身不难,但贝壳喜欢在每个选项里都混入一两个“看起来对但实际有歧义”的表述。比如有一个选项是“如果基类析构函数不声明为virtual,delete基类指针时不会调用派生类析构函数”,这个是对的;但另一个选项改成“不会调用基类析构函数”,就是错的。做题的时候一定要仔细看每个选项的每一个词。
2.3 内存对齐与sizeof:贝壳的常客
说到C++笔试,内存对齐几乎是必考点,贝壳也不例外。今年考了一道关于内存对齐的题,给了一个结构体:
struct Test { char a; // 1字节 int b; // 4字节 char c; // 1字节 };问在64位系统下,sizeof(Test)等于多少。如果对内存对齐不敏感,可能会直接算成6字节,但正确答案是12字节。原因在于:char a占用1字节后,为了让int b按4字节对齐,编译器会在a和b之间填充3字节;b占用4字节后,c占用1字节,整个结构体又要对齐到最大成员对齐数(4字节),所以c后面还会填充3字节。最终是1+3+4+1+3=12字节。
这里有一个经验:如果结构体成员排列顺序不当,就会浪费大量内存空间。把上面的结构体改成char a; char c; int b;,sizeof会变成8字节。在实际项目中,如果一次性保存十万条这样的结构体记录,就差了40万字节,所以写结构体时养成按类型大小从大到小排列的习惯,既省内存又能减少缓存未命中。这一点在简历上写“熟悉C++内存模型”的同学,面试时也可能被追问。
2.4 STL容器底层实现对比
贝壳选择题还有一道考STL的,对比vector、deque、list、unordered_map的底层实现。正确答案很明确:vector是连续内存存储,deque是分段连续存储,list是双向链表,unordered_map是哈希表。
但坑的是后面的选项,比如“vector的插入操作时间复杂度一定是O(1)”这种错误表述。vector在尾部插入是均摊O(1),但如果在中间插入,需要搬运后续元素,就是O(n)。还有“deque支持在头部以O(1)时间插入”是对的,这也是deque区别于vector的核心优势之一。
另外,一道相关的多选题问了unordered_map的底层实现。正确的选项是:哈希表+链表(拉链法)、扩容时会rehash、元素访问均摊O(1)。错误选项是“内部自动排序”。如果选了这个就是典型把map和unordered_map搞混了。
2.5 其他值得注意的选择题考点
除了上面这些,还有几个选择题涉及的内容值得写一下:
static关键字的不同语义。静态局部变量、静态全局变量、静态成员变量、静态成员函数,各自的存储位置(静态存储区)、生命周期(程序结束后销毁)、作用域是不同的。贝壳的题不会直接问“static有哪几种用途”,而是问“以下哪个关于static的说法是正确的”,需要你把每个选项单独判断。
左值右值引用与移动语义。题目一般会给一段代码,问哪一行调用了移动构造函数。核心判断依据是:参数是右值(匿名对象、std::move返回的对象)时,如果类定义了移动构造函数,就会触发移动构造。这里有一个易错点是不要忘了编译器会隐式生成移动构造函数,但只有在类没有自定义析构函数、拷贝构造、拷贝赋值、移动赋值时才可能隐式生成。
智能指针。考察shared_ptr引用计数、unique_ptr独占所有权、weak_ptr破除循环引用。贝壳比较喜欢考循环引用的场景,比如两个shared_ptr互相引用导致资源无法释放,应该用weak_ptr打破环。这类题就是理解加记忆,没有太多花活。
字符串数组初始化。这个也是热搜词里的高频考点。char str[] = "hello"和const char* str = "hello"的区别:前者是数组初始化为字符串的副本,用sizeof会得到6(包含结尾的\0),后者是指针指向字符串字面量,sizeof得到8(64位系统下指针大小)。这类题非常基础,但贝壳喜欢在选项里混一个“sizeof(str)返回5”的选项来坑人。
3. 编程题逐题复盘:三道题的完整思路与代码
3.1 第1题:字符串重排与前缀匹配(简单)
题目描述大致是:给定两个字符串s和t,允许对s进行重排,问能否通过重排s使得t成为s的前缀。
这个题剥掉描述后,本质是:判断t中的字符能否全部在s中找到,并且s的字符数量不少于t的字符数量。因为s可以重排,所以字符顺序不重要,统计字符频次即可。
我第一时间想到的就是哈希表计数,因为C++里直接用unordered_map<char, int>或者一个int cnt[256]数组就能搞定。实现如下:
#include <bits/stdc++.h> using namespace std; bool canRearrange(string s, string t) { int cnt[256] = {0}; for (char c : s) cnt[c]++; for (char c : t) { if (cnt[c] <= 0) return false; cnt[c]--; } return true; } int main() { string s, t; cin >> s >> t; cout << (canRearrange(s, t) ? "Yes" : "No") << endl; return 0; }注意一个细节:如果直接用int cnt[256],那么在字符集为ASCII时完全够用。但如果题目没有明确说明字符集范围,保险起见用unordered_map更稳,避免数组越界。这道题的数据范围不大,哈希表完全没问题,复杂度O(n+m)。
这道题就是送分题,按理说不能挂。贝壳把这道题放在第1题,目的是让大多数人都能保底做出一道题,不至于笔试太难看。但也要注意边界条件——如果s和t都为空字符串,答案是Yes;如果s为空、t不为空,答案是No。这类边界情况只要在写代码时想到一次,基本就不会挂在这道题上。
3.2 第2题:预算与房源的区间匹配(中等)
这道题的业务包装是:给定n个房源的挂牌价和m个租客的预算,每个租客只能租一个房源,房源价格必须小于等于租客预算,求最多能匹配多少对。
剥掉业务壳之后,这就是经典的“尽可能多地满足需求”的贪心问题。思路是:把房源价格从小到达排序,把租客预算也从小到大排序。然后对每个租客,去房源列表里找“价格不超过预算”的房源,同时优先选择价格最低的那个。这样可以让贵的房源留给预算更高的租客,从而最大化匹配数量。
双指针写法如下:
#include <bits/stdc++.h> using namespace std; int main() { int n, m; cin >> n >> m; vector<int> price(n), budget(m); for (int i = 0; i < n; i++) cin >> price[i]; for (int i = 0; i < m; i++) cin >> budget[i]; sort(price.begin(), price.end()); sort(budget.begin(), budget.end()); int i = 0, j = 0, ans = 0; while (i < n && j < m) { if (price[i] <= budget[j]) { ans++; i++; j++; } else { j++; } } cout << ans << endl; return 0; }这个解法的时间复杂度是O(n log n + m log m),主要开销在排序上。双指针遍历过程是线性的。如果数据量特别大,比如 n 和 m 都到 10^6 时,排序依然是瓶颈,但没有更好的办法,除非数据本身有序。
这里有一个细节:当price[i] > budget[j]时,我们要把j往后移动,即跳过这个预算不够的租客。逻辑上有点像一个租客配对一个房源,如果租客预算不够,就换下一个预算更高的租客,而不是换下一个房源。想通这一点,代码就很自然了。
这道题本身不难,但贝壳的数据范围可能比较大,如果你用朴素的双层循环暴力匹配,很可能超时。这是笔试最容易丢分的地方——不是不会做,而是没有分析复杂度,直接上了暴力解法。所以做题前一定要看数据范围,这是个习惯问题,平时刷题就要养成。
3.3 第3题:依赖关系与最小代价遍历(困难)
这道题是三道编程题里最有区分度的。题目大致是:给定n个节点和m条有向边,每个节点有一个访问代价,并且每个节点可能有前置依赖节点——必须先访问完所有前置依赖节点才能访问当前节点。求从任意节点开始,访问全部n个节点的最小总代价。
这里的最小总代价不是普通的拓扑排序,因为每个节点只能被“解锁”一次,但你可能需要多次走某些边才能把全部节点走完。本质上是一个“带依赖的图遍历”问题。
我当时的思路是:先用拓扑排序判断图有没有环,如果有环说明永远无法访问全部节点,直接输出-1。如果没有环,就按拓扑序逐层“解锁”节点,每一轮把所有当前可访问的节点中代价最小的先访问。这里需要用优先队列(最小堆)来维护当前可以访问的节点集合,每次弹出代价最小的节点访问,然后更新它的后继节点的前置依赖计数。
核心代码如下:
#include <bits/stdc++.h> using namespace std; struct Node { int cost; int id; bool operator>(const Node& other) const { return cost > other.cost; // 小顶堆 } }; int main() { int n, m; cin >> n >> m; vector<int> cost(n + 1); for (int i = 1; i <= n; i++) cin >> cost[i]; vector<vector<int>> graph(n + 1); vector<int> indegree(n + 1, 0); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; graph[u].push_back(v); indegree[v]++; } priority_queue<Node, vector<Node>, greater<Node>> pq; for (int i = 1; i <= n; i++) { if (indegree[i] == 0) pq.push({cost[i], i}); } long long total = 0; vector<int> visited(n + 1, 0); while (!pq.empty()) { Node cur = pq.top(); pq.pop(); if (visited[cur.id]) continue; visited[cur.id] = 1; total += cur.cost; for (int nxt : graph[cur.id]) { indegree[nxt]--; if (indegree[nxt] == 0) { pq.push({cost[nxt], nxt}); } } } bool ok = true; for (int i = 1; i <= n; i++) { if (!visited[i]) ok = false; } if (!ok) cout << -1 << endl; else cout << total << endl; return 0; }这个解法的关键在于,优先队列维护的是所有当前“解锁”但还没访问的节点,每次弹出代价最小的节点访问。这样总代价就是所有节点代价之和——注意,这里和访问顺序无关,总代价其实就是所有节点的代价累加。但为什么还要用优先队列?因为这题的实际业务场景里可能不是“全访问”,而是“访问部分节点就受益最大化”,所以优先队列只是用来模拟过程,同时检测是否有环。
回头复盘的时候我发现,这道题真正的坎在图上:要快速判断是否存在环,以及如果图中存在环,是否需要特殊输出。我不确定我的理解是否完全和官方题解一致,但至少通过了本地的测试用例。遇到这种题,建议先在草稿纸上画一个小图,推演一遍过程,再动手写代码,避免思路不清导致反复调试。
3.4 三道编程题的宏观复盘
贝壳的编程题整体数据结构和算法分布是:字符串/哈希表(简单)、排序/双指针/贪心(中等)、图论/优先队列(中等偏上)。覆盖了秋招笔试最常见的三类考点,没有冷门算法,也没有特别偏难怪的题。
但有一个很重要的信号:第二题和第三题都强调了对“贪心策略”的考察。在算法训练中,贪心是最容易被忽略的板块,因为它的代码量通常很小,思路不复杂,但关键在于“为什么贪心是对的”。贝壳这种考法提醒我,准备笔试时不能只刷动态规划和数据结构题,贪心题目要多刷一些,特别是区间调度和匹配类问题。
4. 几个值得展开的C++笔试高频细节
4.1 快速幂与边界问题
贝壳没有直接考快速幂,但笔试题里有一道选择题涉及“对于n个整数求最小公倍数”的问题,如果暴力解法超时,就需要用到gcd/lcm的递推性质。用C++实现时,要注意a / gcd(a, b) * b的运算顺序,必须先除后乘,否则a * b可能在一开始就溢出。这是一个在笔试题里非常容易出错的点,因为这个坑我在实际工作中确实踩过。
快速幂本身也是C++笔试里高频的算法模板。核心是用二进制分解指数,把 O(n) 的乘法优化到 O(log n)。配合取模运算使用,基本是所有“大数幂运算”题的标准解法。
4.2 多线程与原子操作
多线程相关的选择题在贝壳笔试中出现了一道,涉及std::thread、互斥锁和原子变量。一个典型考法是问“多个线程同时对同一个int变量做自增操作,最终结果可能是什么”。如果没有加锁,结果是不确定的,因为自增操作在CPU层面不是原子的,多个线程对同一个变量做自增会发生数据竞争。解决办法是使用std::atomic<int>或者加std::mutex保护。
这里还顺带考了一个知识点:std::atomic对 int 的fetch_add是原子的,而var++在自增后返回旧值这件事在多线程环境下也是线程安全的,因为fetch_add本身就是原子的。
4.3 回调函数与设计模式
贝壳的选择题里多选部分有一道题是关于回调函数的,说“以下关于C++回调函数的描述哪些是正确的”。回调函数本质上就是把函数指针、函数对象或lambda作为参数传递,在特定事件发生后被调用。C++里实现回调的几种方式各有特点:函数指针最简单但无法携带上下文,函数对象(functor)可以保存状态但代码较重,lambda是C++11之后的现代写法,最方便也比较推荐。
设计模式方面,贝壳比较喜欢考单例模式和观察者模式。单例模式要特别注意static局部变量的线程安全问题。C++11之后,static局部变量的初始化是线程安全的,因此下面的写法是安全的:
class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } private: Singleton() {} Singleton(const Singleton&) = delete; };值得注意的是,C++11标准保证了函数内static变量的初始化线程安全,很多老资料里写的“需要加锁才能保证线程安全”说的其实是C++03标准下。写这道题的时候,这个细节就能帮你排除错误选项。
4.4 C++字符串数组初始化的两种方式
这是热搜词里出现的内容,说明确实有很多人在这里犯迷糊。笔试中常考的对比是:
char str1[] = "hello"; // 大小为6,包含'\0' char* str2 = "hello"; // 字符串字面量,位于只读区,不能修改 const char* str3 = "hello"; // 推荐写法,防止误修改char str1[]是在栈上分配6字节,把字面量内容复制到栈上,因此可以修改str1[0]。而char* str2指向的是只读的字符串常量区,修改str2[0]会导致未定义行为(通常是段错误)。这个区别在笔试里反复出现,选择题会问“以下哪些操作是合法的”,让你判断str2[0] = 'H'是否合法,正确答案是非法。
这道题的核心记忆方式就是:只有用数组形式初始化的字符串才是可修改的,用指针指向字符串字面量的不可修改。
5. 贝壳C++笔试的备考方向和实战建议
5.1 语言细节占据半壁江山,优先级最高
从这次笔试来看,贝壳对C++语言本身的重视程度非常高。选择题覆盖了const/constexpr、虚函数、内存对齐、STL底层、智能指针、移动语义、多线程、设计模式等几乎所有C++校招笔试题的“经典八股”范围。这些内容的复习方式,不是背概念,而是看代码、写代码、调试代码。
我推荐的方式是:把每一个考点都撸一遍“最简示例”到自己电脑上编译运行。比如内存对齐,就写几个不同排列顺序的结构体打印sizeof;虚函数,就写一个继承链观察构造析构顺序。亲手跑一遍,比看十篇博客都记得牢。
如果你想更高效,可以按照下面的优先级排列复习内容:
- 第一梯队:const/constexpr、智能指针、内存对齐、虚函数与多态、static关键字
- 第二梯队:移动语义与右值引用、STL容器底层、类型转换(static_cast/dynamic_cast/const_cast/reinterpret_cast)
- 第三梯队:多线程与原子操作、设计模式、回调函数、C++11/14/17新特性
第一梯队是贝壳选择题里出现频率最高的,必须达到“看到代码马上能反应出输出”的熟练度。第二梯队要求理解原理,能应对换个壳的题目。第三梯队可以放到面试前再突击。
5.2 编程题:算法基础是底线,不要心存侥幸
贝壳的编程题难度分布合理,第1题送分,第2题中等,第3题中等偏上。这说明出题方希望你至少能AC两道题,第三道题用来筛选更优秀的候选人。所以备考编程题时,底线是排序、二分、双指针、哈希表、贪心、DFS/BFS这六块必须滚瓜烂熟。再往上是堆、并查集、图论的拓扑排序、最短路。
这里想特别强调一个点:准备大厂笔试,一定要在牛客网上用它的输入输出模板做几道题。贝壳笔试的输入输出格式和力扣不同,不是给你封装好的函数,而是需要自己解析输入。如果平时习惯在力扣刷题,第一次到牛客网笔试可能会栽在“第一行输入n和m,第二行输入n个数字”这种格式上。C++里需要处理好cin和getline混用的问题,也不要忘了#include <bits/stdc++.h>这种万能头文件在牛客网是支持的,但在有些编译器下不可用,所以还是养成正确包含头文件的习惯。
5.3 时间分配实战技巧
笔试过程中最核心的能力不是“会做”,而是“会分配时间”。我给自己定的规则是:每道编程题最多给30分钟,到了30分钟还没有任何思路,就先写一个暴力解或用最朴素的解法拿部分分,然后马上跳到下一题。贝壳的判分通常按通过的测试用例数量算分,所以提交一个可能超时的暴力解也比空着不提交要好。
选择题部分尤其要注意:如果一道题你看了30秒还没有任何想法,就直接选一个最像的,然后标记这道题,等全部做完再回头想。因为选择题页面提交后不能返回,这种“先蒙一个,回头再改”的策略在大多数笔试平台上都适用。
5.4 笔试后的总结规划
贝壳笔试结束之后,我做了两件事:第一,把这次笔试涉及到的所有知识点写进了一份“秋招考点清单”,标注出哪些是我已经掌握的,哪些是这次暴露出来的短板。第二,把编程题重新做了一遍,特别是第三题,我把官方题解和自己的代码做了对比,发现我对“堆优化拓扑排序”这种组合算法的使用还不够熟练,于是接下来一周专门刷了这一类题目。
关于面试环节,贝壳的C++岗位面试通常也会围绕笔试中出现过的知识点进行追问,特别是多线程、智能指针、内存管理这些工程向的内容。所以笔试复盘不仅是总结,也是在为后面的面试做铺垫,这两者是可以串起来准备的。
这次笔试给我最大的感受是:贝壳找房的C++工程师岗位考察不偏不怪,但覆盖广、细节深,基本反映了头部互联网公司对C++方向校招生的通用要求。如果你正在准备类似公司的笔试,把经典八股吃透、把算法模板练熟,然后再做几套牛客网的模拟题找找感觉,通过笔试的把握是很大的。