ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

趋势科技校招C/C++笔试题全解析:从sizeof到链表反转的陷阱与实战

趋势科技校招C/C++笔试题全解析:从sizeof到链表反转的陷阱与实战 2017年秋季我坐在趋势科技校招笔试现场拿到B卷第一遍翻完其实心里有点底没有偏题怪题全部是C/C基本功、数据结构基础和少量系统编程知识但真正动笔做到最后才发现这套卷子几乎每一道题都埋着雷。尤其是sizeof与指针运算那几道选择题看起来是送分题实际拉分极狠。现在回头看这套题非常适合两类人参考一类是准备安全软件、系统软件、嵌入式方向开发岗的同学另一类是正在刷C/C笔试题但总在细节上翻车的人。虽然年份是2017年但C的底层素养、内存布局、边界条件处理这些东西到今天依然是校招开发岗的高频考点。这篇文章我把能回忆起来的题目整理成完整解析同时从为什么这么考的角度拆一遍希望能帮你避开我当年踩过的坑。1. 这套B卷长什么样题型结构与时间分配趋势科技这份开发岗笔试题整体风格偏古典。没有大数据、机器学习之类花哨内容就是朴素地考察C/C语言功底、数据结构算法以及基础的TCP/IP和多线程知识。试卷大结构分三块不定项选择题、简答与代码填空、编程题。题量大概是这样按我当时拿到的B卷回忆个别题目的排列顺序可能有出入但考点范围是这样题型题量分值占比考察重点不定项选择题10道约40%指针与数组、sizeof、C对象模型、关键字语义、字节序简答/代码填空3道约25%多线程数据同步、socket生命周期、代码输出预测编程题2道约35%链表操作、字符串处理atoi/子串90分钟的考试时间我的建议分配是选择题30分钟简答20分钟编程题40分钟。这里要特别说一句顺序很重要。我当时拿到卷子先花10分钟把编程题看了一遍确认两道题都能想到思路心里有底之后才从第一题开始做。这样做的好处是避免先扣选择题结果在某个纠结的选项上耗太久导致后面编程题时间不够。选择题的不定项设置也是一个陷阱。它不确定是单选还是多选所以如果你遇到一个拿不准的选项宁可少选也不要乱选。我印象中考场里有人把单选当多选做白白失分。整套卷子还有一个隐性特点它用大量场景化的小题考察防御性编程意识。这在后面编程题上体现得尤其明显不单单是算法你提交的代码有没有做空指针检查、有没有处理输入溢出这些都是拿分点。2. 选择题中的高频陷阱指针、内存与语言细节这套选择题密集地考了一个主题你对C/C内存布局到底懂多少。它不是考你能背出多少概念而是给你一段代码让你预测输出。我挑几个印象最深的题目逐个拆给你看。2.1 sizeof与数组传参看起来送分实际最拉分先看这道经典变体char str[] hello; char *p str; printf(%lu %lu\n, sizeof(str), sizeof(p));在很多人的预期里输出是5 5或者6 6但正确结果是6 8。sizeof(str)算的是数组总字节数str是char[6]包含结尾的\0所以是6。sizeof(p)算的是指针变量本身的大小在64位系统上指针统一是8字节。这里有个关键认知sizeof是编译期运算符它永远不会去追问一个指针后面到底指向多大的内存。编译器看到数组名str知道它有6个元素的完整类型信息看到指针p只知道它是个指向char的指针。这个题型几乎年年出现但每次都会有人填错。另一个高频变体是数组作为函数参数void foo(char s[10]) { printf(%lu\n, sizeof(s)); }你一定知道函数形参会退化成指针但拿到考场上还是容易写成10。因为代码里s[10]太有迷惑性了。编译器眼里这个函数签名就是void foo(char *s)sizeof(s)在64位下永远是8。我当时做题时的判断方法很简单看到sizeof先问自己括号里这个东西是数组还是指针。是数组算整个数组的大小是指针就是机器字长32位下464位下8与它指向什么类型无关。别看这方法简单它能帮你逃过一半的sizeof陷阱。2.2 a1到底跳了多少指针运算的步长逻辑这道题我记得很清楚因为考场上一大半人犹豫了很久int a[5] {1, 2, 3, 4, 5}; int *p (int *)(a 1); printf(%d\n, *(p - 1));输出是5。关键在于a的类型不是int*而是int()[5]也就是指向整个数组的指针。对这个指针加1地址会往后跳动一个完整数组的长度也就是5个int。然后把这个地址强转成int再做p-1就回退一个int的大小正好指向数组最后一个元素a[4]。很多地方会把这道题归类为冷知识但我觉得它考的是非常本质的东西指针算术是类型敏感的。加1等于加上sizeof(指针所指类型)的字节数。int加1跳4个字节int()[5]加1跳20个字节。顺带一提如果题目把a改成a那a1指向的就是a[1]了。a和a虽然在数值上相同但类型权限完全不同。建议你做题时先在草稿纸上写出每个表达式的类型再决定它加1后跳多远。我在考场上就是靠先写类型再做运算防住了这类错。2.3 volatile与const共存语言细节里的分水岭有一道简答题给了这样的代码片段extern const volatile int flag;问这个声明是否合法以及它是什么意思。答案是合法。const和volatile并不冲突它们分别从不同维度约束变量。const表示程序自身不应该去修改这个变量volatile表示这个变量的值可能被外部因素硬件寄存器、信号处理函数、多线程环境在程序不可见的情况下改变。真正让你在纸上写答案的时候要讲清楚应用场景。比如一个只读的硬件状态寄存器你用const修饰防止代码里误赋值但同时它反映的是外部硬件状态运行中会变所以必须加volatile防止编译器把它优化到寄存器里缓存。这个点很符合安全软件公司的风格写代码的人如果不理解编译器优化和硬件交互写出的驱动或扫描引擎迟早出bug。别以为这种偏底层的题只有嵌入式公司会考趋势科技这类做系统级安全工具的公司同样看重。2.4 字节序小端机器上的输出选择题里有一道判断题是给int赋值后按字节输出int x 0x12345678; char *c (char *)x; printf(%x\n, *c);如果你在x86小端机器上跑输出的会是0x78。小端机的规则是低字节存放在低地址。0x12345678的最低字节是0x78而指针c指向的就是低地址所以解引用拿到0x78。这道题很多人会把它记成倒过来输出其实没那么复杂你只需要记住两个事实第一大端像人写字高字节放前面小端像人读数字低字节放前面。第二从低地址开始读读到的就是低位字节。这就不会错了。字节序对安全软件有一个实打实的关联解析网络包和文件格式时如果没处理好字节序轻则解析出错重则出现越界访问。网络安全公司很喜欢考这类题不是故意刁难是真实业务里天天会遇到。3. 简答题里的系统知识线程、socket还有动态库选择题之后是简答和代码填空。这部分题量不大但每道题都需要写清楚为什么不是填个答案就完了。3.1 多线程共享什么、独占什么别用教科书回答有一道题是两个线程共享同一个进程地址空间请说明哪些资源是线程共享的哪些是线程私有的。共享的资源包括全局变量和静态变量、堆内存、打开的文件描述符表、信号处理函数等。线程私有部分包括栈每个线程有独立的栈空间、寄存器状态包括程序计数器、线程局部存储thread local。这道题的答案教科书上都有但面试官想听的其实是背后的风险既然堆内存是共享的那么多线程同时malloc或者操作同一个全局变量就可能出现数据竞争。如果两个线程各自持有指向同一个堆内存的指针一个线程释放了另一个线程还在用就是悬空指针。我当时还在答案里补了一句线程共享的还有errno变量。以前errno是全局的多线程环境下它必须是线程私有的否则一个线程出错另一个线程拿到的errno可能被覆盖。这个细节能体现你不是死记硬背而是真能应用到排查问题上。3.2 socket编程中TIME_WAIT的意义一个连接为什么不能立刻关有一道网络方向的简答题问主动关闭连接的一方为什么需要TIME_WAIT状态它存在的两个主要作用是什么。两个作用要记牢第一保证最后一次ACK能够到达对端。如果这个ACK丢了对端会重发FIN主动关闭方需要留在TIME_WAIT状态来处理这个重发的FIN。第二确保本端旧连接的报文段在网络中消逝避免新连接收到旧连接的延迟数据。我在写解析时建议你画一个TCP状态迁移图帮助记忆但不建议在考卷上画复杂的图面试时画图讲更合适。笔试时把状态名写全主动关闭方发FIN进入FIN_WAIT_1收到ACK进FIN_WAIT_2收到对端FIN后回ACK然后进入TIME_WAIT持续2MSL后才真正关闭。这道题对趋势科技这类公司尤其合理因为网络威胁检测系统要处理大量TCP会话TIME_WAIT堆积会影响服务端端口耗尽和连接重试策略。你如果能把大量短连接导致TIME_WAIT过多生产系统里怎么优化这层经验补进去会是个明显的加分项。3.3 动态库与静态库的区别笔试中的高频万金油还有一道简答题问静态库和动态库的区别以及动态库的优缺点。静态库在链接阶段被打进可执行文件程序运行时不需要额外依赖但可执行文件体积大多个程序使用同一静态库会产生多份副本。动态库在运行时被加载多个进程可共享内存中的同一份代码节省磁盘和内存升级库时不需要重新编译可执行文件但带来了依赖管理和版本兼容问题。这道题本身不难但我在笔试时补充了一个实际现象如果两个程序依赖同一个动态库的不同版本可能出现库地狱典型的解决办法是把版本信息写进动态库文件名或者使用符号版本控制机制。这样的补充比单纯背定义更有区分度也更能体现你真正发布过软件。4. 两道编程题的完整拆解反转链表与手写atoi编程题是这套卷子的重头戏。两道题难度适中但一上来就会筛掉一批没有边界条件意识的人。4.1 反转单链表从双指针到递归两种写法第一道编程题是反转单链表函数签名大概是struct ListNode { int val; ListNode *next; }; ListNode* reverseList(ListNode* head);这题不新鲜但胜在考察基础稳定性。迭代写法用三个指针pre、cur、next逐个翻转ListNode* reverseList(ListNode* head) { ListNode *pre nullptr; ListNode *cur head; while (cur ! nullptr) { ListNode *next cur-next; // 先保存下一个节点 cur-next pre; // 当前节点指向前驱 pre cur; // 前驱后移 cur next; // 当前节点后移 } return pre; }我当年考场里写的是教科书式的迭代版。但这里我建议你提前准备好递归版本因为面试官很可能追问能不能用递归写一个ListNode* reverseList(ListNode* head) { if (head nullptr || head-next nullptr) { return head; } ListNode *newHead reverseList(head-next); head-next-next head; head-next nullptr; return newHead; }递归版本的核心理解点是假设head-next已经反转完成当前要做的就是让原链表中的第二个节点指向第一个节点也就是head-next-next head然后断开head-next。很多人的误区是总想着怎么在递归中维护前驱指针其实递归的力气花在递归返回后如何把当前节点接到末尾。写完代码后我习惯性在草稿上自测三个case空链表、单节点链表、普通多节点链表。笔试时不允许跑测试代码手写自测非常能发现问题。比如反转后原头节点head的next必须置空很多人在这里漏写导致形成环形链表面试官一眼就能看出来。4.2 手写atoi边界条件就是面试官的连环追问点第二道编程题是手写atoi把字符串转成整数int my_atoi(const char *str);这道题表面简单骨子里考的全是防御性编程。你至少需要考虑以下输入情况字符串为空指针、开头有空格、带正负号、中间遇到非数字字符、数字超出int范围。我当年写出来的版本大概长这样int my_atoi(const char *str) { if (str nullptr) { return 0; } while (*str ) { str; } int sign 1; if (*str || *str -) { if (*str -) { sign -1; } str; } long result 0; while (*str 0 *str 9) { result result * 10 (*str - 0); if (sign 1 result INT_MAX) { return INT_MAX; } if (sign -1 result (long)INT_MAX 1) { return INT_MIN; } str; } return (int)(sign * result); }用一个long变量做累加再和INT_MAX/INT_MIN比较这是处理溢出最简单可靠的手段。如果你只用一个普通int来做累加到了边界附近再判断就已经晚了因为乘法加法本身就可能溢出。这个坑我见过很多人踩。另外两个容易漏的点第一正负号之后直接是字符串结尾例如输入-你的代码不能崩溃第二个while循环因为不满足条件直接退出返回0即可。第二如果输入是abc123按标准atoi语义应该返回0因为前面的非数字字符不是空格也不是正负号数字转换根本没有开始。我在考场上有几个人讨论过在中间遇到数字再接非数字字符怎么处理比如123abc正确做法是转换到a之前为止返回123。面试官很可能会追问如果数字超出int范围是返回INT_MAX/INT_MIN还是返回一个错误码不同的库实现有不同的约定C标准库atoi本身是未定义行为而C的std::stoi会抛异常。这个问题的答案并不唯一但你要能解释自己为什么选择这个方案。我回答的是作为安全软件公司解析不可信输入时宁可饱和到INT_MAX/INT_MIN也不能让调用方拿到一个被截断的错误值。这种表达比单纯给代码更有说服力。4.3 扩展最长无重复子串严格来说这套卷子编程题是否包含LeetCode风格题我记得不全但我强烈建议你把最长无重复字符子串这道题也准备好因为它属于固定窗口滑动类题最经典的敲门砖考法太常见了。int lengthOfLongestSubstring(const string s) { vectorint lastPos(256, -1); int left 0; int maxLen 0; for (int right 0; right (int)s.size(); right) { unsigned char ch (unsigned char)s[right]; if (lastPos[ch] left) { left lastPos[ch] 1; } lastPos[ch] right; maxLen max(maxLen, right - left 1); } return maxLen; }这里有个值得留意的细节用数组模拟哈希表时下标要转成unsigned char。如果直接用char做下标在某些平台上是带符号的遇到大于127的字节UTF-8编码的中文会出现负数下标属于未定义行为。这个坑在真实项目里踩到过并修复的人是能让面试官眼前一亮的。5. 答题之外的隐性标准面试官是从代码里读人的很多人以为笔试只要答案正确就稳了其实不是。对于开发岗面试官批卷子时会透过笔试题看你写代码的习惯和思维方式。5.1 手写代码的字体与布局也是得分点当时我有个切身的体会批卷的人真会看你代码的可读性。在纸上写代码没有编译器帮你检查变量命名规不规范、缩进整不整齐、有没有在关键步骤旁边写注释直接反映你平时写代码的随手习惯。我给一个非常实用的建议在纸上写代码宁可多花两分钟把变量名写完整也不要写a、b、c这种缩写。写代码的时候顺手写一行注释解释这一步在干什么比如cur-next pre这个操作很多人一眼就懂但手写卷子上写着让当前节点指向前一个节点会让批卷人知道你是理解着写的不是背模板背出来的。5.2 踩过的坑是最值钱的备考素材现在回头看这份卷子它的难点不在于题目本身有多难而在于它把程序员最容易忽略的细节铺满了整张卷子。我在准备校招时把C面试常考的坑整理过一个清单之后每次做笔试题都会用这个清单过一遍有没有检查空指针、数组会不会越界、整数会不会溢出、字符串有没有终止符、字节序假设是否正确、多线程共享数据有没有加锁。这套方法放到现在依然有效。准备任何一家公司的C/C开发笔试你都应该先过一遍自己的常见bug清单再去刷题。因为考场上人一紧张最容易犯错的往往不是高深的算法而是平时就没改掉的粗心习惯。还有一个建议在笔试前把你心仪公司的主营业务方向过一遍。趋势科技这种做安全产品的公司看重边界条件处理和不可信输入防御是很自然的事。同样的思路投嵌入式公司要多看位运算和内存映射投中间件公司要多看并发和IO模型。抓住这个规律你能从试卷结构里读出很多有效信息。最后再分享一个我自己的经验笔试结束后无论感觉好不好第一时间把记得的题目写下来趁热核对答案。我当时在回学校的公交车上就拿手机记了大概七八道题后来面试被问到时这些记录成了复盘的第一手素材。备考这件事很多时候比的就是谁更早开始整理谁的整理更体系化。这套2017年的题放到今天看依然没有过时希望你也能从这份解析里找到自己的复习坐标。
返回列表