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

资讯详情

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

腾讯PC客户端笔试题复盘:C++内存模型与底层机制

腾讯PC客户端笔试题复盘:C++内存模型与底层机制 腾讯2015春招PC客户端开发练习卷这份卷子在网上流传了很多年我最近又翻出来完整做了一遍。说实话现在看这份卷子很多题在今天已经不会再以同样的形式出现但它背后考察的底层能力——C内存模型、数据结构的边界处理、操作系统和网络的底层机制——依然是客户端开发岗位面试的核心。如果你正准备投PC客户端相关的岗位或者想系统查漏补缺这份卷子值得认真过一遍。同时这也是一份很好的“照妖镜”能检验出你过去几年的技术积累到底是扎实还是虚浮。这篇文章我会按自己的复盘顺序来写先还原这套练习卷的考察背景和整体结构再把客观题里那些经典考点逐一展开接着重点推演编程题的完整思考过程最后聊聊做题策略以及这套题对今天的参考价值。每一部分我都会尽量把“为什么这么考”讲透而不是只给个答案。1. 复盘之前先搞清楚这套卷子考察的是什么1.1 2015年春招笔试的流程和处境2015年的腾讯春招一般是实习生招聘面向次年毕业的同学流程大致是网申-在线笔试-技术面试-HR面试。在线笔试的时间通常安排在晚上两个小时左右全程在线OJ答题摄像头监控是后面才有的那会儿主要还是靠自觉。但别以为没人监考就可以放松笔试淘汰率非常高尤其是PC客户端开发这类核心岗位一个岗位收到的简历量级是几千份真正能进面试的不到5%。PC客户端开发这个岗位在2015年前后是一个很微妙的节点。移动端已经明显崛起但Windows客户端依然是腾讯的核心业务形态QQ、微信PC版、腾讯电脑管家这些产品都还需要大量C开发。所以那会儿的春招笔试题对C的考察是极其严格的不像现在有些客户端岗位可以用Java或Kotlin混过去那时候PC客户端开发基本就是C一条路走到底连C#都很少见。也就是说这套卷子从诞生之初就是一个高门槛的筛选工具。它不是给你过把瘾的练习册而是要把“能写业务代码的”和“真正理解C底层机制的”区分开。1.2 PC客户端开发为什么格外重视C功底有同学可能会问客户端开发不是主要写界面吗为什么笔试要考虚函数表、内存布局、深拷贝浅拷贝这些东西这里有实际业务背景。Windows客户端和Web开发有一个本质区别客户端程序在用户的电脑上运行你直接操作的是用户机器的内存、CPU、磁盘和网络资源。一旦发生内存泄漏、野指针崩溃、死锁卡界面用户不会给你留什么调试余地他们只会直接卸载你的软件。而PC客户端软件通常要长年累月运行今天写的一个错误可能三个月后才在某种极端场景下触发。这就要求客户端开发工程师对底层机制有足够的掌控力而不是仅仅“能跑就行”。另一方面老牌客户端产品普遍维护着大量的历史代码这些代码从Windows XP时代一路迭代过来充满了各种手写的内存管理、自研数据结构、诡异的多线程同步逻辑。如果你不懂底层原理光是接手维护这些代码就会崩溃。所以笔试题才会在内存、多线程、编译链接、网络协议这些方向上反复出题。我还记得有个老工程师跟我说过一句话客户端开发是“戴着镣铐跳舞”舞跳得好不好先看你对镣铐的理解有多深。这套卷子就是在测你对“镣铐”的理解。2. 客观题里的经典考点虚函数、内存布局和“必错陷阱”这套卷子的客观题部分是选择题为主覆盖了C语言基础、数据结构、操作系统、计算机网络、设计模式这几个方向。网上能搜到的题目版本虽然不一定完全一致但核心考点基本稳定。我挑几个当年正确率特别低的点展开说。2.1 虚函数与多态考的不是语法是C对象模型虚函数几乎是必考内容但面试官想看的不是“你知道virtual关键字”而是你对虚函数表的理解深度。原题里有一道和下面这个场景几乎一致的题目问下面代码的输出或内存布局class Base { public: virtual void func1() { cout Base::func1 endl; } virtual void func2() { cout Base::func2 endl; } int val; }; class Derived : public Base { public: virtual void func1() override { cout Derived::func1 endl; } virtual void func3() { cout Derived::func3 endl; } double dval; };这类题表面上是问虚函数覆盖实际核心是三个点对象内存布局Derived对象里vptr在对象起始位置然后才是基类成员再是派生类成员。val和dval之间还可能存在内存对齐的间隙。虚函数表是类级别的不是对象级别的。Derived的虚表里面func1被覆盖成Derived版本func2沿用Base版本func3是新增项。而如果你通过Base指针调用func3编译阶段就过不了因为静态类型决定的可见性不是动态类型。构造函数里调用虚函数不会触发多态。在Base构造期间vptr指向的是Base的虚表所以此时调func1只会进Base版本这个坑每年都有一堆人踩。当年的题目还有一道变体问下面这个代码的输出Base* ptr new Derived(); delete ptr;如果Base的析构函数不是虚函数那delete ptr就是未定义行为常见表现是Derived的析构函数压根不会被调用资源泄漏。这道题我记得考的是“析构函数为什么必须声明为virtual”而不是“如果不设virtual会怎样”。所以答题的时候要记住基类析构函数不设virtual多态delete时行为未定义不只是“泄漏”这么简单。2.2 内存对齐、大小端、指针运算“隐形杀手”三件套PC客户端是离内存最近的岗位之一所以内存对齐必考。常见形式是struct A { char a; int b; char c; }; struct B { char a; char c; int b; };问sizeof(A)和sizeof(B)分别是多少。答案是12和8。原因很简单A里面char后要补3个字节对齐到4然后int占4字节最后char占1字节再补3字节到成员最大对齐数的整数倍总共12字节B把两个char紧挨着放在前面占2字节再补2字节int占4字节总共8字节节省了1/3的空间。这题很多应届生不理解为什么要对齐。其实CPU访问内存是按字4字节或8字节为单位进行的如果int的地址不在4的倍数上CPU可能需要访问两次内存才能取到完整的数据。编译器用空格换速度这就是对齐的意义。而在实际客户端开发里尤其当你处理网络协议包、文件格式解析时结构体的内存布局直接关系到数据能否正确反序列化所以笔试考这个不是没道理。大小端也是一道经典送命题。0x12345678在大端机器上内存里是12 34 56 78小端机器是78 56 34 12。那会儿的PC是x86架构小端存储。这个知识点本身不难但考法比较阴让你写一个函数判断当前机器是大端还是小端。常规做法是取一个int的地址强转成char*看第一个字节存的是低位还是高位。这种题在客户端开发里是有实际场景的——网络协议字节序是大端的文件格式可能是小端的写代码时处理不好就是各种诡异bug。指针运算也会搭配数组和字符串出现。比如int arr[] {1, 2, 3, 4, 5}; int* p arr; cout *(p) endl; cout *p endl;这种题其实就是考优先级和副作用。p等价于(p)先取p指向的值再让p后移p是先让p前移再取指。答案分别是1和3。很多人把p理解成“取p的值再自增”混淆了“指针的自增”和“对象值的自增”这类题每次笔试都会刷掉一批人。2.3 操作系统与网络选择题从死锁到TIME_WAIT操作系统部分锁和同步是重点。有一道经典题问产生死锁的四个必要条件是什么互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。题目通常不会直接问四个条件而是给一个场景问“破坏的是哪个条件”。比如“允许进程强行抢占其他进程占用的资源”这破坏的是不可剥夺条件。这就要你把概念理解到位不仅要背下来还得能对应到场景。线程同步方面临界区、互斥量、信号量、事件的区别也是常考。关键是理解它们的本质差异临界区是用户态对象速度快但是只能同步同一进程内的线程互斥量是内核对象可以跨进程而且有更完善的等待机制信号量则是用来控制并发数量的计数器。有的同学把互斥量和信号量搞混记住一个核心区别互斥量是谁持有谁释放没有计数的概念信号量可以做资源计数可以有多个线程同时通过。网络部分TCP三次握手和四次挥手是必考。这类题普遍会问TIME_WAIT状态为什么存在以及如果客户端主动关闭连接TIME_WAIT出现在哪一端。背后的核心逻辑是主动关闭方要确保自己最后发出的ACK能够到达对方如果ACK丢了对方会重发FIN这时主动关闭方还能用TIME_WAIT状态里的socket去响应。同时TIME_WAIT还要保证旧的重复数据包在网络中自然消亡不会混入新连接。还有个高频考点是select、poll、epoll的区别。2015年那会儿服务端开发已经大量使用epoll了但客户端开发也会问因为客户端里也存在多路IO处理。区别的核心select有FD_SETSIZE限制每次都需要重新传入fd集合效率低poll用链表结构消除了数量限制但仍是轮询epoll通过事件驱动和回调机制只在活跃fd上返回适合大规模并发。这道题当年能答完整的人不多但对做客户端的来说理解事件驱动机制也有助于理解窗口消息循环。3. 编程题复盘从读题到AC的完整推演编程题是笔试的重头戏一般最少两道多的三道。腾讯那几年的练习卷通常包含一道链表类题目、一道字符串或数组类题目、一道需要动点脑子的算法题。我按当年的时间压力重新做了一遍把每道题的思考过程写出来比你直接看答案要有价值得多。3.1 第一道链表题永远在考边界处理出现概率最高的是“反转链表”和“判断链表是否有环”偶尔会换成“删除链表中倒数第N个节点”。看起来都是LeetCode原题但笔试环境里很多人翻车。以反转链表为例迭代写法我记得很清楚ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* curr head; while (curr) { ListNode* next curr-next; curr-next prev; prev curr; curr next; } return prev; }这题的核心不是背代码而是理解“pre、curr、next”三个指针的接力关系。很多人在循环里忘了保存next就改了curr-next导致链表断裂链表直接丢失后半部分。另一个常见错误是循环结束后返回curr这时候curr已经是nullptr返回的是一个空指针。正确应该返回prev因为prev此时指向原链表的尾节点也就是反转后的头节点。还有一个细节反转链表要求严格O(1)空间如果你用递归写法空间复杂度是O(n)在笔试里虽然能过但面试追问的时候很容易被质疑。因为递归栈深就是链表长度遇到长链表有栈溢出风险。所以建议优先掌握迭代写法。如果考“判断链表是否有环”经典做法是快慢指针。快指针每次走两步慢指针每次走一步如果存在环那么快慢指针必然在环内相遇。有的同学会问快指针走三步行不行理论上也可以但慢指针就不好保证能相遇了而且当环比较小时快指针可能会“跳过”慢指针所在的节点导致死循环。两步是最经典的方案建议不要自作聪明改步长。这道题真正考查的其实是“你对边界条件的敏感度”。空链表、只有一个节点的链表、链尾节点指向头节点形成完整环这三种情况你写代码的时候是否都考虑到了我当年就是在这上面栽了两次所以我现在看到链表题的第一个反应就是先列边界条件再写主体逻辑。3.2 第二道字符串/数组题暴力解法容易超时第二道题常见的场景是“找到字符串中第一个不重复的字符”或者“合并两个有序数组”。这类题在数据结构上不难但很多同学一上来就写双重循环复杂度O(n^2)然后在大数据量下超时。拿“第一个不重复字符”举例最优解是遍历两遍字符串第一遍记录每个字符出现次数第二遍从头找第一个次数为1的字符时间复杂度O(n)空间复杂度O(1)因为字符集大小是有限的ASCII256或Unicode的常用范围。实际上这种思路就是“空间换时间”的经典应用。有些同学第一遍想到了计数数组但第二遍遍历时不知道该怎么处理直接返回了第一个出现次数为1的字符对应的下标其实答案是判断这个下标是否存在如果不存在返回-1。“合并两个有序数组”的考点就是双指针从后往前遍历。为什么从后往前因为题目要求把合并结果放到第一个数组里而且这个数组预留了足够的空间。如果从前往后会把第一个数组里还没比较的元素覆盖掉所以必须从尾部开始谁大谁放到最后面一路前移。这个思路对于一个客户端工程师来说非常重要因为在实际开发中经常遇到需要原地合并、原地去重这类操作可以节省可观的分配开销。3.3 第三道算法题别急着敲代码先把规律想清楚最后一道题通常需要一点“题感”。常见的有“求无序数组的第K大”、“最长连续递增序列”、“爬楼梯的变体”。这类题不会直接告诉你用什么算法你要自己去判断。求第K大有两个常用方向一是用快速排序的partition思想每次划分后根据左边元素个数来判断第K大在左侧还是右侧平均时间复杂度O(n)二是维护一个大小为K的小顶堆遍历数组堆满后如果当前元素比堆顶大就替换掉堆顶最后堆顶就是答案时间复杂度O(n log K)。笔试环境里堆的方案更稳妥不用过度担心partition最坏情况退化到O(n^2)。这类题的关键是“别急着敲代码”。我见过太多同学读题三分钟就开始写代码写到一半发现思路不对涂涂改改最后连编译都过不了。编程题一般30-50分钟你应该花5-8分钟做三件事仔细读题圈出输入范围和数据规模。根据数据规模估算时间复杂度上限。如果n在10^5量级O(n^2)大概率过不了O(n log n)基本安全。想清楚边界情况空输入、只有一个元素、两个元素、全部相同元素。把这个流程走完再动手写虽然看起来“浪费时间”但反而能在最后的代码阶段更果断出错也更少。4. 做题策略和“隐形分”好白菜是怎么被拱走的我一直认为笔试不只是考你会不会更是考你在有限时间内如何做取舍。腾讯这套卷子的题量大时间紧行测题和技术题掺杂在一起如果不讲究策略可能出现技术题全对但行测没做完的尴尬局面。以下是几个可能直接影响你最终能否进面试的细节。4.1 行测题和图形推理题别死磕它们是抢分题不是拉分题腾讯的笔试一般都会带一部分行测题包括语言理解、逻辑推理、数字推理、图形推理。很多技术型选手一看到这种题就烦躁觉得“我是来写代码的不是来做公务员题的”。但你得明白这行测题在笔试中占的比重是客观存在的如果完全不答会被系统直接按低分处理。我复盘后的策略是行测题限时做不恋战。每道题只给自己一分钟超过一分钟直接标记跳过。但有一个前提要蒙一个答案。因为笔试用的往往是机器阅卷和分数排名多蒙对一个选项排名可能就往上走一点。有的同学行测部分空着很多题系统判定“答题率过低”直接失去了进入面试的机会。别问我是怎么知道的。图形推理题有个技巧优先看图形的数量变化、旋转对称、元素组成。找不到规律就果断放弃不值得为了一道题浪费5分钟。数字推理题往往是等差、等比、递推和奇偶项分开看这几种规律套路不多练几套题就能上手。4.2 技术选择题的“最安全选项”原则技术选择题里有些题一眼看不出答案这时候要会用排除法和“最安全选项”原则。什么是“最安全选项”就是选那个在大多数情况下成立、不需要额外附加条件的描述。举个例子问“关于new和malloc的区别下列说法正确的是”。这题的四个选项里有三个都看着都有道理但往往都存在一个限定词漏洞比如“new分配的内存一定在堆上”实际上new可以重载可以放在任意内存池中“malloc失败会抛出异常”malloc失败返回nullptr不会抛异常new失败才抛bad_alloc。最安全的选项是“new会调用构造函数malloc不会”这个在任何标准实现下都成立。类似的还有“关于const修饰成员函数的说法哪一项是正确的”。最安全的选项是“const成员函数不能修改非mutable成员变量”无论底层怎么实现这个语义都是对的。而那些“const成员函数内部禁止调用非const成员函数”这类选项在编译器层面确实是这样但很多人不知道const_cast可以打破限制容易被绕进去。所以选择题的答题逻辑不是找“正确的”而是找“最没有歧义的”。遇到两个都觉得对的时候看哪个更普适、更基础选它。4.3 编程题环境cin/cout还是scanf/printf别小看这场五杀那几年的在线笔试环境通常是提供一个在线编辑器语言基本锁定C。很多同学平时在Visual Studio里用cin/cout习惯了到了笔试里遇到大数据量的输入卡在读入上超时。不要小看这一点数据量一上万cin/cout如果不能关闭同步ios::sync_with_stdio(false)性能会差几倍甚至十几倍。而且在线OJ对编译警告的处理和本地的VS差异很大。比如变量名大小写、头文件漏include、使用了C11的新特性但编译器没开C11支持这些都会导致编译失败。所以我的建议是提交前先检查头文件。常用的是#include 、#include 、#include 、#include 少一个就会编译失败。尽量使用标准库容器不要自己手写链表或栈因为手写容易出野指针标准库容器经过充分测试性能足够。代码里不要出现中文字符尤其不要有中英文标点混用。在线编译环境对注释和string里的中文支持不一定好。还有个小技巧在写代码之前先在注释里把自己的思路写下来包括时间复杂度、空间复杂度、边界条件。这不只是给阅卷人看的也是给自己理清思路的。编程题改卷时有时候即使代码没全对但注释里的思路清晰、命名规范面试官还是会给一个“进入面试”的机会。当年帮同学改卷子的时候我看到有清晰注释的代码分数都会给得高一些。5. 这套题放在今天看还有多大参考价值5.1 考察内核没有变变的是呈现形式虽然2015年以后PC客户端开发的春招笔试里编程题的在线OJ已经基本取代了纯粹的纸笔答题但客观题的核心考点并没有变。我在2020年代仍然会看到很多面试官在技术面里问虚函数、内存对齐、死锁、TIME_WAIT这些老问题只是形式从笔试选择题变成了面试追问。原因很简单客户端开发的核心矛盾一直没有变。不管你是用MFC、Qt、Duilib还是现在的Fuchsia、Flutter Desktop、Electron你最终都要跟内存、线程、进程、网络打交道。框架可以迭代底层原理不会过期。甚至可以说现在的新框架把用户从很多底层细节中解放出来了但一旦遇到性能问题、崩溃问题、兼容性问题能救你的还是这些底层知识。以内存对齐为例现在开发中如果你用一些序列化库它们会自动处理对齐问题。但一旦你要自己解析一份二进制协议、做网络封包、或者做内存映射文件不懂对齐就会出错。这些场景在PC客户端中非常常见尤其是做音视频、游戏引擎、底层通信的岗位。5.2 如果你现在准备客户端开发岗位这份复习清单值得保存结合这套练习卷和我在客户端开发领域的观察我整理了一份复习优先级清单你可以按这个顺序查漏补缺C核心语言特性虚函数、智能指针、左值右值、移动语义、强制类型转换。这是基础中的基础任何一个环节薄弱都可能在面试中露馅。内存管理new/delete、malloc/free、内存对齐、内存泄漏检测、写一个简单内存池。客户端是内存密集型软件这个能力直接决定你能拿到的薪资档位。数据结构和算法链表、二叉树、哈希表、堆、动态规划、回溯。不要求你达到ACM水平但LeetCode Hot 100应该能熟练写出来。操作系统进程与线程的区别、线程同步、死锁、虚拟内存、用户态与内核态。尤其是线程同步客户端里各种UI线程和工作线程的通信全靠这部分知识兜底。计算机网络TCP/UDP、三次握手四次挥手、粘包问题、HTTP协议。做客户端必然要跟服务端通信这里的问题是高频面试点。编译与链接静态库和动态库、符号表、重定位、静态变量的初始化时机。这个问题在客户端里遇到得少但一旦遇到排查起来特别费劲属于加分项。每条清单后面对应的面试问题其实都能在这套练习卷里找到影子。比如你复习了操作系统进程线程就可以回去看看当年选择题里关于死锁条件的题目思路是完全一样。5.3 笔试之外客户端开发真正难的是工程问题刷题只能帮你过笔试但真正决定你能否胜任客户端开发工作的是解决工程问题的能力。举个实际例子一个Windows客户端程序在用户机器上偶发崩溃崩溃日志显示在某个对象的析构函数里调用了一个已经释放的函数指针。这个问题靠刷题是刷不出来的需要你理解虚表布局、生命周期管理、回调函数注册与注销的顺序。所以在准备笔试的过程里我一直建议不要只背答案而是每做一道题都追问自己这个知识点在实际项目中对应什么样的场景比如内存对齐对应协议解析虚函数和虚表对应插件架构线程同步对应UI线程和工作线程的数据交换。把题和场景挂上钩记得才牢面试被深挖的时候也不慌。对我来说这套腾讯2015春招PC客户端开发练习卷真正的价值不是那些具体题目而是它代表了一种“客户端开发工程师基本功”的验收标准。不管时间过去多久不管框架怎么换这套基本功都是不会过时的。如果你能把这张卷子里涉及的知识点都吃透再结合真实项目经验融会贯通那不管是2015年还是现在你面对客户端开发的笔试和面试都会比大多数人从容很多。
返回列表