我保存了2018年360春招C++开发工程师岗位的笔试客观题,当时做完最大的感受是:这卷子不考偏题怪题,就是实打实考基础。C++开发岗的客观题,看起来是选择题,实际上是把程序员的基本功掰开揉碎了,放在一个个小场景里让你判断。指针、内存、字符串、排序、面向对象、操作系统、网络这些点,全是日常写代码要用的东西。准备校招C++岗位的同学、想系统查漏补缺基础知识的开发者,甚至带新人的技术组长,都可以拿这份笔试合集当一面镜子,照一照自己的底子是不是真的扎实。
我把这套题重做了一遍,再把每类题背后的出题逻辑、解题套路、容易踩的坑全部整理出来,尽量还原当年的考点分布。下面进入正题。
1. 这份卷子到底在考什么:题型设计与出题意图拆解
1.1 客观题不是知识点检索,而是基础能力体检
先说一个很多同学容易误解的地方:笔试客观题不追求你“背了多少知识点”,它考的是“你有没有在实际编程中被这些知识点坑过”。360作为网络安全起家的公司,C++开发岗的候选人会被要求具备很强的底层思维,所以客观题特别喜欢考“这段代码输出什么”“这个表达式值是多少”“这个程序会不会崩溃”这类问题。表面上是选择题,实际上每题都是一段微型的代码排错演练。
从题型结构上看,当年的卷子以单选题和多选题为主,偶尔穿插判断题。单选题覆盖C语言基础、C++语法特性、数据结构与算法复杂度、操作系统概念、网络协议基础;多选题则集中在C++的语言细节上,比如const的多种用法、构造与析构的顺序、STL容器的迭代器失效问题等。多选题是丢分重灾区,因为少选多选都不得分,这就要求你对知识点的掌握必须“精确到边界条件”,而不是模模糊糊知道个大概。
1.2 出题人真正想考的,是三层递进的能力
我把这套客观题的考点拆开以后,发现它其实是在按三个层次筛选候选人。
第一层是“语法敏感度”。这个层次考的是你有没有在一行行代码里被编译器教训过。比如sizeof和strlen的区别、指针自增自减的运算优先级、二维数组的指针传递、static关键字在不同位置的语义差异。这些题目如果只靠背书,很容易记混,但如果平时写代码时真的踩过这些坑,一眼就能看出答案。
第二层是“算法基本功”。360的笔试不会在客观题里出太难的手写算法,但会通过选择题来检验你对常见算法的理解深度。比如排序算法的稳定性、不同排序在最好最坏情况下的时间复杂度、二分查找的边界条件、字符串逆序和链表反转这类经典问题的空间复杂度。这类题的目的不是让你表演算法,而是确认你在真正设计系统时,能选对数据结构、估算出性能。
第三层是“系统全局观”。C++开发工程师写的不只是业务代码,还要跟内存、线程、网络、磁盘打交道。所以客观题里一定会掺入操作系统和计算机网络的选择题,比如进程和线程的区别、死锁产生的四个必要条件、TCP握手过程、数据库索引的数据结构。这些知识在笔试阶段不会考得太深,但它决定了你之后看线上问题、做性能优化时能不能找到方向。
2. C/C++核心考点逐项拆解:从语法到内存
2.1 指针与数组:笔试里最经典的分水岭
指针和数组几乎是每一份C/C++笔试题里都绕不开的内容,360这套题也不例外。出题人特别爱把指针和数组放在同一个表达式里,考察你到底理解的是“类型”还是“地址”。我捡一道典型的回忆版题目说:
int a[5] = {1, 2, 3, 4, 5}; int *p = a + 1; printf("%d %d", *(p + 1), *(a + 3));这道题没有太多陷阱,答案是3和4。但它的变体就开始迷惑人了,比如把p声明成int (*p)[5],或者把a传进函数后问sizeof(a)等于多少。很多人在这种地方丢分,是因为没有分清“数组名是常量地址”和“数组名在表达式里退化为指针”这两件事。数组名在绝大多数表达式中会退化为指向首元素的指针,但在sizeof和&操作符下不会退化。所以sizeof(a)是20,而sizeof(a+0)是8(64位系统下指针大小)。
另一个高频点是二维数组的指针操作。比如声明int b[3][4],那么b的类型是int (*)[4],b+1跳过的是一行,也就是16个字节,而不是4个字节。这种题你光凭“地址是多少”很难算对,必须建立起“指针的类型决定步长”这个观念。我建议你复习时自己在纸上画一画内存布局,把b、*b、b[0]、&b[0][0]这四个表达式打印出来对比一次,胜过于背十道题。
2.2 字符串函数与逆序输出:小题里的大文章
当年的卷子里有一类题,看起来很简单,比如“写出字符串逆序输出的实现”,但在选择题里它会被包装成“下面哪种实现方式是正确的”。网络上关于“字符串逆序输出C语言”的讨论非常多,说明这确实是笔试常客。核心考点有三个:原地逆序还是新开空间、strlen的返回值类型、逆序后是否需要手动补'\0'。
一个常见的错误实现是这样的:
void reverse(char *s) { int len = strlen(s); for (int i = 0; i < len / 2; i++) { char tmp = s[i]; s[i] = s[len - 1 - i]; s[len - 1 - i] = tmp; } }这个实现没问题。但如果你把循环条件写成i <= len / 2,当字符串长度为偶数时,中间两个字符会被交换两次,等于白交换一次,字符串变回原样。这种边界条件就是选择题喜欢的坑。另一个坑是有人会写出这样的代码:
char *reverse(char *s) { int len = strlen(s); char *p = (char *)malloc(len); for (int i = 0; i < len; i++) { p[i] = s[len - 1 - i]; } return p; }这段代码有两处致命问题:一是malloc(len)没有为'\0'留空间,越界写入;二是没有检查malloc的返回值。如果题目问“这段代码有什么问题”,正确答案就是“内存越界”。这类题目考查的其实是你写C代码时有没有养成“给字符串结尾留位置”的下意识习惯。
顺带提醒一下,strcpy、strcat这类不安全函数也是笔试常客,选项里经常会出现strncpy是否能替代strcpy的讨论。strncpy并不是万能的,它不会自动追加'\0',如果源字符串长度等于n,目标缓冲区就不会有结尾符。这个细节很多工作两三年的同事都会记错,但在笔试里它就是一道实打实的多选题。
2.3 排序与查找:复杂度判断是送分题还是陷阱题
排序算法在客观题里主要考复杂度和稳定性,很少让你完整手写。但我发现这道题有一个规律:越是常见的算法,越容易出“反直觉”的选项。比如快排的平均时间复杂度是O(n log n),最坏是O(n^2),这个大家都知道。但选择题里经常会把这个信息跟“快排是否稳定”混在一起,很多同学一紧张就会选错。
我把高频排序算法的关键属性整理成一个表:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定 |
|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
| 快速排序 | O(n log n) | O(n^2) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
注意,冒泡排序和插入排序是稳定排序,这个不难记。堆排序和快排不稳定,原因是它们都存在远距离交换元素的情况,会破坏相同元素的相对顺序。选择题常会这样出:给出一组数据,问“经过某排序算法第一趟后结果是什么”。这时候你必须知道每一趟排序结束后数据的局部特征。比如冒泡排序第一趟结束后,最大的数一定在末尾;快排第一趟结束后,基准元素一定在它最终应该在的位置上。记住这些“一趟特征”,比背代码管用得多。
另外,二分查找的边界条件也很常考。网上关于“字符串逆序输出C语言”“冒泡排序C语言”的搜索热度一直很高,说明这些基础点确实是笔试的刚需。二分查找我最建议你记住一种写法,然后反复用:左闭右闭区间[left, right],循环条件是left <= right,更新方式是left = mid + 1和right = mid - 1。只要固定这一种写法,就不会出现死循环或者漏查边界的问题。
2.4 C++面向对象和STL:客观题最喜欢挖的语言细节
C++部分的客观题是拉开差距的地方。因为C语言部分大家都会用,但C++的语言细节,比如构造析构顺序、虚函数机制、const的各种位置、STL的迭代器失效,很多人只是“听说过”,没有真正在工程里验证过,遇到多选题就会发懵。
构造和析构顺序是选择题常客。规则其实很简单:构造时先基类后派生类,先成员后自身;析构时顺序完全相反。但如果涉及虚继承,情况会复杂一些,虚基类的构造函数最先执行。当年的题目里有一道是这样考的:
class A { ... }; class B : public A { ... }; class C : public A { ... }; class D : public B, public C { ... };问创建D对象时,A的构造函数执行几次。正常情况下执行两次,因为D通过B和C两条路径各自继承了一份A。但如果B和C都虚继承自A,那就只执行一次。这个考点非常经典,也是“菱形继承”问题的核心。
STL部分的考题集中在迭代器和容器选择上。比如vector在插入元素导致重新分配内存后,所有迭代器都会失效;deque在中间插入元素会使所有迭代器失效;list和map在插入元素时不会使已有迭代器失效,但删除元素时只有指向被删除元素的迭代器失效。这类题没有捷径,就是需要你把每种容器的底层实现(连续内存、链表、红黑树)和迭代器失效规则对应起来记。
另外auto_ptr在C++11之后已经被废弃,当年的笔试题还在考它的所有权转移语义。现在复习的时候,建议把它升级为unique_ptr、shared_ptr、weak_ptr的考察。智能指针的考题往往会结合“循环引用”出多选,shared_ptr互相引用会导致内存泄漏,解决办法是把其中一个改成weak_ptr。
3. 笔试中的系统与网络知识:C++工程师的地基
3.1 进程、线程与内存分区
C++开发工程师写的程序最终要跑在操作系统上,所以笔试里一定会有操作系统相关题目。这一类题不是死记硬背,而是跟你写代码时的选择紧密相关。比如“进程和线程的区别”,最常见的正确表述是:进程是资源分配的基本单位,线程是CPU调度的基本单位;同一进程的线程共享地址空间,而进程之间地址空间相互独立。
跟C++关系最密切的是内存分区问题。选择题会给你一段代码,问其中的变量分别存储在哪个区域:
- 全局变量和
static变量存储在静态区(也叫全局区); - 函数内定义的局部变量存储在栈区;
malloc或new出来的内存在堆区;- 字符串字面量等只读数据存储在只读常量区。
一个比较隐蔽的考点是:static局部变量存储在静态区,生命周期是整个程序运行期间,但作用域仍然是函数内。这个知识在“构造函数执行顺序”里也会有变体。比如一个函数内有static对象,第一次调用时执行构造函数,程序结束时才执行析构函数。如果面试官喜欢延伸,会接着问“静态局部变量是否线程安全”,答案是不安全,初始化时在多线程环境下存在竞争。
new和malloc的区别也是客观题高频中的高频。我建议你从四个维度去记:一是new是运算符,malloc是库函数;二是new会调用构造函数,malloc不会;三是new返回类型安全的指针,malloc返回void*;四是失败时的行为不同,new抛出bad_alloc异常,malloc返回NULL。多选题特别喜欢把“失败返回NULL”和“new也会返回NULL”混在一起,就是考验你清不清楚C++98时代new(nothrow)的特殊写法。
3.2 计算机网络与操作系统高频点
网络部分在客观题里不会出得太深,但基本概念必须拿分。TCP三次握手几乎是必考的,核心不是背“SYN、ACK”这几个词,而是理解为什么是三次而不是两次:因为三次握手能确认双方的收发能力都正常,并且能避免历史重复连接请求对服务端造成资源浪费。选择题里经常出现的干扰选项是“两次握手也能建立连接”,这个表述只有在不考虑历史重复报文和资源分配的情况下才勉强成立,在标准答案里必须判错。
死锁的四个必要条件(互斥、持有并等待、不可剥夺、循环等待)也是老面孔。题目会给你四个描述,让你选出哪几个属于死锁的必要条件。这个只要背住就行,但要注意区分“循环等待”和“等待图中有环”的关系:死锁一定有环,但有环不一定死锁,因为持有资源的线程可能马上释放资源。
数据库题在C++岗笔试里不会占太多比例,但常常会出现“索引用了什么数据结构”这道题。正确答案是B+树。B+树为什么适合做数据库索引,因为它可以有效降低树高,减少磁盘IO次数,同时所有数据都存储在叶子节点,方便范围查询。这个知识点如果你只看结论不看原因,遇到变体题(比如“为什么不用红黑树”)就会答不上来。
3.3 Linux与编译链接的基础考查
很多同学容易忽略的另一个客观题来源是Linux基础。C++开发工程师日常要在Linux环境下编译、调试,所以卷子里偶尔会出一些命令题。常考的无非是:grep按行匹配文本、ps查看进程、top动态查看系统负载、gdb调试程序、nm查看符号表、objdump反汇编。这些题本身不难,难的是你平时根本没在Linux下工作过,纯靠背选项容易记混。
编译链接的过程也是一个高频考点:预处理(宏展开、头文件包含)、编译(生成汇编代码)、汇编(生成目标文件)、链接(合并目标文件和库)。选择题里会说“下列哪个阶段处理#define宏”,答案是预处理阶段。还有一道很经典的题:-static静态链接和动态链接的区别。静态链接会把库代码复制进可执行文件,启动快但文件大、更新库需要重新编译;动态链接在运行时加载共享库,节省内存但可能出现“找不到动态库”的问题。现在容器化部署越来越普及,这个考点在面试环节还可能被追问成场景题。
4. 实操:刷题时如何拆解一道选择题,拿到90%正确率
4.1 选择题的标准化解题流程
很多人做选择题是“凭感觉”。但你要知道,笔试是限时的,而且有些题是故意“挖坑”的,靠感觉做题等于把命运交给运气。我给自己定的流程是四步:看选项、判类型、带边界、快排除。
第一步先看选项,而不是先看题干。因为客观题的正确选项往往是“最正确”的那一个,而不是“绝对正确”的那一个。把四个选项扫一遍,如果发现有两个选项说的是同一件事、只是表述不同,那正确答案大概率在这两个之间,剩下的两个是陪跑。第二步判断题目类型:这是编译器行为题、算法复杂度题,还是系统概念题?不同题型的做题策略不一样——算法题可以手推小数据验证,系统概念题得靠定义判断,编译器行为题必须代入“如果是我写这段代码会怎样”的经验。第三步,往极端情况下带边界值。比如题目问“以下哪种操作会导致vector迭代器失效”,你就往“重新分配内存”这个最典型的情形上靠。第四步排除明显干扰项,把正确答案锁死。
这个流程看起来简单,但实际做题时很多人会跳过“看选项”和“带边界值”。尤其是多选题,必须一个选项一个选项地验证,不能用排除法排除到最后一个就草率选中,因为多选题正确答案的个数不固定,有时候四个全对。
4.2 客观题的时间分配与错题整理方法
按照一份笔试45到60分钟的客观题量来算,我建议你单选题控制在每题1分钟以内,多选题每题1分半到2分钟。如果一道题超过2分钟还没有思路,立刻跳过,先把会做的全部做完,再回头啃硬骨头。360的笔试一般不会因为一道题卡住让你交白卷,但因为心态爆炸导致后面简单题也出错的情况却很常见。
刷题之后的整理比刷题本身更重要。我的方法是这样:准备一个错题本,不抄原题,把错题转化成一个“知识点+挖坑点”的组合。比如错误选项是“strncpy总是自动追加'\0'”,你在错题本上记下“strncpy在n等于源字符串长度时不追加'\0'”。再比如错题是“快排第一趟后基准元素一定在正确位置”,你记下“快排一趟特征”。这样一来,第二遍复习的时候,你只需要看几十条这样的“坑位记录”,半小时就能过完一遍,比重新做一遍题高效得多。
另外我特别建议你整理一张“选项高频陷阱清单”。比如sizeof和strlen混用、指针自增与取值运算符的优先级、++i和i++的返回值区别、const int *和int * const的区别、delete和delete[]混用。这张清单刷题时会越滚越长,到考前看一遍,稳定提分。
4.3 模拟真实笔试环境的检查清单
我当年吃过一个亏:线上笔试时,编辑器没有自动补全,代码题手写代码还好,客观题倒是没什么影响,但编译环境切换特别费时间。所以正式笔试前,务必先调试好在线笔试平台,熟悉它的代码编辑区、编译按钮、交卷确认流程。有些平台还支持本地IDE写代码再贴上去,但如果平台禁用了复制粘贴,你就得提前适应网页编辑器。
时间安排上,我的建议是客观题和编程题分开看待。先把客观题全部做完,再做编程题。因为客观题涉及的知识面很杂,越到后面思维越疲劳,先解决客观题能保证基本盘。编程题通常分值大,但也很容易卡壳,把它放到后面,至少不会出现“客观题没时间做,编程题也没做出来”的两头空局面。
5. 高频错题与避坑实录:我自己踩过的那些坑
5.1 五个最常见的丢分点,我直接整理成表
我在重做这套题时,把最容易丢分的知识点挑了出来。不敢说覆盖全部,但下面这几类是我自己以及身边同事当年都栽过的。
| 考点 | 常见错误理解 | 正确结论 |
|---|---|---|
| sizeof 和 strlen | strlen返回数组大小 | strlen返回字符串长度,不含结尾'\0';sizeof在编译期计算,含结尾符 |
| 指针自增与取值 | *p++等价于(*p)++ | *p++先取*p再指针自增,(*p)++是先取值再让值自增 |
const修饰 | const int *p和int const *p不同 | 两者等价,都表示指向常量的指针;int *const p才表示指针本身是常量 |
| 静态变量 | static局部变量作用域扩展 | 作用域不变,生命周期扩展到整个程序运行期间 |
| 容器迭代器 | vector插入元素后所有迭代器一定失效 | 只有插入导致重新分配空间时所有迭代器失效,未重新分配时指向插入位置之后的迭代器失效 |
这里面*p++那道题尤其经典。C++笔试里几乎每份卷子都会出,每次都有不少人做错。它的优先级规则是:后置++优先级高于*,所以*p++等于*(p++),先返回*p的值,再把指针向后移动一位。如果想先让指针移动再取值,应该写*++p。这种语言细节不写代码的人根本不会留意,但只要你在调试或遍历数组时写过类似的表达式,基本秒答。
5.2 遇到有歧义的题,怎么选才不亏分
有些笔试题本身存在争议,比如“运算符优先级”相关题目在不同编译器下可能出现不同行为,或者某道多选题在不同教材里表述不一样。遇到这种情况,我给自己定了一个原则:按标准工程实践来选,不按犄角旮旯的奇技淫巧来选。
具体做法是:如果某个选项的描述在绝大多数情况下成立,只有极端的未定义行为下不成立,那这道题的出题意图多半是“考察你懂不懂未定义行为”,而不是“考察这个选项是否绝对正确”。比如“i = i++的值是多少”这种题,直接选“未定义行为”或者“不要写这种代码”往往就是正确方向。如果你在同一个选项里看到了“一定”“肯定”“总是”这种绝对化词,并且这个说法涉及语言标准未明确规定的区域,那这个选项大概率是错的。
还有一点,多选题拿不准时,宁可少选不要乱选。很多在线笔试平台的规则是“少选得部分分,错选零分”,这时候保证“选上的选项都是对的”比“凑齐所有正确选项”更重要。这个策略并不矛盾,因为少选只是少得分,错选直接归零。
写在最后的一点体会
重做这套题之后,我最大的感受是:笔试客观题看似在考记忆,实际上考的是你平时写代码时有没有建立起“被坑过”的条件反射。指针越界、字符串缺结尾符、迭代器失效、内存泄漏,这些坑在笔试里是选择题,在真实项目里就是线上事故。2018年的题目到现在已经过去好几年,但基础考点没有变,甚至可以说,现在写C++服务端遇到的大部分疑难杂症,根子都还是这些基础概念没吃透。
如果你正在准备类似的笔试,我的建议是:别光背题,动手把每个考点写成小代码跑一遍。比如const的各种组合、strncpy的结尾符行为、vector和list在不同操作下的迭代器变化,这些用几行代码验证一下,印象比看十道题都深。基础越扎实,笔试题越简单——这是我在这么多年写代码、带新人、刷题库之后最想告诉你的一句话。