搜狗C++笔试题复盘:从构造析构到虚函数与内存对齐
考过这套题的人后来基本都成了老油条。我先说结论搜狗2015年那套C工程师笔试题放在今天看依然不过时甚至比现在很多公司的笔试题更有参考价值。它不考偏题怪题但每一道题都在正经考察你“有没有真正写过C”而不是“会不会背C”。我当时刷完这套题最大的感受是原来我自以为熟悉的C其实有一堆模糊地带。这篇文章不打算做成“标准答案汇总”而是以复盘的方式把这套题背后真正想考察的东西拆开揉碎讲清楚。不管你是准备秋招春招还是工作几年想回头补基础都能从这里找到值得琢磨的点。我会把题目思路、隐藏考点、现场答题的节奏和一些复盘教训全部放进来内容比较长建议先收藏再慢慢看。1. 笔试题的考察地图从搜狗的岗位需求反推题目设计1.1 搜索公司要的C工程师到底是什么样的人搜狗做搜索、输入法、浏览器最核心的链路是搜索排序、索引存储、在线服务。这些业务对延迟极其敏感一个查询过来后台要在几十毫秒内完成分词、召回、排序、摘要生成。这个场景决定了它不可能用脚本语言顶着线上流量跑C是当时最务实的选择。所以这套题的底层逻辑非常清晰你要懂C的语言机制对象生命周期、内存布局、模板、STL你要懂操作系统和网络的底层交互多线程、锁、IO、TCP状态你还要具备扎实的数据结构和算法功底链表、树、字符串处理、动态规划。三项缺一不可。有意思的是这套题几乎没有“直接让你默写某个API”的题目。比如它不会问你string类的某个成员函数怎么用而是给你一段带坑的代码让你判断输出什么。这说明出题人默认你是用过STL的但更关心你能不能看出隐式类型转换、临时对象析构、引用折叠这种“运行时才暴露”的细节。1.2 整套卷子的题型分布与答题节奏从题型上看2015年这套题大致分为三块选择题、简答题、编程题。选择题覆盖C语言基础、数据结构、操作系统、网络简答题偏向让你口头描述某个机制比如虚函数表布局、进程间通信方式编程题则是手写代码一般是2到3道题目不算超难但要求你一遍写对。我的建议是选择题控制在25分钟以内简答题15到20分钟剩下的时间全部留给编程题。很多人挂在编程题上不是不会写而是前面磨蹭太久后面只能草草交卷。编程题忌讳“想好了再写”你边写边想先把框架搭出来再往里面填细节比闭眼默写要稳得多。2. 经典题型的逐题拆解与思维过程2.1 C语言机制题构造、析构、拷贝背后的真实顺序这套题里有一类典型题给定一个函数函数里创建了几个对象问输出顺序。表面考构造析构顺序实际考三件事局部对象析构顺序与构造顺序相反临时对象的构造析构时机拷贝构造函数什么时候被调用。class A { public: A() { cout A ctor\n; } ~A() { cout A dtor\n; } A(const A) { cout A copy ctor\n; } }; A func(A a) { return a; } int main() { A a; A b func(a); return 0; }这段代码输出什么关键在C11之前的标准下func(a)传参时触发一次拷贝构造return a时在当前编译器优化策略下可能触发拷贝也可能被NRVO优化掉。A b func(a);在C11之前执行的是“先构造临时对象再拷贝构造b”有优化时直接构造到b的内存上。如果编译时开启优化很可能只看到1次构造加1到2次拷贝如果关掉优化拷贝次数会变多。这个考点在现实中意味着什么你写的任何一个函数如果参数用值传递、返回用值返回在性能敏感代码里都会白丢性能。当年搜索后台的代码对拷贝次数极其敏感因为一个查询可能触发几千次小对象拷贝积少成多就是毫秒级延迟。所以这道题的真正加分点不是答对输出而是能补一句“所以这里最好是传const引用返回时用移动语义或者依赖RVO。”2.2 虚函数与动态绑定一道题看穿你对对象模型的理解C面试绕不开虚函数。搜狗这套题里有一类变形考法不直接问你虚函数表是什么而是给你一个继承结构让你判断某句调用输出什么。这种题表面考多态实际考你对“编译期类型”和“运行期类型”的区分以及缺省参数的绑定时机陷阱。class Base { public: virtual void foo(int x 10) { cout Base, x x endl; } }; class Derived : public Base { public: virtual void foo(int x 20) { cout Derived, x x endl; } }; int main() { Derived d; Base* p d; p-foo(); return 0; }绝大多数人知道输出是Derived, x 10因为虚函数调用走动态绑定但缺省参数是编译期静态绑定的。如果面试官继续追问“为什么标准这么规定”你如果能答出“因为编译器在编译调用点就要生成参数缺省值而此时它无法知道运行期到底是哪个类的函数被调用”这道题就直接从“背答案”上升到“理解对象模型”的层次。这类题背后真正的价值是提醒你写C代码时永远不要让普通函数、虚函数和缺省参数这三者混在一起搞“聪明设计”。实际项目里一旦有人这么写维护的人必然踩坑。2.3 内存对齐为什么你的结构体比预期大了几个字节另一个高频考点是结构体内存对齐。搜狗这类公司后台代码要处理大量网络协议包经常要把一个char缓冲区强转成一个结构体指针对齐做不对解析出来的字段全是乱码。所以笔试里很爱出这种题。struct A { char c; int i; short s; }; struct B { int i; char c; short s; };在32位默认4字节对齐下sizeof(A)是12sizeof(B)是8。原因A里char占0int从偏移4开始后面的short从8开始整个结构体大小需要对齐到4的倍数所以补到12B里int占0到3char占4short从6开始总大小8不用再补。我在实际项目里踩过一个坑自定义协议时结构体里某个字段类型定义错了把uint8_t写成了uint16_t导致整个协议包偏移全部变化线上解析出一堆乱码。排查了整整一个下午最后用offsetof宏打印每个字段偏移才发现问题。从那以后我定义协议结构体都习惯显式加#pragma pack(1)并且用static_assert验证sizeof是否符合预期。这个习惯也建议你养成笔试里顺手写出来属于加分项。2.4 操作系统题进程与线程本质是考“资源与调度”操作系统部分常见有两类题。一类是概念对比比如问进程和线程的区别。如果只答“进程拥有独立地址空间线程共享地址空间”能拿一半分。想拿全分要补上调度层面线程是CPU调度的基本单位进程是资源分配的基本单位。线程切换不涉及地址空间切换所以比进程切换轻量。但同进程内的线程如果崩了整个进程可能一起崩进程间反而更隔离。另一类是死锁题问四个必要条件互斥、持有并等待、不可剥夺、循环等待然后问怎么避免。这里有个很容易被忽略的细节光讲“破坏其中一个条件”不够最好能结合具体场景。比如“用锁的顺序一致性避免循环等待”然后展开说“所有线程必须按同一全局顺序加锁比如先锁A再锁B不允许先锁B再锁A”。这种回答才显示你真的处理过多线程问题。2.5 网络题从TCP三次握手到阻塞非阻塞IO网络题在搜狗笔试里几乎是必出的因为它们线上服务全是网络IO。考TCP三次握手属于礼貌题看似送分但深挖细节可以区分水平。比如问“为什么是三次而不是两次”标准答法是“为了防止失效的连接请求突然到达服务端导致服务端建立无效连接、白白分配资源”。但如果你能进一步说“两次握手时服务端无法确认客户端是否已经收到了自己的同步确认报文如果客户端不回应服务端会一直维持半开连接”这个回答就更有深度。还有一类题是阻塞IO与非阻塞IO的区别、select/poll/epoll的演进。这些虽然是Linux网络编程内容但C后台开发几乎天天接触所以笔试也会考。记住一个关键点select是轮询所有socket监听上限受FD_SETSIZE限制epoll是事件驱动只返回就绪的fd。回答时如果能提到“epoll适合高并发连接但活跃连接占比不高时反而能降低无效遍历”面试官会觉得你不是背书而是真在Linux下写过服务。3. 编程题的核心套路从高频算法到边界条件3.1 手写快排别以为背模板就完事了搜狗这套题里编程题部分有一道快速排序。很多人觉得手写快排太基础十秒钟背完一写就出各种边界bug。其实这类题考察的不是“你知不知道快排”而是“你能不能写出没有off-by-one错误的代码”。void quickSort(vectorint arr, int left, int right) { if (left right) return; int pivot arr[left (right - left) / 2]; int i left, j right; while (i j) { while (arr[i] pivot) i; while (arr[j] pivot) j--; if (i j) { swap(arr[i], arr[j]); i; j--; } } quickSort(arr, left, j); quickSort(arr, i, right); }注意几个细节一pivot 用left (right - left) / 2避免(left right)溢出二外层循环条件是i j不是i j三递归区间是[left, j]和[i, right]不是[left, pivot-1]这种基于位置索引的写法。笔试时光写对不够我建议你在代码旁边或者注释里简单写一下“这里用while不是for因为递归分区的关键是让左右游标跨越pivot”让阅卷人一眼看到你不是死记硬背。快排的时间复杂度平均O(n log n)最坏O(n^2)空间复杂度平均O(log n)、最坏O(n)递归栈深度。这些最好也能脱口而出编程题里经常让你分析复杂度。3.2 字符串相关题目回文判断、反转、子串匹配字符串处理在搜索业务里是家常便饭。笔试里最常见的三小题判断一个字符串是否为回文双指针从两端往中间扫注意处理大小写和非字母数字字符。反转字符串如果只是要求原地反转直接reverse(s.begin(), s.end())如果要求按单词反转比如hello world变world hello先反转整个串再反转每个单词。子串匹配暴力O(n*m)可以写但能写出KMP肯定额外加分。KMP的核心是前缀函数next数组笔试现场写KMP容易错我的建议是先把暴力法写对在注释里说明“这里可用KMP优化到O(nm)”如果时间允许再实现。这类题目的额外陷阱是“C风格字符串”和“C string”的区别。如果题目给的是char*要小心strlen的开销、结尾\0的处理、是否允许修改原串如果给的是std::string要注意size()返回的是无符号数不要和负数比较。3.3 链表题反转链表的两种境界链表题在笔试题里出现率极高因为它能同时考察你的指针理解、边界处理和迭代思维。最经典的反转链表三指针迭代法几乎是标准答案ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* cur head; while (cur) { ListNode* next cur-next; cur-next prev; prev cur; cur next; } return prev; }能写出这个不算本事遇到“反转区间链表”或者“每K个一组反转”还能写对才算经历。我实操过“K个一组反转链表”这道题是字节、搜狗这类公司特别喜欢出的变体因为它要把“反转”和“区间切分”两个思想合在一起。思路是先写一个反转局部链表的辅助函数reverseRange(head, tail)然后主函数中按K个节点切段拿到每段的头和尾反转后接回原链。写的时候有一个极易错的地方反转区间后的尾部要接上下一段的头否则链表就断了。笔试题里链表最容易踩的坑是空指针head为空、head-next为空、K大于链表长度。这些分支必须在写代码时同时考虑别写完主逻辑再回头补很容易漏。3.4 从快速幂到单调栈搜狗爱考的小众算法盲点除了常见的排序和链表题这套题里还出现过快速幂和单调栈。这两个点挺能体现搜狗的出题品味——它们不冷门但很多准备不充分的候选人会在这里翻车。快速幂的核心思想是二分幂把指数按二进制拆位底数不断自乘时间复杂度O(log n)。典型代码long long fastPow(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; }注意mod运算的优先级笔试题经常让你算(a^b) % c考察点也在于“先取模再乘”和“每一步都取模”防止溢出。单调栈则常用于求解“下一个更大元素”“柱状图最大矩形面积”这一类问题。核心性质是栈内元素保持单调递增或递减每个元素最多入栈出栈一次总时间复杂度O(n)。笔试问到这个点时一般会给你一个数组让你求每个元素右侧第一个比它大的元素。写法很固定用栈存下标vectorint nextGreater(vectorint nums) { int n nums.size(); vectorint res(n, -1); stackint st; for (int i 0; i n; i) { while (!st.empty() nums[i] nums[st.top()]) { res[st.top()] i; st.pop(); } st.push(i); } return res; }笔试题遇到单调栈要能说出“为什么复杂度是O(n)”——每个元素只入栈一次出栈一次摊还下来就是线性。这个解释比代码本身更能拿分。4. 从真题延伸出去的隐藏考点与C面试八股4.1 内存管理unique_ptr、shared_ptr、弱引用与ABA问题搜狗2015年那会儿C11已经普及笔试题里开始出现智能指针的考察。现在回头看智能指针已经成了C面试标配。它主要考察你是否清楚所有权语义以及C11之后内存管理思路的转变。我整理一个表方便对照复习智能指针所有权模式典型应用场景注意事项unique_ptr独占所有权工厂函数返回值、局部资源管理不可拷贝只能移动shared_ptr共享所有权多线程共享对象、回调捕获控制块额外开销小心循环引用weak_ptr弱引用观察shared_ptr不增加计数搭配lock()使用避免悬空这里有一个很容易被追问的C并发陷阱无锁编程中常见的ABA问题。具体来说是线程A读到共享变量值为X线程B把它改成Y又改回X线程A做CAS比较时发现值还是X认为没有变化但实际链表结构已经变了。C里解决这个问题常用带标记的原子指针或者使用std::atomic配合版本号。搜狗这家公司做高并发服务问ABA问题完全有动机因为他们的无锁队列和缓存热路径上真的会用到这些。4.2 STL底层原理从vector扩容到unordered_map的哈希策略C笔试必考STL但考得不像“字典背诵”而是“设计权衡”。比如vector的扩容机制每次扩容通常以2倍或1.5倍增长插入均摊时间复杂度O(1)。如果你只答“会搬移元素”就漏了关键点——当元素类型是复杂对象时扩容走的是移动构造还是拷贝构造取决于有没有 noexcept 声明。C11后vector扩容优先用移动构造来搬元素但移动构造函数不是noexcept的时候vector为确保强异常安全会退回拷贝构造。这个细节在笔试里答出来绝对是加分项。再来是unordered_map的哈希冲突解决C标准没有强制规定必须用链地址法但主流实现是哈希桶加链表。当桶链表过长时会触发rehash增大桶数量重新分布元素。问到哈希到负数的情况一般建议先std::hash转成size_t再做位运算取模避免负数对整型求余得到负数。4.3 设计模式在C中的落地观察者、单例与线程安全搜狗笔试如果出现设计模式相关题不会直接问“你说一下单例模式”而是给一个场景让你设计。比如有一个全局配置管理器在多线程环境下只会被初始化一次怎么实现线程安全的单例最简单的写法是C11之后的“Magic Static”class ConfigManager { public: static ConfigManager getInstance() { static ConfigManager instance; return instance; } ConfigManager(const ConfigManager) delete; ConfigManager operator(const ConfigManager) delete; private: ConfigManager() default; };C11标准保证局部静态变量初始化是线程安全的编译器会在初始化时插入相应的防护。这种写法比双重检查锁简单很多也比“饿汉式”支持懒加载。笔试时能写出这种单例再解释一下为什么线程安全基本上属于满分答案。观察者模式在GUI和消息系统里用得很多如果笔试让你实现一个简单的事件订阅发布核心就是维护一个订阅者列表通知时遍历执行。这里要注意的是通知过程中不能修改订阅列表否则迭代器失效。实际项目里我会用“拷贝列表再遍历”或者“加读写锁”来规避。4.4 从字符串数组初始化到读取整行语言细节决定代码质量热搜词里有一个很细的点C字符串数组初始化。题目考法通常是“char str[] hello;和char str[] {h,e,l,l,o};有什么区别”答案是前者会自动补结尾的\0所以长度是6后者长度为5没有自动补\0当字符串用会有风险。另一个常见实操是读入整行。笔试里经常要求处理带空格的输入用cin str只会读到空格前正确做法是getline(cin, str)。但如果前面用过cin n缓冲区里会残留换行符直接getline会读到一个空行需要先用cin.ignore()清掉。这个细节出题人特别喜欢埋因为它考察你是否真正在电脑上跑过输入输出而不是只刷题库。4.5 C11到C20constexpr的版本演进热搜词有“constexpr哪个C版本引入的”这其实是一个高频面试点。constexpr 是C11引入的。C14进一步放开允许在constexpr函数内部使用局部变量、循环和分支语句C17允许 constexpr lambdaC20支持 constexpr 虚函数和 constexpr 的std::vector。笔试问到这个很多人会卡在“C14之前constexpr函数体只能有一个return语句”这个点上。如果你能把版本演进说清楚说明你真正跟踪过标准而不是只会写代码。这里有个练习建议翻出你的编译器在C11、C14、C17三种标准下分别编译一段带循环的constexpr函数观察报错差异。自己做一遍比背十遍八股都管用。5. 现场答题策略与复盘心得5.1 时间分配选择题别恋战编程题先写框架笔试现场最忌讳“选择题纠结五分钟”。搜狗这套题选择题里有些坑比如某个题问你“哪个不是线程同步机制”如果你不确定先标记跳过最后再回头看。基本上每道选择题控制在1到2分钟超过就放下。编程题则完全不同哪怕思路不够完整先把函数签名写好、把主体的循环结构搭出来哪怕只过了一部分测试用例也比白纸强。我见过很多候选人编程题第一题写得极完美第二第三题全空。但阅卷的时候如果两道题没做基本就是一票否决。所以我的策略是先花两分钟看一遍所有编程题从最简单的开始做保证每道题至少拿到基础分再回头优化。5.2 编程环境与输入输出比赛和笔试的差别2015年那会儿笔试还大量采用纸笔答卷现在主要是在线OJ。不管哪种形式写C的输入输出一定要稳。在线OJ里最常见的错误是while (cin n)忘记处理多组数据或者读取字符串时没有考虑空格。我给一个建议写代码前先在草稿纸/注释里写好“输入范围”和“关键变量类型”。比如看到n最大可能到10^9就要用long long看到数组长度10^5就要考虑O(n^2)可能会超时。笔试不是炫技是求稳。能过测试用例的代码就是好代码。5.3 复盘做完题之后的查漏补缺方法刷完这套题我习惯把错题按知识点归类建了一个“C八股查漏表”。每个知识点后面记录了三个东西我的理解、一道典型题、一个容易踩的坑。比如知识点虚函数缺省参数一道题Derived类指针通过Base指针调用 foo()输出什么一个坑缺省参数是静态绑定虚函数是动态绑定实际面试前我就翻这张表十分钟能过完全部重点。这个习惯从搜狗笔试一直保持到工作后带新人效率很高。如果你觉得整理表格太费时间直接按这个思路给每个知识点写一两句话摘要也比零散刷题强。5.4 关于C学习环境的一些个人建议再聊点学习工具。VSCode配C/C环境是现在刷题的主流选择安装C插件、配置tasks.json和launch.json能用断点调试才能更快定位“为什么这段代码和我想的不一样”。另外Visual C Redistributable是运行很多Windows程序的基础运行库如果自己电脑上写C程序提示缺少它直接去微软官网下载对应架构的版本就行。学习C小游戏、手工实现冒泡排序/选择排序这类小项目对理解语言基础非常有帮助。建议在语言入门阶段别急着上框架先把一个程序从编译到链接到运行的完整链路跑通比背语法高效得多。6. 给准备笔试的人一张快速自查清单6.1 考前24小时必过的十一个核心点结合搜狗这套题以及近年主流公司的出题方向我挑出几个必须烂熟于心的点构造函数、析构函数、拷贝构造、赋值运算符的调用时机以及编译器优化对输出的影响。虚函数表的基本布局单继承和多继承下对象内存布局有什么差别。const在不同位置的含义顶层const、底层const、const修饰成员函数、constexpr。内存对齐规则以及#pragma pack和alignas的使用。static的关键字作用域静态全局变量、静态函数、静态局部变量、类静态成员。new/delete和malloc/free的混用风险数组new[]和delete[]配对。进程与线程的对比、锁的种类与适用场景、死锁产生条件和避免。TCP三次握手和四次挥手的状态变化TIME_WAIT为什么存在。排序算法在平均/最坏时间复杂度和空间复杂度的对比。快速幂、二分查找、链表反转这三种代码都要能无脑默写。栈和堆的区别以及堆栈溢出可能出现的场景。上面这些点不仅搜狗考几乎所有C后台岗都考。如果有一半你看着眼熟但说不出具体细节建议别急着海投简历先花两周把基础垒扎实。6.2 面试时如何用“结构化叙述”替代“背诵式回答”笔试是“写答案”面试是“说答案”。很多人技术不差但表达没有层次面试官问一句答一句拿不到好评价。我自己的技巧是“观点-原理-例子-补一句陷阱提醒”。比如问“shared_ptr线程安全吗”。先给观点“shared_ptr自身并不是完全线程安全只有引用计数那块是原子的”。然后讲原理“多个线程同时拷贝或销毁同一个shared_ptr时引用计数的增减是原子操作所以不会造成计数错乱但多个线程同时读写同一个shared_ptr对象本身是不安全的需要加锁或者用atomic_load/atomic_store”。再举例子“两个线程同时对一个shared_ptr赋值一个reset一个拷贝会出现data race”。最后补陷阱“weak_ptr提升到shared_ptr时用lock不要用构造函数参数否则会抛bad_weak_ptr”。这种回答方式面试官听完基本不会再追问这种层次的细节因为你的结构里已经包含了他想问的所有分支点。6.3 编程题交卷前的最后三分钟检查刷题时我发现一个非常实用的检查套路每次在“代码写完”和“交卷”之间必须再走三遍第一遍看“入参边界”n是0、字符串为空、链表只有一个节点代码会不会崩溃第二遍看“输出类型”题目要求返回字符串还是数组有没有转错第三遍看“复杂度有没有明显可以优化的点”如果写了O(n^2)但题目给的范围明显很大哪怕代码能跑也要标注一下“可用双指针优化”证明你意识到了问题。这三分钟的价值不低于前面三十分钟的思考。很多候选人题做对了但由于输入为空时没有做保护被扣了测试用例的分非常可惜。7. 考完想进搜索/后台方向下一步还能怎么补7.1 从笔试题到真实业务索引、缓存、并发模型搜狗2015年这套题背后其实藏着一个完整的“搜索后台技术栈”。如果你目标是搜索公司光会刷题不够至少要懂检索服务通常用倒排索引文档表、词典表、倒排链表。查询时按词查找倒排列表做归并求交集。懂这个后才明白为什么笔试常考“有序数组归并”“字符串处理”——这些是检索的底层原语。缓存层面要理解缓存穿透、击穿、雪崩以及LRU淘汰。C后台里常见用自己实现的双向链表加哈希表做LRU这道题在面试里出现频率很高建议亲手实现一遍。并发模型上搜索后台常见“多线程线程池”和“事件驱动epoll”两种。你可以从一个固定大小线程池的简单实现开始逐渐加上任务队列、工作线程、优雅关闭等机制理解线程安全任务队列为什么需要条件变量和互斥锁。7.2 学习资源推荐与最高效的刷题路径书籍方面《Effective C》和《More Effective C》建议精读一遍里面覆盖大量语言细节和常见坑搜狗这种老牌C公司的出题人很多就是这个风格的受益者。《STL源码剖析》适合进阶能让你真正理解vector、list、unordered_map底层结构。算法方面LeetCode按“数组、链表、字符串、树、动态规划、二分、单调栈”分专题刷而不是按题号刷。实操上我建议自己写一个“玩具搜索引擎”不要求完整但至少包含文档读取、分词简单按空格、建立倒排索引、布尔查询。这个项目规模适中做完之后你会发现笔试里那些算法题全都能在这个项目里找到落点。到时候面试官问“为什么学C”你就不需要背答案直接用这个项目讲。7.3 关于C面试的最终心态调整这套题真正难的地方不是题目本身而是它在短时间内对你知识体系做了一次全面体检。查出来的每一个漏洞都是你接下来重点复习的方向。我的体会是C这座山没有顶但你想进好的团队至少要有能力在这套题覆盖的范围内不露明显短板。做错几道题不用灰心把错题背后涉及的知识点连成网下一次再遇到同类问题你会答得比这次快得多。回头再看搜狗2015年这套C工程师笔试题它的价值早超出了“一家公司的招聘题”本身。它是一张很好的学习地图标记了C工程师这条路上值得深耕的技术坐标。把这套题吃透你拿到的不仅仅是一份工作还是一套后续不断迭代自己的底层框架。
