我是在三月初收到贝壳找房春招C++工程师笔试通知的。当时人在学校,手里同时压着几份笔试要做,但贝壳这份我没有敷衍——不是我有多大的把握,而是房产交易这类重业务、高并发的场景,后端C++岗位考的东西往往比一般业务部门更“硬”。做完之后我花了两天时间复盘,把整套试卷的考法、出题意图、踩坑点给捋了一遍。这篇东西就当作一份个人向的笔试题拆解,给后面要面贝壳或者同类大厂C++岗的同学做个参考。
这套2023年贝壳找房春招C++工程师笔试卷,整体给我的感觉不是“难”,而是“密”。它不靠一道压轴题把人卡死,而是通过大量覆盖基础、中间穿插细节陷阱、最后用场景设计题拉区分度。换句话说,它考的不是“你会不会写代码”,而是“你在真实工程里能不能用对代码”。这一点贯穿了整张卷子,也是今天复盘想跟你说清楚的核心。
1. 笔试整体布局与出题逻辑拆解
1.1 试卷模块与考察维度
从卷面结构上看,这套题大致可以分成三个模块。第一个模块是C++语言基础,以选择题和简答题为主,涉及智能指针、内存管理、移动语义、多态实现原理、STL容器底层机制等。第二个模块是算法与数据结构,以编程题为主,题型集中在数组、链表、字符串、动态规划和单调栈这些经典方向上。第三个模块是系统与场景设计,数量不多,但一道题的分值往往顶得上五道选择题。
这三个模块并不是孤立存在的。语言基础题里会藏算法思想,算法题里又会考到STL容器选型,场景题更是所有知识点的汇合点。我做完复盘后最大的感受是:贝壳的出题人不太关心你背了多少八股文,他们更在乎你在限定条件下能不能做出合理的技术判断。
1.2 贝壳为什么要这样出题
贝壳找房的业务本质是房产信息聚合服务和交易撮合平台,后端服务要处理的数据量级大、实时性要求高。房源信息的上架、搜索、推荐、排序,再到带看、签约、交易,每个环节都需要大量C++后端服务支撑。在这种业务背景下,出题人自然会重点关注三个能力:第一是高并发场景下对内存和资源的精准控制能力;第二是算法能力能不能真正落到搜索、排序、去重这些实际业务上;第三是对多线程、网络通信这些系统底层知识的掌握程度。
理解了这一点,再看这套试卷就不难发现,它的每一个考点都是“能映射到真实业务”的。比如考察智能指针,背后是线上服务的内存泄漏问题;考察epoll网络模型,背后是成千上万的房源检索请求如何处理;考察单调栈,背后是价格区间、涨跌趋势这类分析需求。所以我建议你准备这类笔试的时候,别只刷题,还要多想一步“这个知识点在贝壳的业务场景里用在哪”,这对场景设计题的帮助非常大。
2. C++核心考点:从语法到工程化能力
2.1 智能指针与RAII:送分题还是送命题
智能指针是C++岗笔试里出现频率最高的考点之一,这套卷子也不例外。题目会从“shared_ptr是否线程安全”“unique_ptr如何作为函数参数传递”“weak_ptr如何解决循环引用”这些角度切入。很多人看到这几个问题就觉得稳了,但实际做题的时候容易在细节上翻车。
先说shared_ptr线程安全的问题。正确理解是:shared_ptr的引用计数本身是线程安全的,但其所指向的对象数据不是线程安全的。换句话说,多个线程同时拷贝同一个shared_ptr不会导致计数错乱,但如果多个线程通过shared_ptr访问同一个对象并修改对象内容,那是需要加锁的。这是个非常经典的坑,很多人把“引用计数安全”误理解成“对象安全”。
再就是循环引用。我记得有一道题专门考了双向链表节点用shared_ptr互相指向的后果。两个节点互相持有shared_ptr,引用计数就永远降不到零,内存永远不会释放。解法是让其中一边用weak_ptr。这个知识点本身不难,但笔试里经常和“内存泄漏”的判断混在一起出,稍一粗心就会选错。
如果你有余力,建议自己手动实现一个简化版的shared_ptr,不用太完整,但引用计数、析构释放、拷贝构造这些核心逻辑要能写出来。笔试编程题有时候不会直接让你写智能指针,但会考察你对资源管理的理解,手写一遍后记忆会深很多。
2.2 移动语义、左值右值与完美转发
移动语义几乎是C++11以后所有大厂笔试必考的内容,这套卷子的选择题里专门考了std::move和std::forward的区别。很多初学者会把这两者搞混,因为名字太像了。其实std::move的作用是把左值无条件转换为右值引用,从而触发移动构造或移动赋值;而std::forward是根据模板参数类型推导结果,有条件地将参数转换为左值引用或右值引用,它的核心应用场景是完美转发。
笔试里我最担心的是出“让一个vector多次push_back后,reserve预留多少”这类结合移动语义的问题。比如一个类同时定义了拷贝构造和移动构造,push_back临时对象时会优先调用移动构造而不是拷贝构造,性能差距非常明显。有的同学在追问“为什么这里不调用拷贝构造”时答不上来,本质是对“右值引用参与重载决议”的机制不熟。
要彻底掌握这块,可以记住一个判断口诀:左值有名字,右值没名字。看到std::move就知道后面跟的是左值但想按右值处理,看到T&&在模板参数里就要考虑引用折叠。C++11之后新增的右值引用、移动构造、移动赋值,本质上是为了解决“临时对象拷贝成本过高”的问题,笔试里反复考察,是因为实际工程的性能优化经常会用到。
2.3 多态、虚函数与构造析构顺序
多态这块,笔试里一般不会只考“虚函数是什么”,而是考更底层的实现原理和边界情况。比如“虚函数表存在哪里”“一个类有虚函数,编译器会为它生成什么额外成员”“虚继承的构造顺序是怎样的”“析构函数什么时候必须声明为虚函数”。
我记得其中有一道简答题问的是“基类析构函数不是虚函数会发生什么”。答案是:通过基类指针delete派生类对象时,只会调用基类的析构函数,派生类的析构函数不会被调用,导致派生类中申请的资源无法释放。这一点在工程上非常致命,所以笔试里会反复强调:只要一个类有虚函数,析构函数就应当声明为virtual。
构造析构顺序也是一个高频考点。构造时先走基类构造函数,再走成员对象构造函数,最后执行类自身的构造函数体;析构时顺序完全反过来。看起来简单,但代码里一旦有继承、组合、虚继承同时出现,顺序问题就非常容易出错。建议专门拿几道题练一练,把“构造自基类向派生类,析构自派生类向基类”这个规则刻进脑子里。
2.4 constexpr、C++标准版本与语言演进
热词里出现了“constexpr哪个C++版本引入的”,这其实是个很典型的笔试选择题。constexpr是C++11引入的关键字,用于指示变量或函数可以在编译期求值。C++14放宽了函数体内限制,C++17引入了if constexpr,C++20又进一步支持了consteval、constinit等。出题人如果不是专门考语言标准演进,一般不会问得太细,但基本版本要知道,考到的时候别说反。
我个人觉得,C++11/14/17这些新特性在笔试里越来越重要。因为贝壳这类公司的C++后端项目大多已经切到C++14/17标准,你要是只懂C++98的语法,面对“auto、lambda、std::unordered_map、移动语义”这些问题会直接懵。准备笔试的时候,至少要把C++11的核心特性全部过一遍:auto类型推导、范围for、nullptr、智能指针、移动语义、lambda表达式、std::function、std::thread、unordered系列容器、std::shared_mutex,等等。
2.5 字符串处理:送分题里的隐藏陷阱
字符串相关的题,看起来比智能指针、多态简单,但反而是整套卷子里最容易因为细节丢分的部分。热词里出现的“c++字符串数组初始化”“c++字符串转数组”“c++读入整行”就是典型的笔试考点。
先说读入整行。如果题目输入是一行带空格的字符串,用cin >> s只能读到第一个空格之前的内容,需要用std::getline才行。但getline有个坑:它之前如果用了cin读取整数,缓冲区里会残留一个换行符,导致getline直接读到一个空字符串。正确的做法是在cin和getline之间调用一次“忽略命令”清掉残留换行符。这个坑在笔试编程题里太常见了,每年都有人在这里折腾半天。
字符串转数组这块,主要考察的是对字符数组和std::string的底层理解。char数组的赋值不能直接arr = "hello",要用strcpy或者std::string的构造函数;字符串字面量是const char[]类型,不能轻易转成char*;strlen不包括结尾的'\0',sizeof则会包含。这些细节单拿出来都不难,但组合到一道题里,答错率马上上来了。
3. 算法与数据结构:重点题型与解题思路
3.1 排序算法:基础但不一定能写对
排序算法在笔试里一般不单独考,但会作为前置步骤出现在各种题目里。热词里“冒泡排序算法c++”“选择排序c++”频繁出现,说明很多人对基础排序的理解还停留在“背代码”的阶段。我建议你重新审视一遍这些基础排序的边界条件:冒泡排序的内层循环范围是什么?选择排序每一轮交换发生在什么时候?快速排序的partition遇到相等元素怎么处理?
这其中快速排序的边界最容易出错。经典写法是选取最后一个元素作为基准,用双指针进行partition。笔试里如果让你手写快排,建议用闭区间、边界条件写清楚,避免一上来就写“while(i <= j)”然后死活跑不对。还有一个更隐蔽的问题是:快排在最坏情况下时间复杂度退化为O(n^2),如果题目数据量很大,需要加上随机化处理。
排序算法本身的代码量不大,但笔试里的排序题往往要你分析时间复杂度和空间复杂度。比如归并排序的额外空间复杂度是O(n),快速排序是O(logn)栈空间,堆排序是O(1)额外空间但稳定性不好。弄懂这些,就能应对各种变体题目。
3.2 链表、快慢指针与结构体基础
链表是算法笔试里的常青树,热词里“c++结构体链表基本语法”说明很多人连链表节点定义都还没完全掌握。C++里定义链表节点一般用struct或class都可以,笔试编程题如果用ACM模式,需要自己定义节点结构体。这里有一个小细节:struct里默认成员是public的,class里默认是private的,用class定义节点时如果忘了写public,后面的代码会直接编译报错。
快慢指针是链表题最常用的套路。判断链表是否有环、找到环的入口、找到链表中间节点、找到倒数第k个节点,都可以用快慢指针解决。笔试里如果出“判断链表是否有环并找入口”,你不仅要知道“快指针每次走两步,慢指针每次走一步”,还要能解释“为什么两者相遇后,从相遇点和头节点同时出发,每次走一步,最终会在环入口相遇”。这个推导过程很多人栽过,但其实就是数学里的追及问题,写一写速度差和时间关系就清楚了。
3.3 单调栈:热词里的高频考点
热词里出现了“单调栈算法c++”,这个数据结构在笔试里确实是个分水岭。很多人会暴力的双重循环,但一旦数据范围到10^5以上就超时。单调栈可以在O(n)时间内解决“找下一个更大元素/上一个更小元素”这类问题。
单调栈的基本思想是维护一个栈内元素单调递增或递减的栈,在入栈和出栈的过程中记录答案。以“下一个更大元素”为例,我们从左到右遍历数组,维护一个栈底到栈顶单调递减的栈。当前元素大于栈顶元素时,说明当前元素是栈顶元素的“下一个更大元素”,弹栈并记录答案。
我记得这套卷子里有一道单调栈的应用题,给了一段房价或者房源数量序列,要求计算某个区间内的统计量。这类题只要你掌握了“弹栈时结算信息”的套路,解题速度会快很多。强烈建议把单调栈的经典题目刷一遍,特别是“柱状图中最大的矩形”和“接雨水”,前者是单调栈应用的典型代表,后者能帮你彻底理解栈内元素为什么需要保持单调性。
3.4 动态规划:从暴力递归到状态压缩
动态规划题在整套卷子里占的分数不低,而且往往是区分度比较大的题。普通的“青蛙跳台阶”“最长公共子序列”可能只是热身,真正的拉开差距的题目通常带有一定状态设计难度,需要你找到合适的dp维度。
我做动态规划题的习惯是三步走:第一步,找出状态定义,思考“dp[i]代表什么”;第二步,找出状态转移方程,思考“dp[i]怎么由dp[i-1]或更早的dp[j]推出来”;第三步,找出初始条件和遍历顺序。这里最有迷惑性的是遍历顺序,比如“最长上升子序列”是外层遍历i、内层遍历j,而“01背包”则必须保证内层循环从大到小遍历容量,否则同一个物品会被重复使用。
如果题目是二维dp,还可以考虑优化空间复杂度。比如“最长公共子序列”只用两个一维数组就能完成转移,因为当前行只依赖上一行。笔试里如果写了O(n^2)空间也能过,但你能写出空间优化版本,在面试追问环节会非常加分。
3.5 快速幂与数论小常识
热词里“快速幂算法c++”和“n个整数的最小公倍数怎么求c++”出现得很集中。快速幂是笔试里很有意思的一块内容,它本身代码很短,但思想非常经典——把指数看成二进制,每次平方底数,根据二进制位决定是否累乘到结果中。用递归或者循环两种方式都可以写,循环版本更推荐,因为不存在递归栈溢出的风险。
long long qpow(long long base, long long exp, long long mod) { long long result = 1; base %= mod; while (exp > 0) { if (exp & 1) result = result * base % mod; base = base * base % mod; exp >>= 1; } return result; }最小公倍数的题,解法是先求最大公约数,再用“两个数乘积除以最大公约数”得到最小公倍数。多个数的最小公倍数,需要逐个处理,一开始令ans等于第一个数,每加入一个新数,就把ans更新为ans和新数的最小公倍数。这类题很基础,但工程里经常用到,笔试喜欢拿来考你对“辗转相除法”这个最基础算法的掌握程度。
3.6 STL容器选型:什么时候用map,什么时候用unordered_map
贝壳这套笔试卷里,STL容器选型的选择题几乎必出。map底层是红黑树,插入、删除、查找都是O(logn),且元素按key有序;unordered_map底层是哈希表,平均O(1)查找,但元素无序,极端冲突时会退化。笔试里如果题目要求输出结果有序,就需要用map;如果只要求快速查找,优先用unordered_map。
vector、list、deque的底层区别也常考。vector是一段连续内存,随机访问O(1),中间插入O(n);list是双向链表,中间插入O(1),但随机访问O(n);deque是分段连续结构,头尾操作都是O(1),随机访问比list快但又不如vector。工程里写代码,选对容器往往比优化算法更重要,笔试也爱在这个点上做文章。
4. 操作系统、网络与数据库:笔试里的“隐形门槛”
4.1 进程、线程、协程与多线程编程
C++工程师笔试卷除了语言和算法,还会穿插操作系统和网络基础。贝壳这类业务系统对并发要求高,所以进程和线程的区别、线程同步方式、多线程编程的常见坑,都是常考点。
进程是最小的资源分配单位,线程是最小的调度单位。同一个进程内的线程共享地址空间、文件描述符等资源,所以线程间通信比进程间通信便宜得多。C++11以后可以用std::thread创建线程,配合std::mutex、std::condition_variable等做同步。笔试里比较爱考的是死锁的四个必要条件以及如何避免:破坏互斥、持有并等待、不可剥夺、循环等待中的任意一个。
热词里出现了“ABA问题c++”,这是CAS操作里的经典问题。CAS全称是compare-and-swap,先比较目标地址的当前值和期望值,如果相等就更新为新值。ABA问题是指,在比较和交换的过程中,目标值从A变成B又变回A,导致CAS认为没有变化,但实际已经被其他线程修改过了。解决思路常见的有两种:使用带有版本号的原子类型,或者不用CAS用锁。
4.2 内存管理:堆栈、内存泄漏与排查方法
C++程序员对内存管理必须有敬畏感。笔试里会考栈和堆的区别、内存分配方式、常见的段错误原因。栈由编译器自动管理,空间有限,一般几MB级别;堆由程序员手动管理,空间大但有泄漏风险。栈上分配的对象生命周期结束自动析构,堆上的对象必须手动delete或者交给智能指针。
内存泄漏是一个老生常谈但特别重要的考点。热词里“visual c++ redistributable”这类词其实更多是运行库部署问题,不是笔试重点,但“new和delete必须匹配”“malloc和free必须匹配”“delete和delete[]不能混用”这些一定要烂熟于心。数组通过new[]分配,释放时必须用delete[],否则会导致未定义行为。
笔试简答题如果让你列举排查内存泄漏的手段,可以从三个层面说:第一,使用工具检测,比如Valgrind、ASan;第二,代码审查时重点检查所有new/malloc是否有对应的delete/free;第三,使用智能指针从根源上避免手动释放资源。结合起来回答会比较完整。
4.3 网络模型:从阻塞IO到epoll
贝壳这类高并发服务器,后端离不开网络编程,所以笔试对网络模型的考察也比较看重。从阻塞IO、非阻塞IO、IO多路复用到异步IO,每个阶段的优缺点、适用场景,都要能说出个一二三来。其中IO多路复用的select、poll、epoll是重点中的重点。
select有1024个文件描述符限制,poll虽然去掉了这个限制,但两者都是每次调用时把全部fd集合从用户态拷贝到内核态,然后在内核态线性扫描,效率不高。epoll是Linux下的高效方案,它把fd注册到内核事件表里,通过回调机制通知就绪事件,避免大量无用的遍历和拷贝。笔试里问“为什么高并发服务器选择epoll”时,要能从这三个方面回答。
我记得这套卷子场景设计部分考了类似“一个高并发系统,如何设计服务器IO模型”的问题,本质就是考察Reactor/Proactor模式。Reactor模式是事件驱动,主线程只负责监听和派发事件,工作线程处理业务逻辑,代码结构清晰,非常适合大量短连接或中长连接场景。
4.4 数据库索引与事务
字符串、算法、操作系统之外,数据库基础也是C++后端岗笔试的常驻嘉宾。贝壳的房源、订单、用户信息都存在数据库里,后端工程师不能只看内存里的东西。索引类型、B+树特点、事务隔离级别、乐观锁和悲观锁的区别,都是高频考点。
B+树作为InnoDB的索引结构,为什么不用B树、不用红黑树或哈希表,这个问题值得深挖。B+树所有数据都存储在叶子节点,叶子节点之间通过链表连接,天然适合范围查询;树高比红黑树低,磁盘IO次数更少;哈希表虽然单点查询O(1),但无法支持范围查询。笔试里如果能讲清楚“磁盘预读”和“空间局部性”,回答的深度会明显不一样。
事务的ACID特性和四种隔离级别也要掌握。读取未提交有脏读问题,读取已提交解决脏读但存在不可重复读,可重复读解决不可重复读,串行化解决幻读但并发性能最差。MySQL默认隔离级别是可重复读,这一点在很多面试中被提到。
5. 场景题与设计模式:把代码能力变成系统能力
5.1 房源搜索场景的系统设计
贝壳找房的核心业务场景之一是房源搜索。一次搜索请求会经过关键词解析、条件过滤、地理位置计算、排序打分、分页返回等环节,落到系统设计题上,就变成了“请设计一个房源搜索系统,要求支持高并发、海量数据、地理位置过滤和筛选排序”。
这种开放题看起来没有标准答案,但阅卷人心里有明确的得分点。最先说的是存储方案:房源基础信息放MySQL或分库分表,搜索索引放Elasticsearch或自研搜索引擎,热门的房源信息用Redis做缓存。然后要说查询链路:请求先走缓存,缓存不命中再到搜索服务,搜索服务调用排名服务获得排序结果,最后拼装详情信息返回。可扩展性要说:把搜索、排序、详情拆成不同服务,消息队列做异步解耦,索引数据有变更时通过监听binlog或事件消息同步。
回答这类题,一定要先搭建主框架,再逐步细化。不要一上来就只讲Redis或MySQL,会显得视野很窄。要让阅卷人觉得你做过系统设计,而不是背了一堆名词。
5.2 高并发“秒杀”场景
贝壳也有临时抢购热门房源、预约带看、领取优惠券这类高流量场景。笔试题里如果出现“设计一个秒杀系统”,考点集中在如何防止超卖、如何降低数据库压力、如何保证最终一致性。
秒杀系统的核心是尽可能把请求拦截在上游。前端或入口层限流,同一个用户只允许一次有效请求;服务端用分布式锁或Redis预扣库存,只有真正下单时再校验数据库库存;下单用消息队列异步处理,让用户先看到“排队中”而不是卡在事务里。
这里有个重要的细节:库存扣减不能先查数据库再更新,而要用原子操作。SQL层面写“UPDATE stock SET count = count - 1 WHERE goods_id = ? AND count > 0”,这样在数据库层面就能防止超卖。笔试里如果你能回答出这个细节,会显得非常有实战经验。
5.3 设计模式与回调函数
设计模式在C++笔试里多以选择题或代码阅读题形式出现。热词里“c++ 设计模式”“c++回调函数例子”被反复搜索,说明这确实是许多人的薄弱点。
工厂模式、单例模式、观察者模式、策略模式,是笔试中出现频率最高的几个。单例模式要注意懒汉式和饿汉式的区别,以及多线程环境下的线程安全问题;C++11以后可以用magic static实现线程安全的懒汉式单例,代码非常简洁。观察者模式很适合用在房源信息变更通知、业务事件订阅这类场景上。
回调函数是指把一个函数指针或函数对象作为参数传给另一个函数,由后者在特定时机调用。C++里实现回调的方式很多:函数指针、std::function、lambda表达式、函数对象。笔试里如果出一道“请写一个函数,在事件触发时调用回调”的题目,用std::function加lambda是最现代也最不会出错的写法。下面是一个简单例子:
#include <iostream> #include <functional> void process(std::function<void(int)> callback) { for (int i = 1; i <= 3; ++i) { callback(i); } } int main() { process([](int x) { std::cout << "received: " << x << std::endl; }); return 0; }这种写法笔试里能直接跑通,面试官想追问的时候,你还能解释一下std::function的底层内存分配机制,属于高分回答。
6. 备考策略与实战心得
6.1 拿到试卷先做什么
笔试经验这件事,真的是做得多了才会有感觉。拿到贝壳这套卷子,我第一步不是逐题往下做,而是花了两分钟把整张卷子扫了一遍,对题型分布和分值占比有个底。这个习惯非常重要,特别是遇到编程题比较多、题目描述又长的时候,能有效避免前面耗太多时间导致后面来不及做。
我的策略是:选择题如果一题卡住超过两分钟,先标记跳过,做到后面再回头。编程题先做自己最有把握的,把稳定拿分的题目做完,再去碰复杂场景题。因为笔试平台通常根据通过用例比例给分,暴力解法就算拿不到全部分数,也比提交空代码强得多。所以哪怕时间紧张,也尽量把暴力版本的代码写上去。
6.2 最容易丢分的三个地方
第一是审题不仔细。比如题目要求输出结果精确到小数点后两位,你没有处理;输入有多组测试用例,你只处理了一组;字符串包含空格,你用cin >>而不是getline。这些都不是能力问题,纯粹是细心问题,但笔试踩一次可能要浪费很多调试时间。
第二是溢出问题。用int算乘法中间结果,比如快排里数组索引相加、求最大公约数时两数相乘,都会溢出。凡是涉及两个int相乘再取模的题目,直接考虑用long long或者先取模再相乘,避免溢出导致答案错误。
第三是没有处理边界条件。链表题忘了处理空链表,数组题忘了处理长度为1的情况,动态规划题忘了初始化dp[0],快排partition时忘了一开始的边界判断。这些边界条件在本地自测的时候不明显,一旦上评测机直接0分。我每次写完编程题,都会额外检查一遍“数组为空、只有1个元素、所有元素相同”这三种极端情况。
6.3 我给后来人的建议
从语言基础到系统设计,贝壳这套笔试卷其实在反复传达一个信息:C++不是一门只用来看语法的语言,它是用来构建真实系统的工具。所以准备笔试的时候,我特别推荐你在刷完基础题之后,去认真了解一下你所在学校或公司项目里的C++代码是怎么组织线程、管理内存、处理网络请求的。哪怕只是读懂一个开源小项目,对你理解语言特性在实际工程里的落点都会有很大帮助。
还有一点是关于“vscode配置c/c++环境”这类问题。很多初学者在准备笔试时,会花大量时间折腾编辑器配置、编译器调试,这些当然有作用,但如果是为了应对笔试,更高效的分配是:环境配置保证能手写代码并编译运行就够了,剩下的时间全部花在刷题和复盘考点上。真正的笔试场上,你只有一个网页编辑器,没有任何代码提示和自动补全,平时习惯IDE自动补全的人会非常吃亏。
最后分享一个我个人的小习惯:每次笔试结束后,不管结果如何,我都会第一时间把卷子里有价值的选择题和编程题复制到自己的笔记里,标注考点和当时的错误答案,每周翻一遍。刷过的题量并不代表掌握程度,复盘时能清楚说出每道题的考点和易错点,才是真正吃透了。这套贝壳笔试卷做完后,我最大的感触就是这句话:笔试考的不只是知识储备,更是你平时写代码时有没有用心想过“为什么”。