“浩鲸科技2019校招C类笔试题”这个标题,我在几个校招群里见过不止一次。那年秋天,很多同学把它当成“阿里系”里最跳一跳能够到的目标——浩鲸科技背靠阿里云和中兴通讯的基因,做的是电信运营商核心的BSS/OSS系统,这类公司笔试筛人的逻辑和纯互联网大厂不太一样,它更看重你用C/C++去思考底层问题的能力。
这篇文章不打算给你押题,因为任何一家公司的笔试每年都在变,背题是性价比最低的备考方式。我想做的是把这份笔试题背后的知识体系拆开,讲清楚“C类笔试到底考什么、为什么考、怎么准备最划算”,让你面对的不再是一张陌生的试卷,而是一套可以复用到同类企业(华为、中兴、海康、讯飞这些)的备考方法论。
1. 先搞清楚浩鲸是谁,才能看懂它为什么出这些题
1.1 从通信软件到数字化服务,双基因决定了考点
浩鲸科技的前身是中兴软创,2018年前后完成重组,成为阿里巴巴和中兴通讯共同滋养的数字化服务厂商。注意这个“双重基因”:一方面继承中兴做电信设备与运营商服务时的严谨工程文化,另一方面又向阿里靠拢,学互联网的迭代速度和海量并发处理思路。这两股力量叠加,决定了它的校招笔试既不会像纯互联网大厂那样只考偏门算法,也不会像传统硬件厂商那样追问寄存器细节,而是卡在“用C/C++做大规模系统软件”这个位置上。
它最核心的业务是电信运营商的BSS/OSS——计费、客户关系管理、业务开通、网络资源调度这些后台系统。这类系统的特点是数据量以亿计、并发峰值极高、代码生命周期长达十数年,所以笔试要筛的人必须真正理解内存、理解对象生命周期、理解系统调用背后的代价。很多同学拿到卷子觉得“怎么考得这么基础”,其实恰恰是这些基础决定了你能不能在生产环境里活过三年。
1.2 “C类”到底指什么
我当年备考时也纠结过这个问题。“C类”是C语言类别吗?结合浩鲸的岗位发布习惯来看,这里通常指C/C++方向的技术笔试,与JAVA类、前端类、测试类并列为不同类型的考卷。也就是说,“C”是编程语言方向标签,而不是试卷难度等级。
从题型上看,这份笔试试卷大体由三部分组成:语言基础(C/C++语法、内存模型、面向对象)、数据结构与算法(手写代码题)、计算机基础常识(操作系统、网络、软件工程)。下面我按性价比和踩坑程度,把这三块的备考思路一个个拆开讲。
2. C/C++语言基础:笔试里分值密度最高的部分
2.1 指针、引用和const,每次笔试的老朋友
C/C++类笔试卷子里,指针和引用几乎是必考,而且通常不是单独问概念,而是通过代码片段让你判断输出、找错误、改bug。
先说指针和引用的核心区别。引用是变量的别名,声明时必须初始化,一旦绑定就不能再换绑;指针保存的是地址,可以为空,也可以随时指向其他对象。笔试里经典的考法是写一个swap函数,让你判断传值、传引用、传指针三种写法哪个能真正交换:传值不行,传指针和传引用都行,但传指针要处理空指针,传引用更直接。
更值得警惕的是const与指针结合的位置问题。const int* p表示p指向的内容不可修改,但p本身可以改;int* const p表示p本身不能改,但它指向的内容可以改;const int* const p则两者都不能改。这类题几乎每年都能看到,只要画一张指针和所指向内存的图,彻底理清“哪个是只读”,就不会再丢分。
还有两个高频陷阱。第一个是sizeof:对数组名取sizeof得到的是整个数组的字节数,但当数组作为函数参数传递时,会退化为指针,此时sizeof得到的是指针大小,通常是8字节(64位系统)。第二个是字符串字面量:char* str = "hello"和char str[] = "hello"截然不同,前者指向只读常量区,修改它会导致未定义行为,后者是栈上的可写副本。笔试里那些让程序崩溃的题目,很多就是这个原因。
2.2 对象生命周期:析构、拷贝和移动的精髓
语言基础部分如果只复习一块,我建议优先攻对象的拷贝控制。浩鲸这类做大型系统的公司特别看重这一点,因为系统里的资源泄漏和崩溃,十有八九都出在错误的对象生命周期管理上。
最经典的考题是手写一个String类,实现构造函数、析构函数、拷贝构造函数和拷贝赋值运算符。这四件套能综合考察深拷贝与浅拷贝、资源释放、自赋值处理和异常安全。
class String { public: String(const char* str = nullptr); String(const String& other); String& operator=(const String& other); ~String(); private: char* m_data; }; String::String(const char* str) { if (str == nullptr) { m_data = new char[1]; *m_data = '\0'; } else { m_data = new char[strlen(str) + 1]; strcpy(m_data, str); } } String::String(const String& other) { m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); } String& String::operator=(const String& other) { if (this == &other) { return *this; } delete[] m_data; m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); return *this; } String::~String() { delete[] m_data; }注意几个细节:默认析构函数不会帮你释放堆内存;默认拷贝构造函数是浅拷贝,两个对象会指向同一块内存,析构时double free;赋值运算符必须先判断自赋值,否则先delete再拷贝会崩。如果你还能补一句“用拷贝交换或右值引用实现移动语义更优雅”,那这题的印象分基本拿满。
C++11之后,移动构造函数和std::move也开始出现在笔试里。核心是理解“把析构成本很高的资源转移出去,而不是复制一份”。很多公司会追问unique_ptr和shared_ptr的区别,以及哪些场景不能用shared_ptr(比如循环引用要配合weak_ptr)。这些都不会超纲,但如果你只看语法不写代码,很可能在“为什么这样做”上卡壳。
2.3 static、volatile和内存布局,细节里的魔鬼
这类题目讲究“一眼识别陷阱”。static是出镜率极高的关键字:static局部变量放在全局数据区,生命周期到程序结束,但作用域仍在函数内;static全局变量和static函数都有内部链接属性,只在当前编译单元可见;类的static成员属于所有对象共享。
volatile则常和编译器优化一起考,告诉编译器该变量的值可能在当前线程之外被改变,不要优化掉对它访问。笔试可能会给一个看似无限循环的程序,问为什么用volatile修饰后行为不同。
内存布局同样值得花时间。经典问题是进程地址空间的分区:代码段、未初始化数据段(BSS)、已初始化数据段、堆和栈。堆从低地址向上增长,栈从高地址向下增长,这两个方向常被考到。结构体对齐和大小计算更是老熟人,记住“每个成员按自身对齐值对齐,整体按最大对齐值补齐”的基本规则后,多数题都能算。
再加上new/delete和malloc/free的区别(前者会调用构造/析构函数,后者只分配/释放内存;new的底层是malloc;配对使用别混搭),这几块组合起来,就是语言基础里最常见的失分点。我是建议准备一张大表,把“指针与数组”“const的多种位置”“栈与堆的区别”“static的四种作用”列清楚,考前过一遍,比看十篇面经都有用。
3. 数据结构与算法:绝大多数人的分数分水岭
3.1 高频数据结构与算法,其实就那几类
很多同学一听到算法就怕,觉得要刷几百道题才敢上场。但校招笔试和ACM竞赛是两回事,它考的是工程实用向的算法,频率极高的题型其实非常集中。
| 类型 | 典型题目方向 | 优先级 |
|---|---|---|
| 链表 | 反转、找环、合并有序链表、删除节点 | 极高 |
| 二叉树 | 前/中/后序遍历、层次遍历、最大深度、最近公共祖先 | 极高 |
| 字符串 | 最长无重复子串、回文子串、字符串转整数、KMP | 高 |
| 排序/查找 | 快排、归并、堆排、二分查找边界 | 高 |
| 动态规划 | 背包、最长递增子序列、编辑距离 | 中高 |
| 栈/队列 | 单调栈、用栈实现队列、最小栈 | 中 |
| 哈希表 | 两数之和、重复元素判断、缓存设计 | 中 |
链表和二叉树是手写代码题的重灾区,因为它们的遍历、递归和指针操作最能体现基本功。复习时不要只看思路,每一类都要手写一遍,尤其是非递归的二叉树遍历和链表反转,很多同学到考场上一紧张,递归的写法都会卡。
排序算法里快排的partition写法和归并排序的merge写法要练到肌肉记忆。二分查找更是要小心边界:left和right的更新条件、while里是<还是<=、mid是向上还是向下取整,这些细节在笔试里都是分。动态规划不需要钻太深,但“最长公共子序列”“最长递增子序列”这类经典题最好能写出转移方程和边界初始化。
3.2 手写代码题怎么被评分:三个隐藏维度
不少人以为手写代码题只要跑通测试用例就给满分,这是个误解。笔试的代码题虽然不像面试手撕那样全程围观,但评分时通常有三个维度。
第一是正确性,包含边界条件处理。比如输入空指针、空数组、只有一个元素的情况,很多考生在正常用例上写得不错,边界一上来就崩。一个链表的反转函数,如果不处理空链表或只有一个节点的链表,实际运行就会出问题。
第二是复杂度。题目如果明确要求O(n),你却写了O(n^2),哪怕测试用例过了,也会被扣分。这就要求你写代码前先想清楚时间复杂度和空间复杂度,并在注释里写出来。很多公司会看这一点。
第三是代码风格。命名是否符合规范、逻辑是否清晰、有没有写注释、有没有明显的冗余,这些在代码评审里都算分。面试官看笔试代码几乎等同于看新人的提交代码,一个缩进混乱、常量不提取、变量名是a/b/c的答案,即使正确也很难拿高分。
3.3 一个典型的编程题思路示例
拿一道非常经典、同时也非常高频的题目来说步骤:“给定一个字符串,找出其中不含重复字符的最长子串的长度。”
拿到题先别动手。第一步确认输入和输出,第二步想清楚算法。最直观的暴力法是O(n^2),不推荐。正确做法是滑动窗口:一个left指针指向窗口左边界,一个right指针向右扩展,用哈希表记录每个字符最近出现的位置。如果发现当前字符已经在窗口中出现过,就把left跳到上一个位置+1,时刻更新最大长度。
#include <string> #include <vector> #include <algorithm> using namespace std; int lengthOfLongestSubstring(string s) { vector<int> lastPos(128, -1); int left = 0, maxLen = 0; for (int right = 0; right < s.size(); ++right) { char c = s[right]; if (lastPos[c] >= left) { left = lastPos[c] + 1; } lastPos[c] = right; maxLen = max(maxLen, right - left + 1); } return maxLen; }这段代码里我刻意加了向量索引方式而不是用map,原因是字符范围固定时数组更快,这也是面试官可能追问的点。你可以发现,手写代码题的重点不在于你能背多少模板,而在于你能不能把一个场景问题翻译成清晰的数据结构和稳定的循环不变式。
4. 操作系统、网络与工程常识,用碎片时间补性价比最高
4.1 进程、线程与死锁:选择题的常客
计算机基础这部分通常以选择题和简答题出现,并不要求你背操作系统课本,但高频概念必须能说得清楚。
进程和线程的区别是必考,关键是那句话:“进程是资源分配的基本单位,线程是CPU调度的基本单位。”进程间通信方式要能列举出管道、消息队列、共享内存、信号量、信号、socket,最好能说清各自适用场景,比如共享内存最快但要自己处理同步,管道适合父子进程之间通信。线程同步的互斥锁、条件变量、信号量、读写锁也是常客,特别是“互斥锁和信号量在实际使用中怎么选”这种工程向追问。
死锁几乎是每次笔试都会出现的概念题。四个必要条件(互斥、持有并等待、不可剥夺、循环等待)要能一字不差写出来,并知道破坏任何一个条件就能预防死锁。如果题目给一个资源分配图让你判断是否死锁,核心就是找循环等待链。
虚拟内存部分要理解页式管理、页表、缺页中断、页面置换算法(LRU、FIFO、Clock)。笔试里考得最多的是LRU,甚至会让你手写一个LRU缓存的get和put操作,这题结合了哈希表和双向链表,值得提前练一下。用户态和内核态的切换、系统调用的代价,则时常作为“为什么频繁系统调用会慢”的引子出现。
4.2 网络协议只需要掌握到这层
网络部分的常考点集中在TCP/UDP和HTTP。TCP三次握手和四次挥手是必须的,要能画出来,更要能解释“为什么握手是三次,不是两次或者四次”——本质上是为了防止失效的连接请求突然到达服务端,导致服务端打开无效连接。如果题目继续追问TIME_WAIT状态,要能说出它的两个作用:保证最后的ACK能可靠到达对方,以及让旧连接的所有报文在网络中完全消失。
TCP的可靠传输机制里,确认应答、超时重传、滑动窗口、拥塞控制(慢开始、拥塞避免、快速重传、快速恢复)是高频考点。笔试很少让你写拥塞窗口的完整变化,但给一个场景判断cwnd上升或下降,经常会出现。
HTTP部分要能区分HTTP和HTTPS,知道HTTPS的TLS握手大致流程,以及常见的状态码:200、301/302、403、404、500、502、503。对做C/C++后台的同学来说,还可能问到如何解析HTTP报文、Https通信如何建立、TCP粘包和拆包怎么处理。这块建议配合网络编程一起练,既补笔试知识点,也为后续面试做准备。
4.3 软件工程题的“标准答案”思维
这部分分数不高,但很好拿,因为考来考去就是面向对象原则、设计模式和工程规范。
面向对象的基本特征是多态、继承、封装,笔试里常给一段继承代码问你输出顺序。比如“基类构造、派生类构造、基类析构、派生类析构”的调用顺序,以及虚析构函数存在的必要性(通过基类指针delete派生类对象时,如果析构函数不是virtual,则派生类析构函数不会被调用,导致资源泄漏)。多态的实现原理——虚表和虚指针——也常考,理解“每个包含虚函数的类有一个虚表,对象里存储虚表指针”基本够了。
设计模式考得不多,但单例模式几乎是必背的。要会写线程安全的双重检查锁定版本,并解释为什么用双重检查(同时避免每次加锁的性能损耗和指令重排问题)。另外工厂模式、观察者模式、策略模式也常以“让你说出使用场景”的方式出现。
工程规范类的考点比较零散,但有一年我印象很深的是简答题里让描述git的常用命令和协作流程。这说明浩鲸这类公司很看重新人能不能直接融入团队开发,git add、commit、push、merge、rebase这些命令最好提前过一遍,知道什么时候该用rebase什么时候该用merge,算是基础工程素养。
5. 实战做题节奏:笔试现场最核心的博弈
5.1 发卷后前10分钟,先把全局扫描一遍
笔试的时间普遍是90分钟到120分钟,题量却不小。我见过很多同学拿到卷子就从头开始做,结果在前面一道难题上耗了半小时,后面三道会做的题没时间写。这是一个非常亏的策略。
正确的做法是发卷后花3到5分钟把所有题目浏览一遍,给每道题打一个“难度标签”:一眼会做的、需要想一想但有思路的、完全不会的。然后按“先易后难、先拿分后攻坚”的顺序做题。选择题里有些概念题,30秒就能出答案,属于白送的分数,务必优先拿;代码题里第一道往往比第二道简单,先做简单的练手,再冲难的。
我当年备考时养成了一个习惯:定时模拟。每次都按考试倒计时来做一份真题或模拟卷,做完只复盘两件事——每道题的实际耗时,和哪类题最容易卡壳。这样真的上考场时,时间感会好很多,不会在不知不觉中陷进一道题。
5.2 编程题的时间预算与完成顺序
编程题通常是2到3道,总分占比最大。按我的经验,合理的做法是给每道编程题定一个硬性时间预算,最多不超过25到30分钟,超出就立刻切换到下一道。
写编程题时先不要着急敲键盘。我推荐的顺序是:先用注释或伪代码把整体思路写出来,这既能让你的思路清晰,万一时间不足,评分者也能从注释里看到你的解题方向;然后写关键的数据结构定义和主函数逻辑;最后才补细节。一个很实用的技巧是,先处理边界情况再写主体逻辑——空输入、只有一个元素、最大值、最小值,先把这些if分支写出来,再填中间的正常流程。
完成代码后如果还有时间,一定要在脑中或草稿纸上跑一遍测试用例,尤其跑一个正常用例加一个边界用例。很多编译不过或者运行崩溃的问题,在这个阶段就能被发现。笔试环境往往没有调试器,所以提前在代码周围写清楚“输入是什么、输出是什么、复杂度多少”,既是给自己留后路,也是给阅卷人留好印象。
5.3 如何给自己“留后路”:写注释、拆子函数
笔试代码是在有限时间内写的,不可能像工程项目一样完美,但有几个习惯能显著提高得分率。
第一,把复杂逻辑拆成小函数。比如一个链表题里需要反转链表,单独抽一个reverseList函数,既降低主函数复杂度,也让阅卷人一眼看出你的模块化意识。第二,关键步骤写中文注释。第三种,不依赖IDE的自动补全,命名尽量用完整单词,比如maxLength而不是ml,因为阅卷人会看代码风格,简洁清晰的代码更容易拿高分。
我也见过不少同学把时间花在把代码压缩得很短上,为了少写几个字符让变量名变成单字母,这在笔试中得不偿失。你要展示的是“能写出可维护代码”的潜力,而不是代码高尔夫。
6. 过来人的坑:我的分数曾在哪些地方打折扣
6.1 眼高手低,只看题不写代码
这是最普遍的一个坑。很多同学刷题时习惯看题解,看一眼觉得“哦,原来是滑动窗口”,然后划走,以为自己会了。到了笔试现场,却在变量边界、循环条件上反复卡壳。
我的建议是,备考期间尽量用纯文本编辑器或白纸,关掉IDE的自动补全,手写代码。写完再对照标准答案,重点看边界条件的处理和你漏掉的情况。链表题、二叉树题尤其适合在纸上画图推演,一个指针的指向画错了,代码自然就错了。你在纸上越熟练,考场上的心态越稳。
6.2 死磕一道题等于放弃整张卷
笔试现场和面试最大的不同是,面试官会引导你,笔试不会。一旦你在一道题上耗太久,整张卷子的节奏就可能崩掉。
我见过一个真实的例子,有同学在试卷的选择题环节遇到一道结构体对齐的题,为了算清楚占用大小,在那里推了七八分钟,最后编程题只写了一个函数签名。这不是能力问题,是策略问题。一道题分值再高也是有上限的,把时间留给后面两三道能做对的题,总体得分显然更高。如果你发现自己在一道题上超过15分钟还没有进展,立刻标记一下,先做后面的,回过头来再看往往会有新思路。
6.3 编码习惯的隐性扣分,别不信
最后一个想提醒的是编码习惯。笔试的编程题在交付后,绝大多数公司会人工或半自动地查看代码,一个命名规范、结构清晰、注释恰当的答案,即使存在小瑕疵,评分也往往优于一个功能正确但代码一团糟的答案。
我自己在模拟复盘中就吃过亏:写出的代码能跑,但没有检查指针是否释放、没有处理空串、没有考虑入参合法性。后来我给自己列了一个“交卷前清单”:变量是否初始化、动态内存是否释放、main函数是否返回0、有没有多余的debug输出、是否有注释说明思路。每次交卷前按清单过一遍,能挽回不少因为粗心丢掉的分数。
浩鲸19年那批笔试题的信息我已经记得不太全了,但C/C++方向校招笔试考核的底层逻辑,这些年几乎没有变过。如果你也正在准备一场类似的技术笔试,先从语言基础的地基打起,把数据结构与算法的高频题练熟,再补一点系统和网络常识,最后用一场零失误的做题节奏去考场里检验自己。这样做,你去哪家做C/C++业务的公司都不会慌。