
你如果准备过校招大概率见过这套搜狗2017校招C工程师笔试试卷。就算没做过原题网上流传的考点整理也早被各路面经抄了一遍又一遍。有意思的是这套题放在今天看依然很有参考价值——它没有堆砌偏题怪题而是把C工程师日常开发里真正要用的功底翻来覆去地考。字符串处理、内存管理、STL容器、基础算法这些题目看着朴素实际每一道都在筛“背过八股但写不出代码”的人。这套卷适合谁两类人。一类是正在准备校招或实习面试的在校生你需要用它来检验自己的C基础到底扎不扎实另一类是已经工作一两年、想回头补基础的在职开发很多题你当时可能囫囵吞枣过去了现在再看会有完全不同的体会。我这篇博文不打算把原题答案列一遍——网上能找到的东西没必要复述我重点把题目背后真正想考察的知识点、常见的错误思路、以及我当年踩过的坑讲清楚。文末还会附上笔试时的环境准备、代码调试习惯和一套自查清单照着准备比刷十套题都有用。1. 笔试全景与备考策略1.1 试卷结构分析搜狗这套试卷整体分三块单选、多选和编程题。单选题大概二十来道分值占比不高但覆盖极广从C语言基础到操作系统、网络、数据结构都有涉及甚至有少量概率题和逻辑题。多选题是拉开差距的地方——漏选、错选都不得分对知识点的精确度要求很高。编程题通常两道一道偏字符串处理一道偏算法设计允许用C/C作答在线OJ方式提交需要用标准输入输出。从我当年考场上的感受来说最阴险的不是编程题而是选择题里的那些“概念辨析”。比如考察sizeof和strlen的区别、虚函数能否是构造函数、const成员函数里能不能修改成员变量。这些题单独拎出来都不难但混在一起、加上多个选项组合时很容易在“好像对”和“其实不对”之间栽跟头。1.2 为什么一套老题还值得认真刷很多同学觉得2017年的题太老了C17都出来这么多年了还有必要看吗这个想法我特别理解但实际是错的。校招笔试考的是基础功底不是最新特性。搜狗这套题里考的vector扩容机制、智能指针引用计数、红黑树的插入旋转——这些在今天依然是笔试面试的高频考点而且短期内不会变。恰恰因为题目老网上有大量考点整理、题解、讨论帖学习材料反而更齐全。另外有一点容易被忽略老题能帮你判断一个公司的出题风格。搜狗这套题明显偏重工程实践基础对语言特性的考察明显多于对算法难度的考察。你拿它和字节、阿里那些动辄Hard级别算法题的卷子对比就能看出不同公司对C工程师的能力预期是不一样的。1.3 从搜狗笔试反推C工程师的能力要求站在今天的视角回头看搜狗这套题它真正想筛选的能力有三项第一是语言功底扎实。这不是说你能背出多少条语法规则而是面对一个具体问题时能准确说出“C会怎么处理”。比如传引用和传值的开销差异、深拷贝和浅拷贝的风险、vector扩容时迭代器为什么会失效——需要的是底层机制的清晰认知不是玄学背诵。第二是代码实现干净利落。搜狗编程题通常题干不长但边界条件很隐蔽。字符串去重、数字反转、链表操作这类题目大多数人都能写出来但能在15分钟内写出无bug版本的人并不多。考场上的拼的不是谁思路多惊艳而是谁更少犯错。第三是调试能力在线。笔试环境不像IDE那么友好没有断点调试全靠打印日志和自己review。平时刷题习惯用cout输出中间结果的同学在笔试里反而占优势因为他们已经形成了“预处理输入→分步验证→边界测试”的肌肉记忆。2. 核心考点深度复盘这些题到底在考什么2.1 面向对象三大特性与虚函数机制搜狗的单选题里至少有三四道和面向对象有关涵盖封装、继承、多态其中virtual相关的内容是绝对重点。虚函数这里有个高频考点虚函数表是什么时候生成的构造函数能不能是虚函数析构函数为什么通常要声明为虚函数第一问答案是编译期虚函数表在编译阶段生成运行期只是通过虚表指针去查表。第二问是否定的因为虚函数调用依赖虚表指针而虚表指针是在构造函数体执行之前初始化的此时虚表还没准备好。第三问是要防止“通过基类指针delete派生类对象”时只调用基类析构函数、不调用派生类析构函数导致资源泄漏。class Base { public: virtual ~Base() { std::cout Base destructor std::endl; } }; class Derived : public Base { public: ~Derived() override { std::cout Derived destructor std::endl; } }; Base* p new Derived(); delete p; // 输出Derived destructor 然后 Base destructor如果把上面Base的析构函数去掉virtualdelete p就只会输出Base destructorDerived的析构逻辑被完全跳过。这个例子我反复讲因为它几乎是笔试和面试里百考不厌的知识点。还有一个隐蔽的考点纯虚函数可以有函数体。这个说法是不是听着很反直觉但标准允许纯虚函数带定义体只是派生类仍然必须override它才能实例化。这是很多选择题里用来混淆选项的经典设计。2.2 从关键词看底层功力const和static的多种用法搜狗笔试对const和static的考察密度相当高而且总爱把两种用法混在一起出题。先说const。它至少有五种常见位置修饰全局常量、修饰局部变量、修饰指针本身int* const p、修饰指针指向的内容const int* p、修饰成员函数void func() const。笔试里考察最多的是后两种的区别。int a 10, b 20; const int* p1 a; // p1指向的值不可变但p1本身可以变 int* const p2 a; // p2本身不可变但p2指向的值可以变 // p1 b; // 合法 // *p1 30; // 非法 // *p2 30; // 合法 // p2 b; // 非法记法很简单const修饰谁谁就不可变。const int* p中const修饰*p所以指向的值不可变int* const p中const修饰p所以指针本身不可变。static在类里面的考点又不一样。静态成员变量属于类而不属于对象必须在类外单独定义静态成员函数没有this指针只能访问静态成员。笔试里经常给一段代码问输出结果很多同学搞混“局部静态变量的生命周期”和“程序执行顺序”从而答错。局部静态变量的初始化发生在第一次执行到该声明时但析构发生在程序结束时main退出或调用exit。2.3 现代C特性constexpr与C版本演进这套题虽然出在2017年但已经包含了对C11新特性的考察其中constexpr就是常客。它是在C11引入的关键字用来声明“可以在编译期求值”的函数或变量。C14放宽了限制允许在constexpr函数里使用循环和局部变量C17又把constexpr扩展到if和lambdaC20甚至支持constexpr虚函数和constexpr的std::vector操作。这个演进过程本身就值得记忆。面试官问“constexpr是哪个C版本引入的”不是单纯考察时间点而是想看你是否理解编译期求值和运行期求值的边界。constexpr int square(int x) { return x * x; } constexpr int result square(5); // 编译期计算 static_assert(result 25, result must be 25);static_assert是C11的另一个重要特性配合constexpr可以在编译期做校验。搜狗这类公司对C标准的跟进速度通常是积极的因为搜索引擎对性能敏感能在编译期完成的计算绝不拖到运行期。3. 内存管理与STL容器笔试的必考重灾区3.1 堆与栈不是你new了才叫堆搜狗笔试对内存管理的考查很有代表性——不会直接问“堆和栈的区别”而是给一段代码让你分析变量存放在哪里、生命周期如何、是否会造成内存泄漏。栈上的变量由编译器自动分配和释放速度极快但空间有限。堆上的变量通过new或malloc分配需要手动释放生命周期完全由程序员控制。这里有个经典误解很多同学以为只有显式new出来的对象才在堆上实际上std::vector内部的数据存储就在堆上哪怕你在栈上声明这个vector对象本身。内存泄漏的考察集中在三种场景第一种是忘记delete。这个最直观但笔试里很少直接考因为太简单。第二种是异常安全导致的泄漏。函数中new了一个对象后续代码抛出异常如果没有RAII机制保护delete语句永远不会执行。void func() { int* p new int[100]; // 这里抛出异常的话p永远不会被释放 doSomethingThatMayThrow(); delete[] p; }第三种是基类析构函数不是虚函数导致的“半释放”。前面2.1节已经说过了这种泄漏比较隐蔽很多同学笔试时看不出来。3.2 智能指针从裸指针到RAII搜狗这套题对智能指针的考察不算特别深但已经出现了shared_ptr的引用计数问题。近几年面试对智能指针的考察明显加深我不能不提。shared_ptr通过引用计数管理对象生命周期当最后一个持有它的shared_ptr被销毁时对象被释放。这里有两个高频考点。第一个是循环引用。两个对象互相持有对方的shared_ptr引用计数永远无法归零导致内存泄漏。解决办法是把其中一个改成weak_ptr。struct Node { std::shared_ptrNode next; std::weak_ptrNode prev; // 打破循环引用 };第二个是auto_ptr的黑历史。C11之前用auto_ptr它的拷贝操作会转移所有权导致原指针变成空指针这是非常反直觉的设计。C11标准引入了unique_ptr替代它后者禁止拷贝、只允许移动。搜狗这套题如果出到智能指针的选择题很大概率会考察“哪种智能指针可以用于STL容器”这种问题——答案是shared_ptr和unique_ptr不是auto_ptr。我当时的做法是记一个简单结论默认用unique_ptr共享所有权用shared_ptr需要打破循环或临时观察用weak_ptr绝对不用裸指针管理动态内存。3.3 STL容器面试必问vector扩容与map底层实现STL容器在搜狗笔试里占据的篇幅比智能指针大得多vector和map是绝对重点。vector的扩容机制可以说是STL面试第一题几乎到了必考的地步。vector底层是连续内存的动态数组当size要超过capacity时会执行以下步骤分配新内存块容量通常是原来的1.5倍或2倍不同编译器实现不同gcc是2倍Visual C也是约1.5到2倍之间。将旧元素拷贝或移动到新内存。释放旧内存。这里有两个相关考点。第一个是“迭代器失效”扩容后所有迭代器、指针、引用都会失效因为原内存被释放了。第二个是“频繁扩容的性能问题”如果预先知道需要多少元素应该用reserve提前分配足够容量避免多次扩容。std::vectorint v; v.reserve(1000); // 提前分配容量避免多次扩容 for (int i 0; i 1000; i) { v.push_back(i); }map的底层是红黑树增删查改的时间复杂度都是O(log n)。与之形成对比的是unordered_map底层是哈希表平均时间复杂度O(1)但最坏情况O(n)。笔试常考的问题包括为什么map的迭代器不会在插入元素后失效因为红黑树节点在内存中独立存在插入只改变指针指向不移动已有节点。为什么unordered_map迭代器可能在rehash后失效因为rehash会重新分配所有桶。如果你不想背这些结论可以换个角度理解真正常用的容器就几个知道它们“为什么要这么设计”比背住“时间复杂度是多少”有价值得多。3.4 迭代器失效笔试里的经典陷阱迭代器失效是STL容器笔试里最容易失分的点也是搜狗风格的选择题最喜欢出的方向。vector在执行insert或erase后被操作位置之后的迭代器全部失效。map和set则比较皮实插入操作不影响任何迭代器erase也只让被删除元素的迭代器失效。list同样只有被删除元素本身的迭代器失效。这几个结论背起来不难但实际写代码时经常会犯“在遍历中删除元素”的错误// 错误示范在遍历vector时erase元素 std::vectorint v {1, 2, 3, 4, 5}; for (auto it v.begin(); it ! v.end(); it) { if (*it % 2 0) { v.erase(it); // it已经失效it是未定义行为 } }正确写法是for (auto it v.begin(); it ! v.end(); ) { if (*it % 2 0) { it v.erase(it); // erase返回下一个有效迭代器 } else { it; } }erase返回被删除元素的下一个迭代器这是C11之后的标准行为也是解决“边遍历边删除”问题的标准姿势。笔试里如果遇到相关代码填空十有八九考察的是这个细节。4. 从选择题到编程题算法与代码题实战4.1 字符串处理校招编程题的半壁江山搜狗编程题几乎离不开字符串。这不是搜狗独有的偏好整个校招市场都是如此——字符串处理不需要复杂的数据结构却能充分考察代码的严谨性和边界条件处理能力。高频题目类型包括字符串去重、反转、判断回文、大小写转换、子串查找、字符串转整数或浮点数。每题都能玩出花来但底层能力是相通的字符指针的移动、越界判断、\0结尾的处理。以“字符串转整数”为例这道题在LeetCode上是中等难度但笔试正确率并不高。考察点很清晰空字符串处理前导空白字符跳过正负号处理数字字符的合法性溢出判断非法输入时该如何返回int myAtoi(const std::string s) { int i 0; int sign 1; long result 0; while (i s.size() s[i] ) i; if (i s.size() (s[i] || s[i] -)) { sign (s[i] -) ? -1 : 1; i; } while (i s.size() isdigit(s[i])) { result result * 10 (s[i] - 0); if (result * sign INT_MAX) return INT_MAX; if (result * sign INT_MIN) return INT_MIN; i; } return result * sign; }核心经验笔试写字符串题先把所有边界条件列出来再动手写代码。不要上来就写主逻辑写完再补边界很容易漏。4.2 排序算法底层冒泡排序与选择排序的考点排序算法是校招笔试的常青树搜狗这套题也涉及到了。不过它考的往往不是“让你手写快排”而是给出“某排序算法经过两轮后的数组状态”让你判断这是哪种排序算法。这里我建议把冒泡排序和选择排序的实现细节彻底弄清楚因为它们代码短、容易考而且经常作为对比选项出现。冒泡排序的核心思想是“相邻元素两两比较大元素逐步冒泡到末尾”一趟排序后最大值一定被放到最后。选择排序的核心思想是“每趟从未排序区间选出最小值放到区间最前面”一趟排序后最小值一定在开头。void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; // 优化本趟无交换说明已有序 } } void selectionSort(int arr[], int n) { for (int i 0; i n - 1; i) { int minIdx i; for (int j i 1; j n; j) { if (arr[j] arr[minIdx]) minIdx j; } if (minIdx ! i) std::swap(arr[i], arr[minIdx]); } }注意冒泡排序这个swapped标志的优化很多教材不讲但面试官问“怎么优化冒泡排序”时答出这个会加分。它利用“某一趟没有任何交换说明数组已经完全有序”的性质提前终止排序。4.3 不容忽视的模板算法快速幂和单调栈搜狗这类以工程实践为主的公司编程题通常不会考太冷门的算法模板但快速幂和单调栈是例外——它们代码量不大、思路巧妙、出场率极高值得反复练习。快速幂解决的问题是高效计算倍率次幂比如计算a^b mod m。朴素做法是循环相乘时间复杂度O(b)当b很大时效率极低。快速幂的核心思想是把指数按二进制拆解long long fastPow(long long base, long long exp, long long mod) { long long result 1; while (exp 0) { if (exp 1) { result result * base % mod; } base base * base % mod; exp 1; } return result; }时间复杂度降为O(log b)代价只是多记一个“base每次自乘、exp每次右移”的规则。笔试里求大数幂取模的题基本都是这个模板。单调栈常用于解决“下一个更大元素”“每日温度”等经典问题核心维护一个栈内元素单调的栈结构。代码量不大但要理解“什么时候入栈、什么时候出栈、出栈时做什么计算”非常关键。std::vectorint nextGreaterElement(const std::vectorint nums) { std::vectorint res(nums.size(), -1); std::stackint st; // 栈中存下标 for (int i 0; i nums.size(); i) { while (!st.empty() nums[st.top()] nums[i]) { res[st.top()] nums[i]; st.pop(); } st.push(i); } return res; }用单调栈维护一个“从栈底到栈顶单调递减”的结构每当遇到比栈顶元素更大的值就说明找到了栈顶元素的下一个更大元素此时出栈并记录结果。4.4 并发编程考点再看ABA问题搜狗这套题出得早2017年的试卷对并发编程的考察还比较浅但结合近几年面试趋势ABA问题几乎已经成了C并发领域的必问考点我必须专门讲一下。ABA问题发生在无锁数据结构中。简单说线程1读取共享变量为A线程2把变量从A改成B再改回A线程1再次比较时发现值还是A就认为没有其他线程修改过继续执行。但实际上变量已经被改过两次了这个“值没变”的判断是不可靠的。一个经典例子是栈顶指针的CAS操作。线程1读取栈顶为节点A准备把新节点N的next指向A此时线程2弹出了A随后另一个线程又压入了A内存地址相同线程1执行CAS时发现栈顶还是A就以为栈没变实际栈的结构已经完全变了。解决方案有两种一种是用带版本号的原子变量同时比较值和版本号另一种是使用hazard pointer管理节点回收确保节点不会在CAS前被复用。// 版本号方法示例 std::atomicuint64_t abaGuard; // 比较时同时比较值和版本号 uint64_t oldVal abaGuard.load(); uint64_t newVal (oldVal 0xFFFFFFFF) | ((oldVal 32) 1) 32; abaGuard.compare_exchange_strong(oldVal, newVal);笔试考到ABA问题时只要你能说清楚“为什么值相同不代表数据没有被修改过”以及“版本号能解决这个问题的原理”基本就能拿全分。5. 笔试环境准备与实战演练5.1 VSCode配置C/C开发环境笔试虽然通常在线OJ完成但平时的练习环境直接影响刷题效率。我用VSCode写C已经好几年了这里分享一套简洁稳定的配置方案。第一步安装C/C扩展。这是微软官方扩展提供代码提示、跳转定义、调试支持直接搜索“C/C”安装即可。第二步配置编译器。Windows用户建议安装MinGW-w64macOS用户直接安装Xcode Command Line ToolsLinux用户用包管理器安装g。第三步配置构建任务。在.vscode目录下创建tasks.json指定编译命令{ version: 2.0.0, tasks: [ { label: build, type: shell, command: g, args: [ -g, -stdc17, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true } } ] }这里我特意加了-stdc17。校招笔试环境通常支持C14或C17但部分同学本地默认编译器标准可能是C98或者gnu03写auto、nullptr、基于范围的for循环时语法检查不通过误以为是自己代码写错了实际上只是标准版本太低。5.2 编译器版本与C标准对照笔试中最尴尬的事情不是题不会做而是代码在本地编译通过、提交到OJ上却编译失败。最常见的原因就是C标准版本不匹配。先明确一点C11和C14是校招笔试的默认标准C17正在被更多公司支持但老平台上可能仍然生效C14。标准版本发布时间代表性特性是否建议在笔试中使用C981998模板、STL、异常不建议特性太老C112011auto、智能指针、lambda、移动语义必用兼容性最好C142014泛型lambda、返回值推导推荐增加不多但更灵活C172017if constexpr、结构化绑定、std::optional看OJ支持情况C202020概念、协程、范围for不建议OJ支持率不稳定我建议笔试中默认使用C11或C14风格的代码避免使用C17独有的特性。不是说你不能用std::optional而是万一OJ平台不支持你连改写的时间都没有。提一个细节有些同学喜欢用bits/stdc.h这个万能头文件本地可以但部分OJ可能不支持建议笔试前先确认平台是否允许。如果是在不支持的环境还是老老实实逐个包含需要的标准头文件。5.3 模拟笔试用一套固定流程训练肌肉记忆刷真题和自己模拟笔试是完全不同的体验。校招笔试通常限时90到120分钟编程题2到3道时间分配必须提前规划。我当年的方法是每周固定抽一个晚上模拟一次完整笔试。环境就用VSCode加终端不开IDE自动补全不开语法提示完全模拟在线OJ的裸写状态。计时90分钟选择题和编程题一起做时间到了立即停笔。做完之后一定要复盘。重点不是对答案而是问自己三个问题哪些选择题是蒙的哪些编程题花了太久时间如果在考场上时间不够该优先放弃哪道题我的建议是编程题第一题如果15分钟没有完整思路立刻跳过做第二题不要在一道题上死磕。校招笔试是“做对足够多的题”不是“完美解决一道题”。5.4 常见编译错误与排查技巧笔试紧张状态下编译错误比算法错误更容易导致翻车。我总结了一下最常出现的问题和排查顺序整理成了一张自查表。问题现象可能原因排查方式编译报错undefined reference to main文件名命名有误或入口函数签名写错检查int main()是否存在拼写是否准确编译报错could not convert ... from ...类型不匹配比如把std::string传给const char*参数查看报错行确认函数参数类型运行超时死循环或算法复杂度过高检查while条件是否可能永不满足确认算法是否O(n^2)以上复杂度运行时崩溃Segmentation fault数组越界、空指针解引用、栈溢出检查循环边界条件确认指针是否初始化大数组是否放到局部变量中导致栈溢出输出结果与预期不符边界条件未处理、多组输入未循环读取用极端用例测试空输入、最大输入、单元素输入我印象最深的是有一次模拟笔试代码逻辑完全正确但输出一直多一个空格。原来是循环体内每个结果后面都加了空格没有处理“最后一个结果后面不需要空格”的细节。这种问题在本地能发现但在OJ上只显示Wrong Answer非常浪费时间排查。所以平时练习时一定养成“输出格式严格匹配题目要求”的习惯多一个空格、少一个换行都可能判错。6. 面试官视角与备考建议6.1 搜狗这类公司看重什么我后来和技术圈的朋友复盘搜狗的面试风格大家的共识是搜狗对C工程师的要求是“扎实落地”不是追求最前沿的语言特性花活。笔试题目设计也体现了这一点。它不会问你std::apply的变参模板展开怎么写但一定会问你vector在扩容时迭代器为什么失效、shared_ptr循环引用有什么危害、map底层为什么用红黑树而不是AVL树。这些都是搜索引擎后端开发每天要面对的基础问题——你的代码要跑在千万级请求的线上环境任何一个资源泄漏或迭代器悬垂都可能酿成事故。所以在准备时不要沉迷于冷门特性。把C Primer的重点章节吃透配合Effective C的实践建议再加上LeetCode前200题的中等难度题目练习这个组合拳打下来应对搜狗这套笔试试卷绰绰有余。6.2 一套实用的冲刺备考计划如果你离笔试还有两到三周可以参考我这套安排第一周主打基础回顾。每天抽2小时过C核心语法包括类与对象、继承与多态、运算符重载、模板基础、STL容器。配合完成50道与数据结构和字符串相关的LeetCode简单或中等题。第二周主打真题模拟。找10套不同公司的C校招笔试题每天一套限时模拟重点分析错题分布找出自己的薄弱板块——是内存管理失分多还是算法题超时严重有针对性地专项突破。最后三天只做三件事复习错题笔记、整理各容器的时间复杂度表、练习标准输入输出处理。这里推荐一个我实践过的小技巧建立一个自己的“编码模板”文档包含常用的头文件组合、快读快写模板、快速幂模板、并查集模板、单调栈模板等。笔试时先花2分钟把模板敲出来备用能节省不少时间。6.3 笔试时的时间分配与心态管理最后聊聊考试现场的经验。笔试刚开始的5分钟不要急着做题。先把整张卷子扫一遍看清楚编程题有几道、难度如何、选择题总量多少心里有个时间预算。我通常的分配是选择题控制在40到50分钟每道题不超过2分钟编程题每道留20到30分钟其中前5分钟只读题和想思路不写代码。编程题如果完全没有思路先不要硬写在草稿纸上画一画示例数据的处理过程。很多时候思路是在模拟样例时蹦出来的而不是干想出来的。还有一条特别重要的建议如果一个知识点你有印象但不确定先标记出来跳过不要卡在一道选择题上。校招笔试的分值优势往往来自“会的题全部做对”而不是“不会的题蒙对”。7. 一点个人体悟搜狗这套2017年的C笔试试卷我前前后后刷过三遍。第一遍是在校招季准备阶段很多题做得云里雾里第二遍是工作第一年再看时发现很多考点和实际工作内容不经意间对上了第三遍是写这篇文章前我把所有考点重新复习了一遍发现十年过去了C的标准从C11走到了C20但这套题考察的核心能力——对底层机制的理解、对内存的敬畏、对边界的敏感——丝毫没有过时。如果你正在准备校招我想说刷题有用但别只刷题。每一道选择题背后都有一个“为什么”把这个为什么弄清楚比多做十道同类题更有价值。搜狗这套题里那些关于虚函数、智能指针、迭代器失效的考点本质上都是在逼你想清楚“C这门语言为什么这样设计”。把这些问题想透了将来无论面对哪家公司的笔试题你都会发现万变不离其宗。