尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

360研发工程师笔试题复盘:C++、内存与算法的硬核基本功

360研发工程师笔试题复盘:C++、内存与算法的硬核基本功 先从结论说起2016年的360研发工程师笔试题二这套卷子放到现在依然是很好的基本功自测题。虽然技术栈更新换代很快但对C/C、内存管理、算法数据结构、操作系统和网络这些底层知识点的考察思路和如今各大厂校招笔试没有本质区别。我前几天整理旧资料时翻出当时做的笔记重新做了一遍发现当年觉得模棱两可的地方现在回头看完全是另一个深度。这篇东西就是基于那套卷子涉及的考察范围结合我自己做题、面试别人、带新人的经验做的一次完整复盘希望能帮你把散落的知识点串成体系。1. 2016年这套卷子的出题风格与考察逻辑1.1 为什么一套7年前的笔试题到今天仍有参考价值很多人一听是2016年的题第一反应是“过时了”。我反而觉得这套题比很多当下的套题更值得做。360的主业是安全安全行业对工程师的要求从来不是“会用最新框架”而是“把底层机制彻底吃透”。所以研发工程师笔试题的侧重点和电商、社交类公司明显不同它不迷恋新东西而是集中火力考那些十年不变的知识指针怎么用、内存怎么布局、进程和线程什么关系、TCP状态怎么迁移、排序算法复杂度是多少。这些知识在2016年是基本功在现在依然是基本功。换句话说这套卷子是一面镜子照出来的是你计算机基础扎不扎实。框架可以现学但基础不牢写出来的代码迟早要还债。1.2 题型分布与整体难度印象那套卷子整体题量不算小大致分三个板块客观题、简答题、编程题。客观题覆盖C/C语法细节、数据结构选型、操作系统原理、网络协议简答题会要求你写出关键步骤或者对比两种方案的优劣编程题通常是一到两道算法题需要手写完整代码不是只写伪代码。从难度曲线上看它属于“开头平缓、中段爬坡、最后压轴”的节奏。前面的客观题只要基础扎实基本可以稳定拿分中间开始出现一些迷惑性选项专门坑那种“好像知道但说不准”的考生最后的编程题也不考偏题怪题而是考你能否在限定时间内写出健壮的解法。我当年做题的感受是客观题里有大概四成是“见过但没完全见过”主要原因是自己复习的时候只记结论没推导过程。比如栈和堆的区别能背但问“在栈上申请一个结构体数组和堆上申请有什么性能差异”就有点卡壳。所以这套题拿来查漏补缺效率非常高。2. 算法与数据结构题从暴力解到最优解的思维路径2.1 链表类题目的双指针思路算法题里链表是出现频率极高的考点因为它能把指针操作和边界条件一起考了。当年笔试题里有一类很典型的题找出单链表的倒数第k个节点。有些人的第一反应是先遍历一遍拿到链表长度再遍历第二遍找到目标节点。这个解法没问题但如果要求只能遍历一次呢这就需要双指针。快指针先走k步慢指针再从头部出发之后两个指针一起走快指针到尾时慢指针恰好指向倒数第k个节点。写这个代码时有几个细节容易被忽略。首先是k的合法性判断k大于链表长度、k为负数或者等于0都要考虑。其次是链表本身为空的情况。我见过不少人在LeetCode上能AC但手写时就漏判边界原因就是平时太依赖IDE的自动补全和测试用例缺少对输入做防御的习惯。typedef struct ListNode { int val; struct ListNode *next; } ListNode; ListNode* findKthFromEnd(ListNode* head, int k) { if (head NULL || k 0) { return NULL; } ListNode* fast head; ListNode* slow head; for (int i 0; i k; i) { if (fast NULL) { return NULL; // k大于链表长度 } fast fast-next; } while (fast ! NULL) { fast fast-next; slow slow-next; } return slow; }这种题目真正想考察的不是你会不会背双指针模板而是你能不能想清楚“快指针先走k步”这个动作的含义以及在实现过程中如何处理各种异常输入。笔试的判分系统通常包含隐藏测试用例多半就是这些边界情况。2.2 字符串处理题的边界条件陷阱字符串处理也是笔试题常客尤其是一道类似“实现atoi”的题。请把字符串转成整数看起来简单实际上坑非常多。我当时做题时列了一个清单字符串为空、包含前导空格、正负号、数字中间混入其他字符、整数溢出。任何一个点没处理好都会丢分。从考察意图来看这道题不是看你会不会用库函数而是看你是否具备“把所有异常情况都想到”的工程思维。你在真实项目里写的每一个接口面对的都是不可信的输入不校验就会出线上事故。一种标准的处理流程是这样的先跳过前导空格再判断正负号然后逐字符转换数字每转换一位就检查是否溢出溢出则返回一个约定好的极值。int myAtoi(const char* str) { if (str NULL) return 0; while (*str ) str; int sign 1; if (*str || *str -) { if (*str -) sign -1; str; } long long num 0; while (*str 0 *str 9) { num num * 10 (*str - 0); if (num INT_MAX) { return (sign 1) ? INT_MAX : INT_MIN; } str; } return (int)(sign * num); }为什么用long long来存中间结果就是为了在溢出检查前留出足够余量。笔试时如果你的解法只能处理“正常情况”那分数会很不理想因为测试用例设计者会专门丢各种脏数据进来。2.3 数组和排序追求稳定的拿分项算法题里除了链表和字符串数组排序也是常见考点。不过笔试题里很少直接让你写一个快排更常见的是考察排序算法的变体或者多个有序数组的处理。比如两个已排序数组合并成一个有序数组要求时间复杂度O(n)。这本质上是归并排序的merge部分。还有人会问“在海量数据中找前K大的数”考的是堆的思想。如果K远小于N用大小为K的最小堆堆顶就是当前第K大。我当时做过一个归纳凡是“前K个”“第K大”这种题优先想到堆凡是两个有序数组优先想到归并思路凡是涉及乱序数组的原地操作优先想双指针交换。这几个套路能覆盖大部分笔试算法题。值得提醒的是笔试编程题追求的不一定是最优解而是“在规定时间内能写出来的最稳妥解”。有时候堆的解法虽然最优但实现复杂度高容易在细节上出Bug反过来用全排序时间复杂度虽然高一点但代码简单可靠。如果测试数据规模不大全排序反而能保证拿满基础分。做题要分清楚场景不要一上来就秀骚操作。3. C/C底层题指针、内存与字节序的硬核考察3.1 指针数组与数组指针的纠缠关系这套卷子的C/C部分占比不低毕竟安全公司大量代码都是C/C写的。最常见的一类题就是考察指针和数组的关系题目往往长这样int *p[10]和int (*p)[10]有什么区别。很多人靠死记硬背答题但换成三层指针加数组就懵了。我一般建议从优先级入手[]的优先级高于*所以int *p[10]先解析为p[10]说明p是有10个元素的数组元素类型是int*所以这是指针数组int (*p)[10]用括号把指针操作先括起来说明p是指针指向一个包含10个int的数组所以这是数组指针。笔试还会进一步延伸sizeof的结果是什么在64位系统下指针数组int *p[10]占用80字节而数组指针int (*p)[10]本身只是一个指针占用8字节。这个考点直接检验你在内存布局上是否有直观感受。我当时复习时专门写了一个小工具来验证自己的理解把各种声明逐一打印sizeof和地址偏移发现效果比单纯看书好很多。建议你也试试自己动手得出的结论比背下来的结论牢固十倍。3.2 内存对齐和字节序的规则内存对齐几乎是C/C笔试必考题隐藏考点在于你不仅要会算大小还要明白为什么要对齐。CPU从内存读取数据是按字为单位的如果数据没有对齐可能需要多次访问才能读全性能损失很大。所以编译器会在结构体成员之间插入padding以空间换时间。题目一般会给你一个结构体让你算sizeof。规则就三条第一个成员偏移为0每个成员的对齐数取编译器默认对齐数与该成员大小的较小值结构体总大小必须是最大对齐数的整数倍。我印象里特别喜欢考这样的结构体struct Test { char a; // 1字节 int b; // 4字节 char c; // 1字节 };在默认4字节对齐的编译选项下sizeof(struct Test)不是6而是12。原因a占偏移0随后补3字节padding让b落在偏移4b占4字节到偏移7c占偏移8最后整个结构体大小补充到最大对齐数4的倍数就是12。字节序的题也常出现。给一段代码用char*指针去读取一个int变量问输出是什么。这考的是大小端。x86架构是小端低字节存储在低地址。如果你用char指针按地址递增读取会先读到低位字节。int x 0x01020304; char* p (char*)x; printf(%d %d %d %d\n, p[0], p[1], p[2], p[3]); // 小端输出4 3 2 1 // 大端输出1 2 3 4学字节序不只是为了做题网络编程中收发数据时要考虑字节序转换这也是为什么会有htonl、ntohl这类函数。3.3 栈溢出与安全视角的底层关联360的笔试题不会绕过安全方向。有一类题会考“栈溢出”的产生原理或者给一段有隐患的代码让你指出问题。核心就是没检查写入长度就拷贝到栈上固定大小的缓冲区比如strcpy到一个char buf[64]里如果源字符串超过63个字节还要留一个给\0就会越界写坏栈上邻近的数据包括返回地址攻击者通过精心构造输入可以劫持程序控制流。这道题的考察价值在于它不只是一个安全领域专业概念而是对C语言“不设防”特性的深刻理解。作为研发工程师即使不专门做安全也应该具备基本的安全编码意识比如能用strncpy或snprintf就不要用strcpy和sprintf。话说回来笔试过程中遇到这种题不需要你写出完整的漏洞利用代码但你要能说清楚攻击者可能利用它做什么以及如何防御。提到“栈保护”“NX”这些缓解机制会更显水平。4. 操作系统与网络原理题系统知识如何与研发工作挂钩4.1 进程与线程一道高频但不易答全的题操作系统板块里进程和线程的区别几乎每年都考。但你会发现想拿全分并不容易。常见的回答是“进程是资源分配的基本单位线程是CPU调度的基本单位同一个进程内的线程共享地址空间进程之间地址空间隔离”。这句话没错但还不够。笔试里进阶的问法是多线程程序里一个线程崩溃会不会导致整个进程退出这里牵涉到信号处理机制很多情况下答案是会。线程和进程在Linux下都是用task_struct来描述的线程在崩溃时产生的信号会作用于整个进程的地址空间。还有一道常见的对比题为什么线程切换比进程切换开销小因为同一进程内的线程切换不需要切换地址空间页表不换TLB也不用全部失效而进程切换必须完整切换内存地址空间开销自然更大。这背后是硬件的TLB机制在做支撑。我当时复习的建议是不要只背“进程线程区别”的要点还要能画出虚拟地址空间的布局图明确栈、堆、BSS段、数据段、代码段分别放什么。这张图能串联起很多零散考点。4.2 网络协议基础TCP三次握手之外的延伸问题网络部分的考题三次握手是常客但如今只答三次握手显然不够。当年卷子里有一道题问为什么TCP建立连接需要三次而不是两次以及为什么断开连接需要四次这两个问题的本质都是对“可靠传输”的理解。建立连接用三次是为了防止已经失效的连接请求报文突然又传到服务器从而避免服务器白白建立空连接。如果只有两次握手客户端发出的SYN因为网络延迟重传一次后第一次的那个SYN又到达服务器服务器就会误以为新连接来了分配资源后客户端却不会理它。断开连接用四次是因为TCP是全双工的两个方向的数据通道需要分别关闭。A发FIN表示A的数据发完了B可能仍然有数据要发给A所以B先回ACK确认等自己的数据也发完后再发FIN。这个“等待对方数据发完”的时间差就是四次挥手的根本原因。笔试还会顺带问TIME_WAIT的问题比如主动关闭方为什么需要停留在TIME_WAIT状态通常是2MSL。原因有两个一是确保最后的ACK对方能收到如果丢失还能重传二是让旧连接中的延迟报文不会干扰新连接。这两个原因都要写出来才稳。这类知识看起来和日常CRUD开发关系不大但一旦涉及高并发服务、连接调优不能理解TIME_WAIT和TCP状态机排查问题时就会无从下手。当时的笔试就在提醒你研发工程师不能只做应用层的事。4.3 文件描述符、缓存与IO模型的基础概念操作系统板块还有一些容易忽视的细节考点什么是文件描述符为什么说“一切皆文件”标准输入、标准输出、标准错误对应的fd分别是多少阻塞IO和非阻塞IO的区别select、poll、epoll各自适用什么场景。其中epoll在服务端开发中地位很高。笔试问的通常不是去背三个接口而是问select的fd集合是有限的而epoll能支撑海量连接底层靠的是回调机制而不是轮询。这个差别在面试中也是加分项。我当时做这套题的时候对epoll的理解还很浅只知道“比select高效”。后来在实践中才真正理解select每次调用都要把fd集合从用户态拷贝到内核态然后内核线性扫描所有fd效率随连接数线性下降。而epoll通过红黑树管理关注的文件描述符通过就绪队列返回真正有事件发生的连接配合mmap减少拷贝这才扛得住百万连接。所以复习网络和操作系统时尽量把每个知识点都追问到底这个机制解决了什么问题不用它会怎样底层怎么实现的把这三个问题都答清楚笔试基本就不会丢分。5. 从这套卷子反推研发工程师笔试的复习方法论5.1 知识点分层哪些必须全对哪些可以适度放弃做完这套题我最大的启发是备考不能平均用力。笔试复习的时间是有限的一定要区分主次。根据这套卷子的考察结构我习惯把知识点分成三层。第一层是“基础保分项”包括C/C语法细节、指针与内存、基本数据结构和排序算法、进程线程概念。这部分几乎必考而且一旦丢分很可惜必须做到条件反射般熟练。第二层是“能力区分项”包括内存对齐计算、TCP状态机、复杂指针声明、编程题的最优解设计。这部分是拉开差距的地方值得花大量时间刷题和推演。第三层是“锦上添花项”包括网络协议栈的深度细节、Linux内核机制、安全攻防原理。这些内容不一定卷子直接考但写进答案或者面试时提一嘴会显得你知识面广且理解深入。我当时给自己定了一个目标第一层失分率必须控制在5%以内第二层尽量做到不失分第三层能答多少答多少。按这个策略复习效率比漫无目的刷题高得多。5.2 刷题之外的动手验证本地实验环境搭建我特别想强调的一点是不要只看书、只刷题笔试里很多“选择题”的迷惑选项光靠记忆是区分不开的必须亲自动手验证。比如内存对齐你可以写一个包含不同类型成员的结构体逐个打印成员地址比如大小端你可以用上面的char指针代码直接在本地跑比如进程线程的区别你可以写一个多线程程序打印线程ID和进程ID观察同一进程内的线程pid是否相同。这些实验需要的东西很简单只要有GCC编译器和Linux环境就能做。我当时还专门搭了一个“笔试实验仓”里面放了几十个这样的小程序每个程序对应一个考点。考前不翻书直接跑实验回忆知识点效率极高。实验仓里包含指针类型与自增步进的验证、结构体内存布局打印、fork与线程的行为对比、TCP连接状态查看、字节序输出等。如果你用的是macOS或者Windows也可以用docker起一个Linux容器体验完全一致。关键是把“我看到过这个结论”变成“我跑出来就是这个结果”这种记忆非常深刻。5.3 笔试中的答题顺序与时间分配建议除了知识储备答题策略也能实际影响分数。我根据自己的经验总结了一套答题顺序先做有把握的客观题再做简答题最后留至少30分钟给编程题。客观题不会的不要死磕先标记跳过。一道题卡太久会严重影响心态而且后面可能有送分题因为卡题而没时间做才是真正的亏损。简答题注意分点作答写答案时把关键术语放在前面阅卷人扫一眼就能看到你得分的点。编程题一定先审清楚题目再动手。我见过太多人读题只读了一半就开始写写到一半发现理解错了时间全浪费。我建议先在草稿纸上列出输入范围和边界情况再动笔写代码。写完后一定要花两分钟从头到尾检查一遍特别是数组下标越界和空指针。编程题的时间分配还有一个技巧如果最优解一时想不出来先写出一个正确但复杂度稍高的版本保证基础用例能过再尝试优化。笔试的判分通常按用例给分部分用例能拿分也是好的。5.4 刷这套题最容易踩的坑最后说几个我自己刷这套题时踩过的坑希望你能避开。第一个坑是只看答案不写代码。很多时候以为自己会了上机一写就卡壳。尤其是编程题必须亲手敲一遍编译通过、测试通过才算会。看一眼答案觉得懂了和真正写出来还隔着十万八千里。第二个坑是忽略编译环境差异。笔试环境可能用32位也可能用64位sizeof(long)在这两种环境下不一样。做和内存相关的题时要搞清楚题目是按什么位数的环境来考。如果没说明通常默认64位。这一点不知道坑了多少人。第三个坑是只刷算法题不复习基础知识。我见过有人拼命刷了三百道LeetCode结果C/C基础题反而答得一塌糊涂。笔试看的不是单项极限而是综合水平。算法、基础、系统知识三条腿都要稳缺一条都会影响总分。第四个坑是复习时不整理错题。笔试复习最有价值的部分就是错题复盘。我当时每做错一道题都会记录错因和相关知识点考前集中过一遍。这样做看起来花时间但正是这些错题是你真正不会的地方把它们弄懂了提升最明显。这套2006年的360笔试题二说实话难度并不比现在许多公司的校招笔试题低。它考察的内容很“硬”几乎没有水分每一道题背后都对应着实际工作中会用到的基础能力。如果你能把这套卷子的知识点吃透再去准备其他公司的笔试会发现很多题目都是相通的。我个人做完这套题之后最大的变化是看问题不再止步于表面。看到一段代码会本能地思考它的内存布局思考它是否会溢出思考它的并发模型是否合理。这种思维方式的转变比多背几个知识点更值钱。希望你也能通过这份复盘找到自己的薄弱环节把基础打扎实。
返回列表