☰
浩鲸科技2019C++笔试题全解析:考点拆解与备考策略
2026/10/26 7:03:55 网站建设 项目流程

前几天整理硬盘,翻出一个旧文件夹,里面躺着当年校招季收藏的一份浩鲸科技2019年C++笔试题回忆版。那时候大家管这套题叫“南京校招试金石”,后来我自己也在备考过程中把这里面的考点反反复复吃透了,受益很大。今天索性把这份题拆开揉碎了讲一遍——不仅告诉你答案,更想讲清楚每道题背后的出题意图,以及你刷到类似的题时该怎么应对。

浩鲸科技的前身是中兴软创,2018年底完成更名后,阿里巴巴和中兴通讯成为重要股东,公司主营电信行业数字化、政企数字化转型这条线。2019年那届校招,C++岗位笔试题整体走的是“基础扎实 + 原理深度 + 少量算法实战”的路子,难度中等偏上,但区分度很高。换句话说,这套题不是靠背诵能过的,它考察的是你大学四年到底有没有真正写过、调过、思考过C++。无论你现在是准备春招还是秋招,这份复盘都值得花半小时读完。

1. 先弄清楚:浩鲸科技和这套题到底什么来头

1.1 公司背景:为什么2019年这套题值得翻出来看

浩鲸科技在电信BSS/OSS领域深耕了很多年,产品线覆盖运营商业务支撑、政企数字化、智慧城市等场景。这类业务的特点是:系统大、并发高、数据量大,且往往运行在传统的Unix/Linux服务器上,对C++开发者的底层功底要求很高。

所以你看它的笔试题,几乎不考花哨的框架或新特性,反而特别偏爱C++的基础语法、内存模型、STL底层实现、操作系统原理、TCP/IP网络协议这五类内容。2019年那套题尤其典型,整张卷子约80%的分数集中在“你能否讲清楚C++程序在机器上究竟是怎么跑起来的”这个维度上。对于目标岗位是C++服务端开发的同学来说,这套题放到今天依然有很强的参考价值,甚至可以说,它的考点就是整个校招C++笔试题的“公约数”。

1.2 题型结构:一眼看穿整套卷子的出题套路

我当时把回忆版题目整理成了一个表,方便对照分析。整体结构大致是:

题型题量分值占比核心考点难度
选择题20题30%语法细节、sizeof/strlen、指针、继承低
填空题10题15%输出结果类、内存布局、类对象模型中
简答题4题20%虚函数机制、进程线程区别、TCP三次握手中高
编程题3题35%字符串处理、链表操作、排序与查找中高

从分值分布能明显看出,编程题是拿分大头,但选择题和填空题才是“阴阳怪气”的重灾区——你觉得自己会,一对答案才发现错了一半。很多同学备考时只刷LeetCode,忽略了对基础概念的精确记忆,结果在选择题上失分严重。这套题真正想筛选的,是那些“既能写代码,又懂底层原理”的候选人,而不是纯刷题机器。

2. 基础语法题:最容易拿分也最容易丢分的地方

2.1 经典陷阱题:sizeof、strlen、strcpy 三件套

2019年这套题的选择题第一题就给了很多同学一个下马威。题目大概是:定义char str[] = "hello";,问sizeof(str)和strlen(str)分别是多少。

这道题看着简单,但错的人相当多。sizeof是编译期运算符,它计算的是变量本身所占用的内存大小,因此包含字符串末尾的'\0',结果是6;而strlen是运行时函数,遇到'\0'就停止计数,所以结果是5。一个看“盒子多大”,一个看“里面装了几个有效字符”,这两个概念经常被混为一谈。

如果再换一个玩法,把char str[]改成char* p = str;,那么sizeof(p)在32位系统上是4、在64位系统上是8,它就成了指针本身的大小,和字符串内容毫无关系。这个点就是出题人最爱设置的陷阱:你以为在考字符串,实际上在考指针和数组的区别。

还有一道填空题,手写strcpy的实现,要求写出函数体。标准的实现是:

char* strcpy(char* dest, const char* src) { if (dest == nullptr || src == nullptr) { return nullptr; } char* ret = dest; while ((*dest++ = *src++) != '\0') { // 空循环体,赋值即移动 } return ret; }

这里有两个得分点:第一,返回值必须是char*,这样才支持链式调用;第二,src必须用const char*修饰,因为源字符串不应该被修改。很多同学只写了一个简单的赋值循环,丢了返回值,直接扣掉一半分。另外提醒一句,实际工程中更推荐strncpy这类带长度限制的函数,笔试时如果题目没明确要求,你可以在答案末尾补一句“生产环境下建议使用带边界检查的版本”,这会让面试官觉得你有安全意识。

2.2 指针和引用的辨析:看似送分题,其实是分水岭

“指针和引用的区别”是C++笔试的常青树,浩鲸2019年简答题里也有一道。面试官想听到的答案,不只是一句“引用是别名,指针是地址”,而是下面这一整套逻辑:

  • 引用必须在定义时初始化,而且一旦绑定就不能再绑定到其他对象;指针可以先声明后赋值,也可以随时改变指向。
  • 引用不占用额外内存(逻辑上),指针变量本身占用一个机器字长的空间。注意我说的是“逻辑上”,因为某些编译器的实现可能会为引用分配底层存储,但语言层面不允许你取引用的地址来操作这个存储。
  • 引用没有多级引用,但有多级指针。int**是合法的,int&&是右值引用,不是“引用的引用”,两者完全不同。
  • 运算语义不同:sizeof(引用)得到的是被引用对象的大小,sizeof(指针)得到的是地址的大小;对引用取地址得到的是原对象的地址,对指针取地址得到的是指针变量本身的地址。

笔试答题时,建议按“初始化要求、绑定关系、内存占用、运算语义、应用场景”五个维度展开,分点作答。如果能再举一个“为什么函数参数建议用const引用传递”的例子,比如避免拷贝、保证不修改原对象,这道题基本就是满分水准。

2.3 static 和 const 的实际落地场景

这套题的选择题里还专门考了static的多种用法,属于典型的“一题多考点”。static在C++里一共有三副面孔:修饰局部变量、修饰全局变量/函数、修饰类成员。局部静态变量存放在静态存储区,只初始化一次,生命周期跨越整个程序运行期;文件作用域的静态变量和函数限制了外部链接性,等价于“只在本文件可见”;类的静态成员属于整个类而不是某个对象,必须在类外单独定义。

出题人经常会这样组合:一个函数内定义静态局部变量,连续调用三次,问输出结果。只要你记得“静态局部变量的初始化只发生一次,之后每次调用都沿用上次的值”,这类题就很好解决。const的考点也类似,难度会上升到顶层指针和底层指针的区分:const char* p指的是“不能通过p修改指向的内容”,而char* const p指的是“p本身不能指向别的地址”。一个管内容,一个管指向,背口诀不如亲手写两行代码验证来得实在。

3. 面向对象与内存管理:笔试中的“重灾区”

3.1 构造/析构顺序:从一道输出题看懂派生类的生命周期

2019年填空题里有一道非常经典的程序输出题,大意是定义基类Base和派生类Derived,各自在构造函数和析构函数中打印一行标记,然后创建派生类对象,要求写出输出顺序。

正确答案是:先基类构造函数,再派生类构造函数;析构时顺序完全相反,先派生类析构函数,再基类析构函数。原因很简单:派生类对象在内存中可以被理解成“基类子对象 + 派生类新增成员”的组合,构造时必须从依赖的底层开始搭建,析构时自然先从当前层拆起。如果基类对象还没就绪,派生类的初始化逻辑就没有依托。

这个知识点本身不难,但出题人喜欢加戏:在类中再嵌入一个成员对象,考“基类、成员对象、派生类”三者的构造顺序。这时候规则是:先基类,再成员对象(按声明顺序),最后派生类构造函数体。注意,成员对象的构造顺序只和声明顺序有关,和初始化列表中的书写顺序无关。很多同学在初始化列表里调整了顺序,以为能控制实际构造顺序,结果答错。原理很简单:类成员的内存布局在编译期已经确定,编译器会按声明顺序依次构造,初始化列表只是给每个构造函数传参的入口。

3.2 虚函数与多态:为什么析构函数必须声明为虚函数

简答题里有一道高频题:基类析构函数为什么通常要声明为virtual?标准答案是:当通过基类指针删除一个派生类对象时,如果析构函数不是虚函数,则只调用基类的析构函数,派生类的资源就无法释放,从而造成内存泄漏。

虚析构函数背后依赖的是虚函数表机制。每个含虚函数的类会生成一张虚函数表,对象内存起始位置保存一个虚表指针(vptr)。当用基类指针调用虚函数时,程序会通过虚表指针找到实际的函数地址,实现“运行时多态”。你看,这其实就是“为什么C++能实现多态”的完整答案链:虚函数、虚表指针、动态绑定,这三个词必须连在一起回答,缺一不可。

另外,纯虚函数的考点也值得注意:含有纯虚函数的类是抽象类,不能实例化;派生类必须实现所有纯虚函数才能成为具体类。题目如果问“一个类能否同时拥有纯虚函数和普通成员函数”,答案是可以,这并不冲突。这种辨析题考的是你对面向对象设计原则的理解,而不只是语法。

3.3 智能指针:从裸指针到RAII,答出“为什么”才不会被刷

虽然智能指针是C++11才正式进入标准库的东西,但浩鲸这套题里已经有所涉及,足以说明出题人非常关注现代C++开发习惯。std::unique_ptr独占所有权,不能拷贝,只能移动,适合明确所有权归属的场景;std::shared_ptr共享所有权,内部用引用计数维护生命周期,适合多所有者场景;std::weak_ptr用来打破shared_ptr的循环引用,不增加引用计数。

笔试答题时,不能只背区别,要能说清底层机制。比如shared_ptr的引用计数是线程安全的,但它保护的“被管理对象”本身并不线程安全——这个点很关键,也是面试官最爱追问的细节。再比如循环引用的问题,经典例子是两个类互相持有shared_ptr成员,导致引用计数永远无法降为0,资源泄漏;解决方式就是把其中一个成员改成weak_ptr。

结合RAII(资源获取即初始化),你可以补一句:智能指针不是简单的垃圾回收,而是把堆资源的生命周期绑定到栈对象的生命周期上,栈上对象作用域结束自动析构,堆资源随之释放。这种“用栈管理堆”的思想,C++里几乎所有资源管理类都在用,理解了它,你看很多库代码都会豁然开朗。

4. STL、操作系统和网络:笔试里的“隐形差距”

4.1 vector 扩容:一道送分题里隐藏的底层机制

编程题之外,选择题里有一道很多人“靠感觉”答的题:往std::vector里不断push_back时,它内部是怎么扩容的?答案是:当容量不够时,分配一块新的更大内存(常见策略是容量翻倍),把旧元素拷贝或移动到新内存,再释放旧内存。于是原来的迭代器、指针和引用全部失效。

2019年题目还加问了一步:在vector扩容时应使用“拷贝”还是“移动”?如果你答“移动”,面试官会追问一句“移动构造函数可能抛出异常吗?如果抛异常了怎么办?”这就是传说中的“强异常安全保证”问题。标准库的做法是,如果类型的移动构造函数声明为noexcept,则用移动;否则退化为拷贝,因为拷贝失败时旧内存中的数据还完好无损,可以回滚。

更进一步的考点是vector的翻倍因子。GCC的实现通常按2倍扩容,而某些版本的MSVC使用1.5倍,因为内存分配器配合不同倍率会导致不同的内存碎片化特征。这种题不需要死记数字,但你要明白扩容的本质是“空间换时间”的权衡:扩容次数越少,均摊复杂度越低。

4.2 map 与 unordered_map:选型背后的哈希和红黑树

这套题的简答题里出现了“map和unordered_map的区别”,本质上是在考你“红黑树”和“哈希表”两种数据结构的选型逻辑。std::map底层是红黑树,元素按key自动排序,插入、删除、查找的时间复杂度都是O(log n);std::unordered_map底层是哈希表,查找均摊O(1),但元素无序,需要提供哈希函数。

面试官想要的答案,不仅是你知道它们“哪个快”,而是你能结合场景说出适用性:当需要范围查询、顺序遍历,或者对稳定性要求高时,选map;当只做单点查找、关注极致性能时,选unordered_map。另外,自定义类型作为key时,map只需要重载operator<,而unordered_map需要自定义哈希函数和相等判断函数,后者实现更繁琐,容易埋坑。答题时如果能补一句“哈希表的最坏情况退化到O(n),红黑树则能保证上界”,会显得你理解更深入。

4.3 进程、线程与死锁:操作系统四道经典问答题

操作系统考点里,“进程和线程的区别”几乎年年出现。标准的展开方式是:进程是资源分配的基本单位,线程是CPU调度的基本单位;同一进程内的线程共享地址空间、文件描述符等资源,而进程之间地址空间相互隔离;线程切换的开销远小于进程切换。出了这套题的公司很多都是做高并发服务端的,所以深层考察点是“为什么在C++服务端编程中要用线程池而不是频繁创建线程”,答案涉及内核态切换成本、缓存局部性、并发度控制等因素。

死锁的四个必要条件也值得认真记:互斥、持有并等待、不可剥夺、循环等待。回答时不仅要默写这四个词,还要能给出实际例子,比如两个线程分别持有锁A和锁B,同时又互相等待对方手里的锁。解决办法一般围绕“破坏四个条件之一”展开:例如用锁的顺序一致性来消除循环等待。这部分内容很考验平时的积累,建议多结合具体代码去理解,而不是只背概念。

4.4 TCP 三次握手与 TIME_WAIT:面试官最喜欢追着问的点

网络题里,TCP三次握手是高频考点。2019年的简答题直接让考生描述三次握手过程,并追问“为什么是三次而不是两次”。很多人能画出流程,但解释不清“为什么”。这里的关键点是:三次握手能防止“历史重复连接请求”对服务端造成资源浪费。比如客户端发送了一个连接请求A,在网络中滞留,超时后重发请求B,服务端回应对B的确认;如果只有两次握手,A之后再到达服务端,服务端也会建立连接,白白浪费资源。而三次握手时,客户端收到服务端对B的确认后会进入已建立状态,B对应的连接才真正生效;如果服务端收到A的确认请求,只要发回确认,客户端不会响应,服务端就能识别过期请求并撤销。

顺着这个知识点,面试官通常会追一个实际运维问题:服务端出现大量TIME_WAIT状态连接怎么办?原因常在于主动关闭连接的一方是服务端,而大量短连接快速建立、关闭,导致TIME_WAIT堆积。解决方向包括改用长连接、调整TIME_WAIT复用参数等。这个问题在C++后端岗位笔试中颇为常见,建议提前组织好答案。

5. 算法与编程题:完整实现与踩坑实录

5.1 字符串逆序输出的三种写法

浩鲸2019年的第一道编程题是“字符串逆序输出”,看起来极其简单,但想拿满分需要注意细节。我见过很多同学一上来就申请新的字符串数组倒着拷贝,其实更符合面试官期待的是原地逆序,不开额外空间,用双指针从两端往中间交换。

#include <cstring> void reverseString(char* s) { if (s == nullptr) return; int left = 0; int right = static_cast<int>(strlen(s)) - 1; while (left < right) { char tmp = s[left]; s[left] = s[right]; s[right] = tmp; ++left; --right; } }

注意几点:第一,函数入参必须判空,这是工程素养;第二,遍历次数是长度的一半,时间复杂度O(n),空间复杂度O(1);第三,如果题目要求输出逆序但不能修改原字符串,那就直接for (int i = len - 1; i >= 0; --i)输出即可,别画蛇添足去修改内存。这道题的高分答案往往不是算法多高级,而是边界处理干净、代码风格清晰、有基本防御性编程意识。

5.2 单链表反转:递归与非递归都能写才叫过关

第二道编程题是单链表反转。很多同学只准备了迭代写法,结果被要求“再用递归写一遍”时就卡壳了。两道都要掌握,因为这两者在面试中能分别展示出你对指针操作和递归栈的理解。

迭代写法核心是三个指针:pre、cur、next,每次把cur->next指向前驱,然后三人同时后移。

struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* pre = nullptr; ListNode* cur = head; while (cur != nullptr) { ListNode* next = cur->next; cur->next = pre; pre = cur; cur = next; } return pre; }

递归写法更简洁,但理解门槛更高。主要思路是:先反转后续的链表,然后把当前节点的后继节点的next指向当前节点。这里需要保存原头节点的next节点,否则反转后链表会断掉。

ListNode* reverseListRecursive(ListNode* head) { if (head == nullptr || head->next == nullptr) { return head; } ListNode* newHead = reverseListRecursive(head->next); head->next->next = head; head->next = nullptr; return newHead; }

递归写法的边界条件是head为空或只有一个节点,很多同学漏掉“只有一个节点”的情况,导致空指针解引用。如果你在笔试中时间紧张,建议先用迭代写一版保证通过,再在注释里补充递归版本思路,这样判卷人能看出你的思维完整度。

5.3 数组笔试题:双指针和排序边界

第三道编程题偏向数组操作,典型考法是“对一个整数数组进行非递减排序,去重并返回新长度”这类LeetCode风格题目。解法核心是双指针:一个指针遍历原数组,另一个指针维护结果数组的“最后一个不重复位置”。

#include <vector> int removeDuplicates(std::vector<int>& nums) { if (nums.empty()) return 0; int j = 0; for (int i = 1; i < static_cast<int>(nums.size()); ++i) { if (nums[i] != nums[j]) { ++j; nums[j] = nums[i]; } } return j + 1; }

这道题的坑有三处。第一,前提是数组已排序,如果没排序需要先排序;第二,循环边界建议用static_cast<int>(nums.size())避免无符号数和有符号数比较的告警,这是C++笔试中一道非常隐蔽的扣分点;第三,返回值是去重后的长度而非去重后的数组,有些同学最后忘记返回或者返回成下标,直接少拿一半分。这题的目的就是考察你“能在约束下写出简洁、不出错的代码”,而不追求奇技淫巧。

6. 备考阶段的真实经验与容易忽略的细节

6.1 环境准备:先把 C 盘和开发环境收拾利索

可能和题目没关系,但备考周期中你一定会遇到装备问题。很多人准备刷题时发现电脑 C 盘容量越来越少,编译环境各种出问题,光是 VS Code 配置 C/C++ 环境就折腾了一晚上。网上那些“C盘清理”“AppData文件夹太大”的热搜,其实说的就是程序员的日常。真诚建议:备考前花小半天把系统盘清理一下,删掉临时文件、把开发工具和项目放到非系统盘,VSCode 的 C/C++ 插件需要 MinGW 或 Visual Studio 工具链,路径里尽量别带空格。环境清清爽爽,你刷题的心情和效率都会好很多。

6.2 时间分配和刷题策略

如果你距离笔试还有一个月,我的建议是把时间分成三块:第一周专门过语法和内存模型,把《C++ Primer》里关于类、继承、虚函数、智能指针的章节认真读一遍;第二周集中刷操作系统的进程线程、死锁、内存管理真题,同时每天练两道基础算法题;第三周进入“模拟笔试”模式,每次给自己90分钟,按时完成一套模拟卷,重点训练做题节奏。

千万不要把时间全部花在刷LeetCode上,校招笔试不是算法竞赛,基础概念题占比非常高,性价比绝对不低。我当时认识一个朋友,刷了三百多道LeetCode,结果笔试时挂在“构造函数和析构函数调用顺序”这道填空题上,非常可惜。日常练习的时候,建议把每道题都当成面试题来对待,问自己“为什么这样写”“有没有更优解”,而不是对完答案就翻篇。

6.3 笔试心态与检查习惯

最后说几个容易在考场上翻车的细节,都是真题考场上实际发生过的情况。第一,C++笔试题的答题区域很小,不要写大段代码却忘记写注释,判卷人更想看到你的思路;第二,遇到“输出结果”类题目,先在草稿纸上手动推演一遍,不要直接看代码就写答案,尤其是虚函数覆盖和多层继承的输出题;第三,编程题的输入输出格式一定要按题目要求,不要多打印提示信息,很多在线笔试系统会因此判定答案错误。

做题顺序上,我个人的习惯是先做编程题再回头做选择填空。编程题分值大,而且需要清醒的头脑;选择题陷阱多,放在后面即使状态略差,也容易靠直觉拿分。如果时间还剩最后十分钟,优先检查指针有没有空指针风险、数组下标有没有越界、返回值类型和题目要求是否一致。这些细节往往就是你能不能拿到下一轮面试资格的区别。

我个人在实际备考和复盘这套题时,最深的感受是:C++笔试考的不是“你背了多少”,而是“你踩过多少坑,又总结出多少规律”。那些看似刁钻的填空题,背后都是编译器在各种复杂边界条件下的真实行为。备考期间不妨多在本地环境跑一跑小实验,把代码改了改、输出看了看,遇到不理解的就打断点调试一遍,这比死记硬背面试题集有效得多。希望这篇复盘能帮你在下一场C++校招笔试里少踩几个坑,多拿几分底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询