ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++校招笔试复盘:指针内存STL与多线程考点精讲

C++校招笔试复盘:指针内存STL与多线程考点精讲 2017年秋天我在图书馆刷到爱奇艺的校招笔试通知点进去做完那套C开发工程师笔试卷整整花了一晚上复盘。说起来这套卷子不算偏但也正因为不偏它特别能暴露基础功底的薄弱点。指针、内存、STL、算法、多线程基本把C方向校招笔试的高频考点都覆盖了一遍题目难度呈现明显的梯度前面还能靠记忆应付越往后越考验真功夫。我当时做完最大感受是这套卷子筛的不是谁刷题多而是谁对C这门语言有真正体系化的理解。今天我把这类卷子的出题逻辑、高频考点和答题策略拆开讲一遍给准备秋招的同学做个参考。1. 这套卷子的出题逻辑它到底想筛什么样的人1.1 笔试形式与C岗位的考察范围先说形式。这类大厂C方向的秋招笔试卷通常会分为三个部分选择题、简答题和编程题。选择题大概20到30道覆盖语言基础、操作系统、网络、数据结构简答题一般2到3道考设计思路或问题分析编程题有2到3道要求在线编码跑通测试用例。整套卷子时间给得不多有些公司甚至是90分钟到120分钟完成所有题目这就要求答题节奏非常紧凑。爱奇艺2017年这套卷子的整体风格是基础题量大、编程题有区分度。选择题里的C语法和内存题数量不少而且选项之间非常接近如果平时只是眼熟而没有真正理解很容易掉坑。编程题里既有可以直接套模板的经典题也有需要现场推理的变种题靠死记硬背过不了最后那几道。从岗位属性来说C开发工程师在互联网公司主要做的是底层基础设施、音视频处理、网络服务和高性能组件。爱奇艺这类视频平台对C的需求尤其集中在播放器内核、CDN调度、流媒体传输、转码服务这一条链路上。笔试考的东西其实就是在为这些业务方向筛选候选人。1.2 从爱奇艺的业务属性反推考点偏好视频平台的核心技术栈里C主要出现在三个方面一是流媒体服务端需要处理高并发连接和海量数据转发二是音视频处理涉及编解码、转码、画质增强对内存管理和性能极度敏感三是播放器内核和客户端组件要求代码在资源受限的终端设备上稳定运行。这三个方向对应的C技能要求其实很明确内存管理和对象生命周期要扎实这块直接决定线上服务稳定性多线程和并发控制要熟练因为流媒体服务本质就是并发系统STL和算法要熟海量数据的排序、查找、去重是日常工作网络编程和系统调用要懂这涉及传输协议和IO模型。所以这套卷子里出现的知识点并不是出题人随手抓的而是业务倒推考点的结果。你在复习的时候如果能站在这个角度理解题目就不容易觉得某些偏怪问题没有意义。比如虚函数考察的是你对对象内存布局的理解放到实际场景里就是基类指针能不能安全地操作派生类对象这在组件化架构中天天遇到。1.3 这套卷子的时间节奏与丢分重灾区我当时给自己模拟了真实考试的时间分配发现最耗时间的不是编程题而是前面的选择题和简答题。选择题里很多是下列哪个说法是错误的这类否定式提问每个选项都很像是对的需要逐个排查。一道这样的题就可能花掉3到5分钟20道基础题做完40分钟就没了。结合周围同学反馈这套卷子丢分比较多的位置通常有三处内存和指针相关的题目尤其涉及二维指针、指针数组、数组指针的辨析拷贝构造与移动语义的调用时机判断题目改一个参数传递方式答案就完全不同编程题里的边界条件处理测试用例隐藏了空指针、越界和溢出导致看似正确的代码只有部分用例通过。搞清楚这些丢分点后你会发现复习重心其实很明确。基础题靠体系化复习编程题靠高频题型训练两者缺一不可。下面我把每类考点的答题思路拆开细讲。2. 语言基础题拆解指针、内存和对象生命周期2.1 指针与引用的组合陷阱C笔试题里指针和引用是一个怎么考都不过时的主题。这套试卷里涉及指针的部分主要不是让你写指针运算而是给你一段代码让判断输出结果或指出错误。比如下面这类问题int a[5] {1, 2, 3, 4, 5}; int *p a; cout *(p) endl; cout *p endl; cout *p 1 endl;很多人会在这种题上翻车原因是把p和p的副作用时机搞混了。*(p)先把p指向的值取出来然后p后移一位所以输出1*p先把p前移一位再取值此时p指向a[2]输出3*p 1由于运算符优先级等价于(*p) 1输出4。这类题考的不是你记没记住优先级表而是你对表达式求值过程中指针状态如何变化有没有清晰认知。指针数组和数组指针也是一个经典区分点。int *p[3]表示一个数组数组元素是三个int*指针int (*p)[3]表示一个指针指向含有三个int元素的数组。在二维数组传参时才分得清这两种写法的差异。不少同学在简化代码时把int (*p)[3]错写成int *p[3]编译虽然能过但语义完全错了。2.2 构造、析构、拷贝与移动的调用时机C笔试里考察对象生命周期的方式很多最常见的是输出题让你数一个类被构造了几次、析构了几次、拷贝了几次。比如传值、传引用、返回临时对象的情况class Test { public: Test() { cout ctor endl; } Test(const Test t) { cout copy endl; } Test(Test t) { cout move endl; } ~Test() { cout dtor endl; } }; Test func() { Test t; return t; } int main() { Test a func(); }在C11之前这段代码可能会触发两次构造和多次拷贝在C11之后由于返回值优化和移动语义的存在实际输出可能简化。这里的关键点是编译器优化RVO/NRVO)并不是强制行为而移动构造的优先级高于拷贝构造当你有右值引用时return t会优先走移动而不是拷贝。当时这套卷子在移动语义上挖了一个很细的坑类里如果手动实现了析构函数或者拷贝构造函数编译器就不会自动生成移动构造函数。这意味着即使你的类满足移动条件也只能退回到拷贝性能差异在上千万元素容器里非常明显。这类题表面考语法实际考的是你知不知道C11规则中的隐式函数生成条件。2.3 虚函数与多态的实现机制虚函数是C笔试的必考内容考察方向一般有两种一种是让你描述虚表vtable的实现机制另一种是给一段多态代码让判断输出。描述虚表时关键是讲清楚三点每个包含虚函数的类都有一张虚函数表表中存放虚函数地址每个对象内部有一个虚表指针vptr指向所属类的虚表构造对象时vptr会被设置为指向当前正在构造的类的虚表所以在构造函数里调用虚函数不会触发多态。关于最后一点经常有人踩坑。因为父类构造过程中子类部分还没有初始化如果这时候调用虚函数走的是子类实现子类成员可能还没构造程序就会出问题。C选择了在构造期间把vptr指向当前类从机制上避免这个风险。笔试题里让你判断new Derived()之后构造函数中虚函数调用的输出答案就是父类版本。这套卷子还考过一个很容易错的知识点析构函数为什么要声明为虚函数。如果基类析构函数不是虚的通过基类指针删除派生类对象时只会调用基类析构函数派生类资源无法释放造成内存泄漏。代码层面对应的场景是所有组件类、接口类、抽象基类这类体系设计中的基类析构函数几乎都应该是虚的。3. 算法与编程题实战字符串、排序和数学计算的拿分策略3.1 字符串类题目从字符数组的转换说起C笔试题里字符串处理是编程题常客而且经常结合字符数组和std::string互相转换来考。很多同学对string用得很熟一旦碰到C风格字符串就懵。比如要求按指定分隔符把字符串拆成数组有人会用strtok但这个函数会修改原字符串并且不是线程安全的在多线程题目场景下容易被扣分。更好的做法是使用std::string的find和substr组合实现分词std::vectorstd::string split(const std::string s, char delim) { std::vectorstd::string result; std::string::size_type start 0; auto pos s.find(delim); while (pos ! std::string::npos) { result.push_back(s.substr(start, pos - start)); start pos 1; pos s.find(delim, start); } result.push_back(s.substr(start)); return result; }写这类代码时注意find的第二个参数表示从哪个位置开始搜索避免漏掉最后一个分隔符后面的内容。字符串转数字也是高频题std::stoi和std::to_string虽然方便但笔试题更爱考察手写转换逻辑因为你得自己处理符号位、溢出和非法字符。3.2 排序算法冒泡和选择之外的考察角度排序算法在笔试里的考法分为两种一种是直接让你实现某个排序算法另一种是考察算法特性和复杂度。选做题里经常出现冒泡排序和选择排序因为代码短、易于验证。但这里有个容易被忽略的知识点冒泡排序是稳定排序选择排序是不稳定排序。为什么冒泡排序只在相邻元素之间做交换相等元素的相对顺序不会改变选择排序在每一轮把一个元素放到最终位置如果当前轮发现一个更小值会和前面的元素交换这个交换可能跨过相等的元素导致相对顺序改变。看似只是八股文实际在排序对象的属性有序性要求上很关键比如按分数降序、分数相同按学号升序的场景。快速排序在笔试中出现频率也很高但多数不是让你写基本版而是考察如何优化。比如三数取中、小区间插入排序、尾递归优化。我见过一道题是要求手写快速排序的partition函数并保证把所有等于pivot的元素集中在中间这就是三路快排的思路。写三路快排的要点是维护lt、gt两个边界把小于pivot、等于pivot、大于pivot分成三个区域避免重复元素的性能退化。3.3 数学类题目快速幂与最小公倍数的边界处理数学类题目看着不起眼但往往是编程题里的送分题前提是你能快速写出无Bug的版本。快速幂是高频考点中的高频描述很简单计算a的b次方模mod。如果直接循环乘b次b到1e9量级就超时了所以要用二分思想long long quickPow(long long a, long long b, long long mod) { long long result 1; a % mod; while (b 0) { if (b 1) result result * a % mod; a a * a % mod; b 1; } return result; }写这个代码有三个容易出错的地方第一步要a % mod否则a可能溢出乘法时要用long long接收中间结果因为两个1e9量级的数相乘会超过int范围循环条件是while (b 0)而不是while (b)虽然效果一样但显式比较更清晰不容易让阅卷人误解。最小公倍数的题目也不少见核心公式是lcm(a, b) a / gcd(a, b) * b。注意这里要先除后乘否则a * b可能溢出。如果是多个整数求最小公倍数就两两迭代计算。笔试里常给几个较大的数比如6、8、12、15很多人能算对但如果换成包含质数的长列表并且在代码里要求处理就需要确保gcd函数在递归和迭代两种写法下都能正确工作尤其注意gcd中a或b为零的边界情况。3.4 进阶结构单调栈与链表操作的常见考法这套卷子或者同类互联网公司的笔试卷编程题里偶尔会拔高到单调栈这种进阶数据结构。单调栈的典型应用是寻找每个元素下一个更大或更小的元素位置比如每日温度类问题。核心理解是元素入栈时保持栈内单调性出栈时就可以确定某些答案。写单调栈的代码初学者最常犯的错误是混淆栈中存值和存下标的区别。大多数情况下应该存储下标因为最终要求的往往是位置距离或者需要根据下标去原数组取值。如果存值当数组里有重复元素时索引信息会丢失。链表操作中反转链表、合并有序链表、判断链表是否有环是三个标配题。反转链表迭代写法要维护三个指针顺序上容易错ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* curr head; while (curr ! nullptr) { ListNode* next curr-next; curr-next prev; prev curr; curr next; } return prev; }这道题的关键是在改变curr-next之前先把next保存下来否则链表断掉。很多面试官会追问如果链表有环会怎样、如果要求递归实现呢所以现场编码时要把边界情况一并考虑清楚。4. 多线程、设计模式与C新特性容易拖后腿的半基础题4.1 多线程同步从互斥锁到ABA问题多线程是互联网公司C笔试里绕不开的题因为它直接对应真实服务端的高并发场景。基础考察方向包括互斥锁、条件变量、读写锁、原子操作以及死锁的形成条件。一个经典考题是多个线程对同一个变量进行自增操作如何保证结果正确。最直接的回答是加互斥锁但要注意自增操作count不是原子的它包含读、加、写三步。用C11的std::atomicint可以解决但如果你在代码里用了两次原子操作做比较并交换就会引入ABA问题。ABA问题的场景是线程A读取到值X线程B把X改成Y又改回X线程A的CAS操作无法察觉中间的变动。这在无锁数据结构中特别危险比如无锁栈中可能因为ABA问题导致重复释放同一块内存。解决办法一般是引入版本号或者使用带有标签的指针每次修改带上递增的标签CAS时比较值的同时比较标签。笔试中能说出这层说明你真的理解并发场景下的内存安全问题。另外还有个细节容易被忽略std::mutex类型的变量不能拷贝所以包含互斥锁的类不能直接放进std::vector或使用默认拷贝构造。如果类里需要互斥锁又必须实现拷贝只能自定义拷贝逻辑这一般在笔试简答题里出现。4.2 设计模式的经典问法与答题思路C方向笔试涉及设计模式时最常考的是单例模式其次是观察者模式、工厂模式。单例模式问你如何实现线程安全的单例这道题有一个标准的进化路线懒汉式加锁在获取实例的成员函数里加锁但性能差双重检查锁先判断指针是否为空为空才加锁加锁后再判断一次注意防止指令重排序要加内存屏障C11之后的静态局部变量初始化编译器保证线程安全。class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() {} };这个写法的核心在static局部变量C11标准规定它的初始化是线程安全的所以最简洁高效的实现方式就是靠它。把构造函数设为私有并把拷贝构造删除防止外部创建实例。观察者模式的考察方式一般是用一句话说明模式中Subject和Observer的关系或者让你写一个简化版本。答题时不需要硬背UML图只要说清主题对象维护一个观察者列表状态变化时遍历列表通知观察者更新即可写代码时用std::vectorstd::functionvoid()来存放回调比定义抽象观察者基类更符合现代C的做法。4.3 C11之后的新特性constexpr、智能指针与回调C11是C发展史的分水岭笔试中涉及新特性的题目逐年增加。一个常见的送分题是constexpr是哪个C版本引入的。答案是C11引入C14放宽了函数内可以包含的逻辑C17又允许在if和switch中声明变量C20进一步支持constexpr虚函数和constexpr动态分配。这个问题不难但很多人会答错成C14或C17。智能指针也是重点。unique_ptr是独占所有权语义不可拷贝只能移动shared_ptr是共享所有权语义通过引用计数管理生命周期weak_ptr用于打破循环引用它不会增加引用计数。笔试里经常给一段代码问你shared_ptr管理的对象什么时候析构。关键是要能发现循环引用两个对象互相持有对方的shared_ptr引用计数永远不为零析构函数永远不会被调用。解决办法就是把其中一个方向的指针改为weak_ptr。回调函数的题目这几年越来越多主要问法有回调函数是什么、在C里怎么实现。从最传统C函数指针到std::functionstd::bind再到C11的lambda表达式三层递进。答题时写出lambda版本是最讨巧的因为代码简洁且捕获列表能控制捕获方式std::functionint(int, int) add [](int a, int b) { return a b; };如果深究还可以讲一下回调在异步IO、定时器、事件循环中的底层作用。理解到这个层面笔试的简答题分数基本稳了。4.4 现场答题的代码风格与踩坑预防编程题除了算法正确性代码风格也会影响整体评价。我当时总结出几条实用原则变量命名要有含义i、j、k可以用在循环里但最好不要出现tmp1、tmp2这种写完代码要自查边界条件空数组、单元素数组、全相同元素数组、指针为空的情况涉及数组下标的地方留意是不是会越界尤其是while循环里同时访问i和i1的情况能用常量引用做参数的就用常量引用避免无意义拷贝这既是性能优化也是代码习惯的体现。还有一个小技巧笔试环境没有本地IDE的自动纠错代码写完之后自己是没法编译运行的所以要靠人工编译自查。我会在脑中模拟一次执行过程用一个小例子从头走一遍比如排序算法用一个5元素数组模拟基本能发现绝大多数低级错误。5. 复盘与延伸这套卷子留给今天的备考建议5.1 笔试后的复盘方法不管是这套爱奇艺的卷子还是其他公司的笔试卷考完以后最重要的事是复盘不是看分数。复盘第一步是把每道题按会做但错了蒙对的完全不会三个标签分类。第二步是回来查每一个错题背后的知识点找到知识盲区后补一轮系统学习而不是只看这道题的解析。我当时会把所有错题对应的知识点整理成一份清单比如指针数组和数组指针辨析移动构造的生成条件单调栈应用场景。每收集一个新知识点就往清单里加到秋招结束时这份清单已经覆盖了上百个考点。笔试前重看一遍这份清单比刷一大堆新题更有针对性。5.2 从笔试到技术面试的知识衔接笔试通过后紧接着是技术面试很多在笔试里考的知识点会在面试中以追问形式出现。比如笔试考了单例模式面试就会追问静态局部变量初始化为什么线程安全或者如果我需要提前释放单例对象怎么办。因此笔试复盘的内容其实也是技术面试的复习素材你需要从知道怎么回事升级到能讲清楚原理。编程题也一样笔试里写了快速幂面试时可能会让你口头分析复杂度并说明为什么取模要分布在整个计算过程中。准备面试的时候把笔试中遇到的每个知识点都往深处想一想基本就能覆盖大部分问题。5.3 几条具体的刷题和复习建议结合我自己的经验给准备秋招C方向的同学几条实操建议语言基础要系统过一遍推荐找一本讲C原理的书完整读下来做笔记、写示例代码不要只看博客碎片算法题按专题刷数组、字符串、链表、树、动态规划、数学计算各找20到30道代码手写到熟练为止多线程、设计模式这类半基础题一定要动手写demo光看概念记不住比如自己实现一个线程安全的单例再用两个线程并发调用测试每次笔试完都要把错题整理到个人题库里标记好错误原因和正确思路。如果时间有限优先保证基础和常见算法题的正确率这两项撑起了笔试的大部分分数。我之前见过不少同学在偏题怪题上花了很多时间结果基础选择题错一片编程题也没写完非常可惜。把握住自己能稳稳拿分的部分再逐步提升难度这才是校招笔试最务实的策略。
返回列表