前几天整理移动硬盘,翻出了当年参加深信服校园招聘C/C++软件开发岗G卷时留下的草稿和代码截图。说实话,那次笔试给我的印象比很多公司的现场面试还要深:题量大、覆盖面广、语言细节考得极细,几乎把"你到底有没有真正写过C/C++"这件事摊开来看。后来我带过一些应届生,也会拿G卷当模拟题练手,发现学弟学妹们最容易挂在同一个地方——不是不会写算法,而是死磕选择题上的语言陷阱,把编程题的时间砍掉了。
这篇内容我尽量还原G卷的考察逻辑和典型题型,并结合我自己的笔试、面试复盘经验,讲讲C/C++开发岗备考时真正值得花时间的点。如果你正在准备深信服的校招,或者目标就是网络、安全、云计算这一类偏底层的C/C++岗位,这篇文章应该能帮你少走不少弯路。
1. 拿到G卷的第一印象:考点分布和整体逻辑
不管哪一年的G卷,它的出题思路都带着浓厚的"产品驱动"味道。深信服的产品线覆盖了终端安全、超融合、云桌面、SD-WAN、上网行为管理这些方向,而这些产品的底层大量依赖C/C++实现。客户端组件要直接操作文件、进程、网络数据包,高转发性能模块要贴近内核和协议栈,运维平台的后端服务也要考虑高并发和内存模型——这就决定了笔试卷子的考察维度必须覆盖语言、算法、系统、网络四个层面。
1.1 为什么深信服需要大量C/C++工程师
很多同学会问:明明是软件研发岗,为什么笔试里C/C++语言细节题比重这么大?我的理解是,安全产品和企业级硬件设备里的代码,很多是长生命周期项目。一个EDR客户端可能要在Windows和Linux两个平台上跑五年以上,一个网络转发模块要持续处理几万条并发连接。这种场景下,写代码的人如果不懂内存布局、不懂编译链接过程、不懂多线程下的竞态条件,线上出问题会非常难排查。
所以在G卷里,出题人不是在故意刁难你,而是在模拟"你能不能直接上手维护一个真实产品"。
1.2 一套G卷的典型结构与时间分配
不同批次的G卷题型会有微调,但从我见过的情况来看,大致可以归纳为三个模块:
| 模块 | 题型 | 建议用时 | 考察重点 |
|---|---|---|---|
| C/C++语言基础 | 单选、多选、程序改错 | 30到40分钟 | 指针、内存、C++特性 |
| 数据结构和算法 | 2到4道编程题 | 60到70分钟 | 字符串、链表、动态规划 |
| 操作系统与网络 | 单选、简答 | 20到30分钟 | 进程线程、TCP/IP、Linux |
这里有个很容易犯的错误:把太多时间花在选择题上,尤其是那种在两个选项之间纠结的题。我的经验是,拿不准的选择题先凭第一印象标记,后面做完编程题再回头想。因为编程题只要AC一道,往往能顶好几道选择题的分,而一道纠结很久的选择题不一定能拿分。
1.3 拿到试卷后的三个决策
进笔试页面之后,我习惯先花一两分钟通读全部题目,而不是急着做题。第一是确认编程题有几道、难度大概在什么位置;第二是看有没有需要写完整程序还是只写核心函数;第三是判断自己的熟练度,把最有把握的编程题排在前面做。这种"先侦察再进攻"的策略在紧张状态下特别有用,能避免最后发现还有一道很简单的编程题没来得及做。
2. 选择题里的C/C++陷阱,每个都是实打实的工程问题
G卷的选择题很少考那种"背概念就能选出来"的题目,而是喜欢把代码片段、编译结果和运行行为结合在一起考。这类题做错的原因往往不是知识点没学过,而是平时写代码时根本没踩过那个坑。
2.1 指针、引用与内存布局
笔试里出现频率最高的一类题,是关于字符串字面量、数组名和指针运算的。
char *p = "hello"; p[0] = 'H';这段代码在绝大多数平台上会直接崩溃,因为字符串字面量存在只读存储区,试图修改它是未定义行为。很多同学能背出"字符串字面量不可修改"这句话,但笔试换成const char *p和char *p的差异时又容易蒙。我的记忆方法是:从变量名开始,遇到const就修饰它左边的东西,如果左边没有东西就修饰右边。这样const char *p和char *const p的区别就永远不会忘。
另一个高频考点是数组和指针的区别。sizeof(array)在数组定义所在作用域返回整个数组大小,但数组作为函数参数传递后,会退化成指向首元素的指针,sizeof只能拿到指针大小。笔试里经常给一个看起来人畜无害的函数,内部用sizeof(a) / sizeof(a[0])计算元素个数,结果算出个错误数值。这就是典型的"看着对、跑起来就错"的工程问题。
2.2 编译、链接与关键字语义
C++笔试里static、const、volatile、extern "C"这些关键字几乎年年出现,而且会反复换着角度考。
static在不同位置的语义完全不同:局部变量变成了静态存储期,函数内部状态跨调用保留;文件内的全局函数或变量被限制为内部链接,其他编译单元访问不到;类成员变成所有对象共享。一个多选组合题能把这三种情况全部覆盖到。
const在C和C++中的差异也值得多看一眼。C语言里const int n = 10;不能直接用来定义数组大小,因为它本质上是只读变量;C++里const有编译期常量的语义,可以用来定义数组长度。这个区别在G卷的C/C++混合题目里经常出现。
volatile则是告诉编译器这个变量可能在外部被修改,不要优化掉。写嵌入式、写驱动、开发网络转发程序的人基本绕不开它,所以在这个岗位的笔试里出现并不意外。
2.3 C++11/14特性是必须掌握的底线
现在的G卷早就不再只考C with Classes了,而是明确覆盖C++11之后的新特性。我印象比较深的高频考点有:
shared_ptr、unique_ptr、weak_ptr的使用场景和区别,循环引用为什么会导致内存泄漏;- 移动语义和右值引用,
std::move到底做了什么,为什么能减少拷贝; vector的扩容机制,capacity 增长时迭代器为什么会失效;- lambda 表达式捕获列表的
[=]、[&]、[this]语义。
如果没系统学过这些,光靠学校课程里的老C++是吃亏的。我的建议是笔试前把 Effective Modern C++ 里面关于智能指针和移动语义的章节啃一遍,再拿几个小例子在编译器里跑一遍。尤其是shared_ptr在多线程下的线程安全性问题——它保证引用计数是原子的,但不保证指向的对象是线程安全的。这种结论光背没有用,需要在代码里经历过一次才有直觉。
2.4 程序改错题的实战心得
G卷有时候会出现一小段有Bug的代码,让考生找出问题。这种题比选择题更逼近真实开发。常见的Bug来源包括:
new和delete不配对,new[]用了delete;- 结构体里有指针成员,直接浅拷贝导致重复释放;
- 忘记检查
malloc或new的返回值; - 多线程共享变量没有加锁;
- 字符串操作忘记预留
'\0'的位置。
我的做题策略是先看内存再跑逻辑。凡是涉及指针、数组下标、字符串长度的代码,优先排查越界和释放问题;然后再看控制流有没有死循环。这个顺序在真实排Bug时同样适用。
3. 编程题最容易翻车的地方:边界、输入输出和代码习惯
编程题是最能拉开分差的部分。不过我觉得G卷的编程题难度并不算偏高,关键在于你能不能把一道题从头写到尾、跑出正确结果。很多人挂在不是算法不会,而是代码不规范、边界没处理、输入输出格式搞错。
3.1 从"字符串逆序输出"说起:基础题怎么拿满分
编程题里经常出现字符串逆序、字符统计、括号匹配这类基础操作。以字符串逆序为例,看起来简单,但踩过坑的人都知道,坑全在细节里:
#include <cstring> void reverse(char *s) { if (s == nullptr) return; size_t len = strlen(s); for (size_t i = 0; i < len / 2; i++) { std::swap(s[i], s[len - i - 1]); } }如果题目要求原地逆序,那主要考察的就是双指针交换;如果要求逆序后输出,那就要考虑是直接输出还是构造新字符串。有些题还会混入空格处理,比如"只反转单词顺序,不反转单词内部字符",这就从单纯的字符串操作升级成了先分割再拼接的组合逻辑。写题之前先确认题目到底要什么,能省下一大半调试时间。
3.2 边界条件决定AC率
我见过太多人算法思路完全正确,却因为没处理边界条件导致不能AC。这里列一下我在笔试和刷题中最常遇到的边界场景:
| 场景 | 典型问题 | 处理方式 |
|---|---|---|
| 空指针/空字符串 | 函数直接崩溃 | 开头判断nullptr或empty() |
| 数组只有一个元素 | 循环少算或多算 | 用长度小于等于1的用例先测 |
| 元素全部相同 | 排序/去重逻辑错误 | 单独跑一遍等值输入 |
INT_MIN取绝对值 | 整数溢出变负数 | 用long long过渡 |
| 字符串包含空格 | 读取方式不对 | 用getline而不是cin |
| 容器迭代器删除 | 迭代器失效 | 正确接收返回值或延迟删除 |
这些边界在本地IDE里一测就能发现,但很多笔试平台不允许本地测试,所以只能靠经验提前想。我的习惯是写完主逻辑后,强制自己在脑子里过五种输入:空、单元素、双元素、正常值、极端大值。
3.3 C++刷题时的输入输出习惯
笔试平台大多支持C和C++,我建议用C++的cin/cout,但在做题开始就加一行ios::sync_with_stdio(false); cin.tie(nullptr);避免不必要的性能损耗。遇到大量数据时,scanf/printf有时候更稳,但写起来容易出错,看个人习惯。我自己倾向于用getline读带空格的整行,用stringstream按分隔符拆分,这样能规避很多字符串输入问题。
另外,在编程题里合理使用STL是加分项。std::sort、std::reverse、std::map、std::unordered_map这些容器能够把注意力集中在算法核心上。不过使用容器时要清楚底层复杂度,比如map底层是红黑树,插入和查找是O(log n),unordered_map底层是哈希表,平均O(1)。如果面试环节被追问时间复杂度和空间复杂度,还是要能讲清楚。
3.4 一个手写题节点的模拟回放
以经典的"删除链表中的重复节点"为例,我通常这样规划:先问清楚是已排序链表还是未排序,已排序就一次遍历去重,未排序可以用哈希表记录出现过的值。递归写法最简洁,但要注意递归深度;迭代写法需要用哑节点处理头节点被删除的情况。最终代码我会尽量写成包含main函数和测试用例的完整程序,而不是只写核心函数——因为笔试平台虽然只看最终结果,但完整代码能让自己在调试时少犯错。
4. 网络、系统与Linux:深信服笔试的后半场
如果说C/C++语言题和算法题是基本功考试,那操作系统、网络和Linux相关的题就是区分度最高的部分。这一块做得好不好,往往取决于你大学期间有没有自己折腾过Linux环境、有没有写过网络程序。
4.1 为什么深信服G卷一定会有网络题
深信服的业务基因是安全和云计算,不管哪个产品线,日常工作中都离不开TCP/IP、HTTP、DNS、NAT这些基础协议。比如做终端安全产品,需要分析网络连接行为;做广域网优化,需要理解报文的转发路径;做防火墙类产品,更要懂状态检测和报文过滤原理。所以G卷出现网络题非常自然。
高频考点集中在:
- TCP三次握手和四次挥手过程,以及各状态转换;
- TIME_WAIT状态产生的原因和影响;
- TCP和UDP的差异,什么时候用TCP,什么时候用UDP;
- select、poll、epoll 的区别;
- socket编程的基本流程,服务端和客户端分别要调用哪些函数。
我在笔试时遇到过一个印象很深的题:一个TCP连接主动关闭后,主动关闭方会进入什么状态?答案是TIME_WAIT。接着会问为什么需要TIME_WAIT,核心原因有两个:一是确保最后的ACK能送达对方,如果丢失可以重发;二是让足够长的时间过去,避免旧连接上的延迟数据包干扰新连接。这两个原因在真实项目里排查连接问题时非常有用。
4.2 操作系统与并发模型
进程和线程的区别、死锁的四个必要条件、用户态和内核态的切换、虚拟内存和物理内存的映射关系,这几类题目在操作系统模块里的出现率最高。
我建议把这几个问题关联起来整理:进程是资源分配的基本单位,线程是CPU调度的基本单位;同一个进程内的线程共享地址空间,所以多线程编程要处理同步问题;同步手段包括互斥锁、读写锁、条件变量、原子操作;死锁的产生需要同时满足互斥、占有且等待、不可剥夺、循环等待四个条件,破除任何一个条件就能避免死锁。
复习时不要只背结论,要能举出实际场景。比如多线程环境下两个线程分别持有锁A和锁B,同时都在等待对方释放,这就形成循环等待,也就死锁了。这种例子在项目经历里非常常见,笔试简答题如果能结合一个实际场景展开,会比只罗列概念得分高很多。
4.3 Linux命令行与调试基本功
作为一个C/C++开发岗候选人,Linux基本功几乎是默认要求。G卷不一定直接考命令行参数,但简答题可能会让你描述排查思路,而面试环节一定会问。
我整理过一份高频命令清单,笔试面试前过一遍非常有用:
| 场景 | 常用命令 | 说明 |
|---|---|---|
| 查看进程 | ps -ef、top | 看进程PID、CPU和内存占用 |
| 查看端口 | netstat -tunlp、ss -tunlp | 看某个端口被哪个进程占用 |
| 查看文件句柄 | lsof -p PID | 排查句柄泄漏问题 |
| 内存检查 | free -g | 系统可用内存 |
| 磁盘检查 | df -h、du -sh * | 定位磁盘空间占用 |
| 调试程序 | gdb、strace | 查看崩溃调用栈和系统调用 |
其中调试能力是我特别想强调的。现代Linux环境下编译C/C++程序,至少要学会用gcc -g生成调试信息,再用gdb执行bt查看调用栈、break设置断点、next单步跳过、print打印变量。很多线上问题,尤其是段错误和死锁,用gdb或strace能快速定位,面试时能说出这类排查经历,会明显增加"这人真的在做开发"的可信度。
4.4 简答题怎么答得让面试官觉得"这人真写过代码"
简答题最忌讳只写教科书定义。比如题目问"什么是内存泄漏",如果你只写"分配的内存没有释放",那基本拿不到高分。更好的回答是:说明泄漏发生的原因,比如new之后没有对应的delete,或者shared_ptr循环引用导致引用计数永远不为零;说明常见的排查手段,比如用valgrind、AddressSanitizer、查看top里内存持续增长;再举一个实际项目中遇到的例子。这种回答方式在任何技术面试里都通用,本质上是把你放在"解决问题的工程师"位置上去表达。
5. 从G卷到面试:错题复盘与技术深挖
笔试结束不代表备考结束。G卷最大的价值不在于那张分数,而在于它暴露出来的知识盲区。如果你属于"会复盘的人",那后续面试环节的收获会非常大。
5.1 把错题变成专项练习清单
笔试出来之后,我会第一时间把不确定和写错的题记录下来,按模块归类:
| 知识模块 | 错因分析 | 复盘行动 |
|---|---|---|
| 指针与内存 | 数组退化指针理解不牢 | 重写5道指针相关选择题 |
| C++11特性 | 移动语义不熟 | 手写一个简易unique_ptr |
| TCP协议 | TIME_WAIT原理不清楚 | 用 Wireshark 抓包验证 |
| Linux命令 | 不熟悉ss和lsof | 在服务器上逐个实操 |
| 算法边界 | 没考虑空串 | 整理一道题目的边界清单 |
这种复盘方式比单纯刷题高效得多,因为它是围绕你真实的失分点展开的。我也建议把这些内容做成自己的错题卡片,面试前快速过一遍,比临时抱佛脚刷题库要靠谱。
5.2 从笔试题延伸出的面试追问
面试官往往会把笔试题当引子继续深挖。比如笔试考了vector的扩容机制,面试可能接着问:vector扩容为什么选择2倍或1.5倍而不是固定增加大小?底层涉及分配器、移动构造和异常安全。又比如笔试考了shared_ptr,面试可能追问:shared_ptr线程安全吗?如果多个线程同时访问同一个shared_ptr对象,引用计数是安全的,但对象本身的操作需要外部加锁。
我建议你在准备G卷时,把每一道题往深处问三层:"为什么是这个问题""底层原理是什么""如果换一个场景会怎么变"。这样做以后,面试遇到类似问题基本不会卡壳。
5.3 简历项目要和笔试题呼应
笔试考察的是基础,面试考察的是综合能力,项目经历就是展示综合能力的窗口。C/C++方向的简历项目,不一定非要多炫酷,但一定要能体现你对语言和系统的理解。比较合适的项目类型有:
- 一个基于epoll的高并发网络模型,能讲清楚线程池怎么设计、连接怎么管理;
- 一个简单的内存池,能比较它和malloc的性能差异;
- 一个日志系统,能说清多线程写入时如何避免锁竞争;
- 一个文件监控工具,涉及
inotify或 Windows 文件系统 API。
写项目时不要只堆功能,要能讲清楚你遇到了什么问题、通过什么方案解决。比如日志系统在多线程并发写同一文件时,为什么用双缓冲区比直接加锁更好。这比简历上写"熟悉C++"有说服力得多。
5.4 面试追问清单
结合G卷常见的考点,我整理了一份低成本的面试准备清单:
map和unordered_map的底层实现与适用场景;shared_ptr、unique_ptr、weak_ptr的区别;select、poll、epoll各有什么优缺点;- 进程和线程的上下文切换开销差异;
- 什么是内存对齐,为什么要对齐;
- 回调函数和函数指针在C++里的使用场景;
#include头文件中的#pragma once和#ifndef宏定义有什么区别;- C++多态的实现原理,虚函数表是什么。
这些问题看似零散,其实是把笔试考点转化成了工程知识。花时间把这些搞懂,收获的不仅是一场笔试的通过率,更是后续工作里排查问题的基本功。
个人经验上说,我当年G卷前面选择题做得还算顺利,但到了网络部分的简答题,因为对TIME_WAIT的理解不够深入,白白丢了不少分。后来我把所有错题整理成自己的错题卡,每天抽十分钟过一遍,最后面试时遇到相关追问反而答得特别顺。所以如果你现在准备时间还来得及,千万别只刷算法题,语言的坑、系统的原理、网络的细节,每一分都值得认真对待。