我当年刷这题的时候,说实话,第一反应是“这考察范围也太杂了”。但实际做完再对着答案复盘,你会发现它本质上不是考某几道题,而是在给你画一张计算机基础知识的雷达图。语言功底、数据结构、算法设计、操作系统、网络原理,甚至连概率和逻辑推理都揉进去了。这篇我不想简单给答案,而是把每一类的考察意图和你应该怎么准备说清楚,顺便把当年踩过的坑也一并交代了。
1. 整体考察思路与知识结构拆解
1.1 为什么实习生笔试要考得这么广
很多人不理解,我应聘的是Java开发或者算法岗,为什么要考C++内存布局,为什么还要看TCP状态码?其实站在出题人的角度就很好理解:实习生不像社招那样有明确的项目经验可以挖掘,笔试是筛选“基本功是否扎实”效率最高的方式。
阿里这种体量的公司,内部的技术栈极其复杂,今天你可能在写Java,明天一个故障排查就涉及JVM甚至操作系统底层交互。所以笔试不看你写过什么框架,而是看你对“计算机这台机器”本身的认知程度。2017年这套题里,语言侧的题目(C++/Java语法、内存、多态)和系统侧题目(进程线程、网络状态)比例差不多,这不是偶然,它反映的是研发岗位的通用底色:不管你做业务还是做中间件,底层逻辑是通的。
1.2 考察模块的占比逻辑
从当时流传出来的版本看,大致可以分为四块:语言基础、数据结构与算法、操作系统与网络、逻辑与概率。其中语言基础占比最大,差不多四成,数据结构和算法占三成左右,剩下的是系统和逻辑。
这个占比其实很有讲究。语言基础考的是你“能不能立刻干活”,算法题考的是“遇到新问题能不能拆解”,操作系统网络考的是“线上出问题能不能排查”。三个维度正好对应三个层次:会写、能解、懂排障。很多同学只刷算法不补系统知识,结果笔试里栽在“进程和线程的区别”这种看似简单但需要深度解释的题目上,非常可惜。
1.3 对现在备战面试的参考价值
虽然这是2017年的题,但我建议现在准备大厂实习的同学也认真过一遍。不是说题还会原封不动出现,而是它的考察骨架和出题品位,后来几年基本没有变。公司可能会在题目里加入新的技术热点,比如容器、云原生、分布式一致性,但底层那些关于内存、并发、数据结构复杂度的考察点,永远都在。
我的理解是,这套题更像一份“体检清单”,你哪里薄弱,做一遍就暴露了。与其到处找面经背答案,不如拿它做一次全面的自我排查。
2. 语言基础题解析:内存、多态与语法陷阱
2.1 C++虚函数与多态的底层原理
这类题几乎是必考的。典型的问法有:“以下关于虚函数的说法正确的是”、“基类指针指向派生类对象后调用虚函数,会发生什么”、“析构函数为什么要声明为虚函数”。
我当时看到“虚函数表是每个对象一份还是每个类一份”这类选项时,都容易犹豫。这里的关键在于理解对象的内存布局。每个含有虚函数的类,编译期会生成一张虚函数表(vtable),存放该类所有虚函数的地址,每个对象内部会有一个vptr指针指向这张表。所以虚函数表是每个类一份,而vptr是每个对象一份。
为什么不把虚函数表直接放进对象里?因为那样每个对象都要复制所有函数指针,内存浪费太大。用一个指针间接引用,对象只需要多一个指针大小的空间。这是典型的空间换时间与时间换空间的权衡思维,笔试未必直接考,但解析里一定要明白。
2.2 构造函数与析构函数的执行流
这类题喜欢这样出:“定义一个派生类对象时,构造函数的调用顺序是什么”。答案大家都知道是先基类后派生类,但我觉得更值得关注的是为什么。因为派生类对象包含基类的子对象,如果基类部分没有被正确初始化,派生类自己的初始化逻辑就失去了依赖基础,比如基类里维护的内部状态就没有建立。
场景上,我记得有一道题是:“基类析构函数不是虚函数时,delete基类指针会怎样?”答案是未定义行为,因为不会调用派生类析构函数,派生类的资源就没人释放。这里有个明显的坑:如果你在项目中把基类指针当作通用句柄,析构函数一定要加virtual。笔试只是考选择题,线上的事故却是实实在在的内存泄漏。
我建议记一种内存心理模型:构造从根开始,一层层向外;析构恰好相反,从叶子开始,一层层向内。这样无论题目怎么变,你都能推出来。
2.3 static关键字的多重身份
C语言里static修饰函数和全局变量意味着“内部链接”,限定在当前编译单元可用。C++里static成员变量就是属于类而不是属于对象,所有对象共享一个副本。static成员函数没有this指针,只能访问static成员。
有一道印象深刻的题目,大概是:“static成员函数能否访问非static成员变量?”答案是绝对不能。原因是静态成员函数没有this指针,它连“当前对象是谁”都不知道,怎么访问对象的成员?要访问也可以,只能通过外部传入的对象引用。
这里的核心思想是:static把“实例维度”提升到了“类维度”。实例维度下,每个对象是独立王国;类维度下,所有对象共享同一个数据仓库。理解了这个,后续并发编程里“类级别锁”和“对象级别锁”的区别也会顺畅很多。
2.4 Java方向的对照复习
做这套题的时候,如果你是Java方向,不能用“我不看C++”来逃避,但可以有侧重地对照理解。比如C++的虚函数对应Java的普通方法重写,C++的static成员变量对应Java的static字段。Java没有指针,但引用传递和值传递的区分也是经典考点。
还有一道典型的Java题:“String、StringBuilder、StringBuffer的区别”。String不可变,每次拼接会产生新对象;StringBuffer线程安全但慢;StringBuilder快但线程不安全。单线程环境用StringBuilder,多线程共享可变字符串才用StringBuffer。选择依据就一句话:是否有跨线程共享修改的可能。没有就追求性能,有就求稳。
我的建议是准备一张“语言特性对照表”,把C++和Java里类似的概念列在一起,比如虚函数/方法重写、析构函数/finalize、static成员/static字段。这样笔试遇到任何一个语言的题,你都能在脑中快速找到“另一个语言里对应的东西”,理解速度至少快一倍。
3. 数据结构与算法题:不只是写代码
3.1 复杂度分析是算法题的隐形考点
笔试里的算法选择题,很多不是让你手写代码,而是给出几段代码,问时间复杂度。比如双重循环嵌套,内层j从i开始遍历,这个复杂度其实是O(n^2/2),但渐进意义下还是O(n^2)。
有一道题印象很深,是关于递归计算斐波那契数列的复杂度。直接用递归写法,每次调用产生两个分支,深度为n,复杂度是O(2^n)。这不是因为递归本身慢,而是它把同一个子问题反复计算了很多遍。如果用一个数组做动态规划,自底向上算,复杂度直接降到O(n)。
我当时的感受是:复杂度不是靠背的,是靠“数次数”数出来的。遇到循环就数迭代次数,遇到递归就画调用树,遇到二分就关心每次缩小的比例。面试官真正想看的是你有没有这种“成本意识”,而不是答案本身。
3.2 二叉树的遍历与重建
“已知前序和中序遍历,求后序遍历”,这是数据结构的老古董题,但2017年的卷子里依然出现了。它的本质是递归分治:前序第一个元素是根,在中序里找到根的位置,左边是左子树,右边是右子树,然后递归处理左右部分。
我记得有个常见的错误是只想着“背代码”,结果题目变成“已知后序和中序求前序”就不会了。其实思路完全一样,只是根的定位方式变了。后序最后一个元素是根,从中序里一分为二,递归即可。不要记题型,要记“如何定位根”的逻辑。
如果觉得递归绕,可以拿一棵三层的具体二叉树在草稿纸上手动模拟一遍。把每一步的“当前根是谁”“子树范围是什么”写下来,十遍之后,你就能在脑子里递归了。
3.3 排序算法的稳定性和适用场景
排序是笔试选择题的重灾区:“以下哪个排序算法是稳定的”、“堆排序和快速排序平均复杂度一样吗”、“数据基本有序时用什么排序合适”。2017年的卷子里有一道:“以下排序算法中,平均时间复杂度为O(n log n)的是:冒泡、插入、快排、堆排序”。答案是快排和堆排序。
我更想多说一句:为什么快排在实际场景里通常比堆排序表现更好,即使两者平均复杂度一样?因为快排的局部性更好,CPU缓存命中率高;堆排序的访问模式是跳跃式的,缓存不命中率高。这已经不仅仅是笔试知识点,而是真刀真枪写高性能代码时的工程考量。
关于稳定性,我的记忆技巧是:稳定的排序,本质上不会“跨过相等元素”。冒泡和插入排序只在相邻元素之间交换,所以稳定;快排的partition会把元素移动到很远的位置,可能跨过相等元素,所以不稳定。记住“相邻交换稳定”这个直觉,大部分题目就能推出来。
3.4 哈希表解决冲突的两种策略
哈希表的考题通常不是让你实现,而是问“开放定址法和链地址法的优劣”。链地址法把冲突的元素挂在同一个桶的链表上,实现简单,负载因子可以大于1,但缓存不友好。开放定址法把所有元素都存在表内,缓存友好,但负载因子不能太高,否则冲突急剧增加。
有一道题问:“线性探测法在删除元素时为什么不能直接置空?”因为如果直接置空,后续查找某个元素时,探测链可能在这里中断,明明元素存在却找不到了。要用“标记删除”的方式,而不是直接清空。这类细节,没有自己实现过一次哈希表的人很容易忽略。
我的建议是:这一块别只背结论,动手实现一个分离链接法的哈希表,再实现一个线性探测版的,感受一下两者在插入和查找时的区别。笔试里遇到“哈希表扩容为什么开销大”这类题,你脑子里自然能浮现出“所有元素重新计算哈希位置”的画面。
4. 操作系统与网络原理:排查问题的底气
4.1 进程、线程与协程的本质区别
这个考点几乎是所有大厂笔试的保留节目。进程是资源分配的基本单位,线程是CPU调度的基本单位,同一个进程内的线程共享地址空间和文件描述符,而进程之间是隔离的。协程则是用户态自行调度的轻量级执行流,切换成本比线程更低。
2017年的题里有道选项是“线程之间可以共享什么”,答案是堆和全局变量。为什么栈不能共享?因为栈是每个线程私有的运行上下文,记录着函数调用关系。如果两个线程共用一个栈,函数返回地址就会乱套。
我的理解是:线程共享的是“堆上的数据”,独享的是“执行流的状态”。堆是房子里的公共区域,栈是每个房间的私人空间。如果面试官追问“协程为什么轻量”,核心原因是协程切换不需要陷入内核态,只需要在用户态保存/恢复寄存器上下文,这比内核线程切换成本低一个数量级。
4.2 死锁产生的四个必要条件
互斥、持有并等待、不可剥夺、循环等待,这四个条件缺一不可。笔试里经常让你判断“破坏哪个条件可以预防死锁”。比如资源一次性分配,就是破坏“持有并等待”;允许抢占,就是破坏“不可剥夺”;按序分配资源,就是破坏“循环等待”。
我曾经天真地以为“避免死锁”就是“预防死锁”,其实是两码事。预防是把四个必要条件之一直接否定掉,策略是静态的;避免是在运行时动态判断是否进入不安全状态,典型算法是银行家算法。笔试里如果问“Dijkstra的银行家算法属于哪一类”,答案不是预防而是避免。
做题时的建议是:看到“死锁”两个字,先在草稿纸上把四个必要条件默写出来,然后逐项对照。大多数多选题的答案,就是“哪个选项对应破坏哪个条件”的排列组合。
4.3 TCP三次握手和四次挥手的状态变化
网络题里,TCP状态机是重中之重。三次握手:客户端发SYN,服务端回SYN+ACK,客户端再回ACK,连接建立。四次挥手:主动关闭方发FIN,对端回ACK,对端再发FIN,主动方回ACK,连接关闭。
有一道题问:“TIME_WAIT状态出现在哪一端”。答案是主动关闭连接的一端。因为主动方要确保最后一个ACK能被对端收到,如果丢了,对端会重发FIN,所以主动方需要等待2MSL(最大报文段生存时间)才能彻底关闭。这个设计是为了防止旧连接上的迟到报文干扰新连接。
我记得很多同学在这道题上栽过,因为只记了“四次挥手”,没记状态属于谁。我的技巧是画时间序列图,把每一端的状态变化写上去。什么时候SYN_SENT、什么时候ESTABLISHED、什么时候FIN_WAIT_1、什么时候TIME_WAIT,画完一遍,你对TCP的整个生命周期会有一个立体认识。
4.4 HTTP状态码与常见故障排查
笔试里考察的HTTP状态码不算深,但非常实用。200是成功,301永久重定向,302临时重定向,403禁止访问,404不存在,500服务器内部错误,502网关错误,504网关超时。
2017年的题目里有一道:“用户在浏览器访问某页面时返回503,最可能的原因是什么?”。503的意思是服务不可用,通常不是代码bug,而是服务器过载或者正在维护。这类题对有线上排查经验的人来说很简单,但对纯刷题的学生来说,容易和500混淆。
我的建议是,不要只背状态码数字,而是把每个状态码背后可能的物理原因也记一下。502是上游没收到有效响应,504是上游响应超时,看到这两个码就应该去查后端服务和网络链路。这才是笔试题目真正希望你具备的能力。
5. 错题复盘与备考策略:避免低效刷题
5.1 建立错题背后的“知识盲区”
单纯订正答案是最低效的复盘方式。拿到错题后,我会问自己三个问题:我为什么选错?正确答案的解释里,哪一步是我没想到的?这个知识点在真实项目中,会以什么形式出现?
比如我做过一道关于“Java中HashMap扩容时是否需要重新计算hash”的题。当时我选错了,后来才明白,JDK 1.8对这个问题做了优化:扩容后元素要么在原位置,要么在原位置+原容量的位置,不需要重新计算hash,只凭高位bit判断。这个知识点在什么场景下有用?如果线上HashMap容量设置不当,引发频繁扩容,涉及到的就是这类底层行为。
所以做题的意义不在于“做对”,而在于“暴露”。一道错题背后是一个知识盲区,你花20分钟把它补上,比刷五道已经会做的题更有价值。
5.2 时间分配与答题顺序
笔试的时间通常比较紧张,我建议的原则是:先做会做的,再做能推的,最后做要蒙的。不要在一道题上死磕超过3分钟。因为选择题的知识点之间往往是独立的,你卡在C++多态上,不代表后面的网络题不会。
如果一道题完全没有思路,但选项里有两个明显是干扰项、两个看起来合理,可以结合常识猜一个。这不是鼓励蒙题,而是说在可控范围内,不应让低分值的题消耗高分值题的时间。整套卷子做完后,如果还有时间,再回头琢磨卡住的题也不迟。
5.3 针对性复习路线
如果你的目标是阿里或者同类公司的实习,建议按这个顺序复习:先语言基础(重点是多态、内存、集合类),再数据结构(重点是二叉树、哈希、排序、复杂度),然后操作系统(重点是进程线程、死锁、内存管理),最后网络(重点是TCP、HTTP、DNS)。
每复习完一个模块,不要急着进入下一个模块,先做十道对应模块的选择题巩固。题目不需要太偏,把常见考点吃透就好。我的感受是,技术笔试比面试更“标准化”,它考的不是灵感,而是你是否系统地把核心知识过了一遍。
5.4 从笔试到面试的延伸准备
笔试里出现的知识点,面试里大概率会被追问。比如你笔试做了“HashMap的扩容机制”,面试官可能会问:“如果多个线程同时触发扩容,会发生什么?”这就涉及并发安全问题了。所以每一道笔试错题,都要准备一个“如果再往深挖一层,我会怎么回答”的预案。
我会在自己整理的复习文档里,给每个知识点加一栏“延伸问题”,比如:虚函数问题后面记“构造函数里调用虚函数会发生什么(答案是只会调用当前类的版本,因为动态类型还没完成)”。这样笔试和面试就能打通复习,而不是割裂地准备两种考试。
6. 最后的几点实操心得
马上要参加这类笔试的同学,我在实际刷题中有几个很具体的建议。
第一,草稿纸一定要用起来。别以为选择题不需要演算,复杂度的推导、二叉树的重建、TCP状态的流转,在纸上画一遍和脑子里空想,效果完全不同。我当年准备的时候,桌上永远放一沓A4纸,题目还没读完手已经开始画了。
第二,不要只刷“面经”里的题目。面经只能帮你了解出题风格,但刷题的重点应该放在系统复习知识点上。因为题目会变,考点不变。今天考虚函数表,明年可能考lambda表达式的捕获列表,但它们都归在“语言特性底层机制”这个大类下。
第三,适当的“恐惧”是有用的。我第一次做这类笔试模拟题时,正确率大概只有六成,心里确实慌。但换个角度想,好在这是练习题,不是真正的面试,我还有时间把漏洞补上。备考阶段暴露问题,是成本最低的时候。
第四,如果你时间有限,优先保语言和数据结构。这两块占比最大,而且是确定性最强的题目,不像系统网络偶尔会出现偏题。把语言基础和数据结构练到九成正确率,你已经超过了大多数竞争者。
做这套题最大的价值,不是让你记住某道题的答案,而是让你发现自己对计算机这座大厦的地基理解得够不够深。地基不牢,后面聊什么分布式、高并发都是空中楼阁。认真刷一遍,把每个模糊的知识点都搞清楚,你会发现面试时底气都不一样了。