news 2026/8/30 4:49:08

搜狗C++笔试题深度解析:从内存多态到并发新特性

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
搜狗C++笔试题深度解析:从内存多态到并发新特性

每年到了校招季,总有人问我搜狗2016年那套C++工程师笔试题到底考了什么。这套题在圈子里流传得挺广,很多人拿着它当练手材料,也有人把它当成衡量自己C++水平的标尺。坦白说,这套题放在今天来看也不落伍,它没有堆砌冷门语法,而是把一个C++工程师日常开发中最容易翻车、也最能体现功底的知识点挨个筛了一遍:指针和内存、多态和虚函数、STL容器选型、手写算法、并发同步,外加几道新标准特性的考察。如果你正准备C++方向的技术面试,或者想自查一下基础是否扎实,这期内容值得仔细看完。

1. 这份笔试题的整体风格:不考偏题,专考“基本功的坑”

1.1 为什么搜狗这类公司偏爱用C++考底层功底

搜狗的核心业务是搜索、输入法、浏览器,这些产品对性能的要求极高,后端服务要处理海量请求,客户端要保证流畅输入体验。在这种场景下,C++的优势在于可以直接管理内存、精准控制资源、做底层性能优化。笔试面试官想找的不是会背API的人,而是真正理解内存布局、对象生命周期、并发模型的人。

记得我当年做这套题时的直观感受:大部分题看起来都“见过”,比如指针和引用的区别、new和malloc的区别、构造函数里能不能调虚函数,但每道题往下深挖一层,就能筛掉一大批只是背了面经的候选人。面试官真正想看到的,是你对“为什么”有清晰的解释,而不是只会说结论。

1.2 整张卷子的知识点分布与考察逻辑

从流传出来的题目和面试者的复盘来看,这套题大致覆盖五个方向:

  • C++基础语法:指针/引用、const、内存布局、类型转换
  • 面向对象机制:虚函数、多态、构造函数与析构函数的调用顺序
  • 标准库与算法:容器选型、迭代器失效、排序/查找类手写算法
  • 并发与多线程:互斥锁、条件变量、原子操作、回调陷阱
  • 新标准特性:C++11引入的关键字、智能指针、移动语义

我发现它有一个很聪明的出题策略:把“背答案就能答对”的题和“理解了才能答对”的题混在一起。比如“指针和引用的区别”属于前者,而“构造函数中调用虚函数会发生什么”属于后者。后者往往配一个小代码段,让你判断输出结果,不少人在这种题上栽跟头。

2. 语言基础题:指针、引用、const、内存,全是基本功

2.1 指针与引用:一个关于“绑定”的经典送分题

先看这道高频题:

#include <iostream> using namespace std; int main() { int a = 10; int b = 20; int* p = &a; int& r = a; p = &b; // 指针指向改变 r = b; // 这里发生了什么? cout << a << " " << b << endl; cout << *p << " " << r << endl; return 0; }

很多新人以为r = b是让引用 r 改绑到 b,这是最大的误解。引用在初始化时就绑定了 a,之后所有的赋值操作都是给 a 赋值。所以输出结果是20 2020 20吗?不对,仔细算:r = b等价于a = b,a 变成 20,b 还是 20,*p 现在是 b 的值 20,r 是 a 的值 20。所以四行输出全是 20。

面试官的考察点在这里:指针是“保存地址的变量”,可以重新赋值指向其他对象;引用是“对象的别名”,一旦绑定不能改绑。另一个常考区别是sizeofsizeof(p)在64位系统上是8字节,sizeof(r)sizeof(int),也就是4字节。还有一点,引用必须初始化,不能存在空引用;指针可以为空,但也正因如此,使用指针前必须判空。

2.2 const的“多重身份”:变量、指针、成员函数

const 是C++笔试的常青树。最经典的一道题就是让你说出下面几个声明的区别:

const int* p1; // 指向常量的指针:不能通过p1修改指向的值 int const* p2; // 与p1相同,const修饰的是 *p int* const p3; // 指针常量:不能修改p3指向的地址 const int* const p4; // 既不能改地址,也不能改值

记忆方法一直很好用:从右往左读,遇到*就拆开。const int* p中,先读*pconst int,说明*p不能被修改;int* const p中,先读pconst指针,说明 p 本身不能被修改。

还有一道常见的题目,考察 const 成员函数:

class A { public: void f() const { /* 这里能修改成员变量吗? */ } };

在 const 成员函数里,this 的类型是const A*,意味着不能修改普通成员变量。如果想在 const 函数里修改某个成员变量,必须把该变量声明为mutable。面试时如果要答得更有深度,可以补充一句:const 成员函数的本质是给 this 指针加了底层 const,编译器把它当作重载的一部分,所以 const 版本和非 const 版本可以同时存在。

2.3 new/malloc与内存管理:堆和栈的爱恨情仇

搜狗这套题里有一道老生常谈:new 和 malloc 的区别。标准答案是:

  • new/delete 是操作符,malloc/free 是函数
  • new 会调用构造函数,malloc 只分配原始内存
  • new 返回类型安全的指针,malloc 返回 void* 需要强转
  • new 失败抛出 bad_alloc,malloc 失败返回 NULL
  • new 可以重载,malloc 不行

但光背这些不够。面试官会接着问一个更实际的问题:“如果你用 malloc 分配了一块内存,然后在这块内存上构造对象,会有什么问题?”这就是 placement new 的考点。正确的做法是:

void* mem = malloc(sizeof(MyClass)); MyClass* obj = new (mem) MyClass(); // 使用完之后 obj->~MyClass(); free(mem);

这个考点出现在笔试题里,通常不是为了让你写这段代码,而是考察你是否理解“内存分配”和“对象构造”是两件事。理解了这一点,你对内存泄漏、对象生命周期、RAII 都会有更深的认识。内存泄漏的隐蔽场景往往是异常路径,比如函数中途抛出异常,后续的 delete 没执行,处理方式是使用智能指针或 RAII 封装。

3. 面向对象与多态:虚函数表里的门道

3.1 虚函数到底是怎么实现多态的

C++笔试题里,虚函数相关的题目几乎是必考的。常见的一道概念题是:有虚函数的类,实例化后的对象大小是多少?

class Base { public: virtual void func() {} private: int x; };

在64位系统上,答案是16字节:4字节的 int 加上4字节对齐填充,再加上一个8字节的虚函数表指针。如果去掉 virtual,大小是4字节。从这里可以看出,只要类里有虚函数,编译器就会悄悄给对象插入一个 vptr(虚函数表指针),指向该类的虚函数表 vtable。

虚函数表是类级别的,每个类一张表,表里存放的是函数指针。对象通过 vptr 找到自己的虚函数表,再通过偏移量调用对应的函数。继承时,派生类会重写虚函数表中对应槽位,指向自己的函数实现,从而实现在运行时根据实际类型调用不同的函数。

有个容易答错的问题:虚函数表是编译期生成的还是运行期生成的?答案是编译期生成。vptr 在构造对象时被初始化,但表的生成完全发生在编译期。概念上运行期只是通过 vptr 找到表并调用函数。

3.2 构造函数、析构函数里的虚函数陷阱

这套题里出现过一道非常经典的输出题,考的是构造函数中调用虚函数:

class Base { public: Base() { show(); } virtual void show() { cout << "Base"; } }; class Derived : public Base { public: Derived() { show(); } virtual void show() { cout << "Derived"; } }; int main() { Derived d; return 0; }

输出结果是BaseDerived,不是很多人以为的DerivedDerived。原因在于,当执行 Base 的构造函数时,对象的动态类型还是 Base,vptr 指向 Base 的虚函数表;只有进入 Derived 构造函数体时,vptr 才被更新为 Derived 的虚函数表。所以构造函数里调用虚函数不会发生多态,这是C++的一个设计原则:在构造和析构期间,对象的类型就是当前正在构造/析构的类。

同样的道理也适用于析构函数。所以如果一个类的析构函数不是 virtual,通过基类指针 delete 派生类对象时,行为是未定义的,可能只调用基类析构函数,导致派生类资源泄漏。这就是为什么“带虚函数的类应该把析构函数写成虚的”。

3.3 高频设计模式题:从单例到观察者

搜狗笔试题里还有一类题,不算纯语言题,但非常考察工程能力,就是设计模式。最常见的是让手写一个单例模式。经典写法:

class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; };

这个写法的好处是,C++11 标准保证了局部静态变量初始化的线程安全性。如果你在回答时能主动提到:在 C++11 之前,这种写法存在线程安全问题,需要加锁;C++11 之后编译器会生成保护代码,保证只初始化一次,这题基本就拿下了。

观察者模式、工厂模式在笔试题里更多以扩展题形式出现,比如“写出一个简单工厂模式”或“观察者模式中如何避免悬挂指针”。后者是C++特有的坑:观察者生命周期比主题对象短,主题通知观察者时可能访问到已销毁的对象,解决方案是使用弱引用或从观察者注册表中安全移除。

4. STL与算法:容器选型与手写算法

4.1 容器对比:vector、list、map、unordered_map怎么选

搜狗的笔试里有一道简答题:请说明 vector 和 list 的区别,以及 map 和 unordered_map 的适用场景。这种题只要逻辑清晰,很容易拿分,关键是答出底层原理。

vector 底层是连续内存,随机访问 O(1),但在中间插入删除需要搬移元素,O(n);list 底层是双向链表,任意位置插入删除 O(1),但随机访问 O(n),每个节点有额外的指针开销。迭代器方面,vector 的插入可能导致迭代器失效,因为可能触发扩容重新分配内存;list 的插入不影响其他迭代器,但删除当前节点后当前迭代器失效。

map 基于红黑树,内部有序,查找 O(log n);unordered_map 基于哈希表,查找平均 O(1),但内存开销更大,且不保证有序。面试官经常追问:什么时候一定要用 map?答案是当你需要有序遍历键值对的时候,或者需要查找前驱后继时,红黑树天然支持这些操作。如果只做单点查询且无顺序需求,优先 unordered_map。

4.2 手写排序算法:快速排序、快速幂与相关基础

笔试里手写排序算法也很常见,快速排序出现的概率最高。标准的递归版快排:

void quickSort(vector<int>& arr, int left, int right) { if (left >= right) return; int i = left, j = right, pivot = arr[left + (right - left) / 2]; while (i <= j) { while (arr[i] < pivot) i++; while (arr[j] > pivot) j--; if (i <= j) { swap(arr[i], arr[j]); i++; j--; } } quickSort(arr, left, j); quickSort(arr, i, right); }

写快排时常犯的错误有三个:递归终止条件写错导致栈溢出;pivot 选择不当导致最坏 O(n^2);交换后的边界调整不对导致死循环。我在模拟面试时见过不少候选人背下了代码,但一追问“为什么 i<=j 而不是用 i<j”就卡壳了。原因是 i<=j 能保证区间划分正确,避免左右区间交叉时遗漏元素。

如果你备的是搜狗这套题,还要注意快速幂。虽然不是排序,但经常出现在算法小题里:

long long fastPow(long long base, long long exp, long long mod) { long long result = 1; base %= mod; while (exp > 0) { if (exp & 1) result = result * base % mod; base = base * base % mod; exp >>= 1; } return result; }

快速幂的原理是把指数拆成二进制,利用乘法的结合律减少乘法次数。很多人在循环里忘记取模,导致溢出,这点要特别注意。

冒泡、选择这类简单排序也偶尔出现,主要是考察写代码的基本功和是否了解稳定性概念。冒泡排序是稳定排序,选择排序是不稳定排序,因为选择排序可能把相同元素的相对顺序打乱。

4.3 字符串、数组和C++11的初始化语法

笔试题里还有一组基础题,专门考字符串数组初始化和 C 风格字符串与 std::string 的转换。看到热搜词里也有“c++字符串数组初始化”、“c++字符串转数组”,说明这确实是高频的失分点。

一个典型的题目:

char str1[] = "hello"; char str2[] = {'h', 'e', 'l', 'l', 'o'}; cout << sizeof(str1) << " " << sizeof(str2) << endl;

答案是 6 和 5。str1 是字符串字面量初始化,末尾自动补一个 '\0';str2 是字符数组的显式初始化,没有 '\0'。如果你用 strlen(str2) 去计算长度,会读到栈上往后的任意字节,直到遇到 0,这就有未定义行为。正确做法是用 sizeof 或显式加上 '\0'。

std::string 转字符数组的场景也常考。用str.c_str()拿到const char*,注意返回的指针在 string 被修改或销毁后就失效了。要想安全拷贝,用std::copymemcpy

std::string s = "hello"; std::vector<char> buf(s.begin(), s.end()); buf.push_back('\0');

C++11 以后,数组和容器的初始化也经常考,比如int arr[] = {1, 2, 3};array<int, 3> arr{1, 2, 3};vector<int> v{1, 2, 3};。注意vector<int> v(10, 1);表示10个1,而vector<int> v{10, 1};表示一个initializer_list,里面只有两个元素:10和1。这种“括号匹配”细节就是面试官最爱挖的坑。

5. 并发编程:线程同步与ABA问题

5.1 线程同步三板斧:mutex、条件变量、atomic

搜狗的服务端并发场景多,笔试题中对并发的考察从来不含糊。一道典型题是:说明互斥锁、条件变量、原子变量的使用场景和区别。

互斥锁用于保护临界区,确保同一时刻只有一个线程访问共享资源。条件变量用来解决“等待条件满足”的问题,必须配合互斥锁使用。在 C++ 里,标准写法是:

std::unique_lock<std::mutex> lk(mtx); cv.wait(lk, []() { return ready; });

注意这里必须用unique_lock而不是lock_guard,因为 wait 内部需要暂时释放锁,等其他线程 notify 后再重新获取锁,unique_lock才支持这种操作。

原子变量用于保护简单的共享变量,比如计数器、标志位。它的底层利用硬件提供的原子指令,性能远高于锁。但原子变量不能保护复杂的临界区,如果多个操作之间需要保持一致性,仍然需要锁或事务内存。

这里特别要提醒:volatile 不能用于线程同步。volatile 的作用是告诉编译器不要对这个变量的访问做优化,每次都要从内存读取,但它不保证操作的原子性,也不能阻止CPU乱序。很多人把它和atomic搞混,这是面试官非常喜欢的陷阱。我见过不止一个候选人在“为什么不能用volatile保证线程安全”这题上翻车。

5.2 不说ABA问题的无锁编程都是耍流氓

ABA问题几乎是无锁编程必考的知识点,热搜词里也有“aba问题c++”,说明关注它的人非常多。我先说背景:无锁数据结构通常用 CAS(Compare And Swap)操作更新共享变量。CAS 的语义是:只有当当前值等于预期值时,才把值更新为新值,否则失败。

ABA问题的场景是这样的:

  1. 线程1 读取共享变量,当前值为 A
  2. 线程1 被挂起
  3. 线程2 将值从 A 改成 B,再从 B 改回 A
  4. 线程1 恢复执行,CAS 发现当前值仍然是 A,于是成功更新为 C

问题在于,值虽然是 A,但期间已经发生了两次修改。如果数据结构基于指针构建,A 指向的节点可能已经被释放并重建,CAS 成功更新后会导致悬空指针或数据错误。

解决思路通常有两种:一是使用带版本号的原子变量,比如std::atomic<std::shared_ptr<T>>boost::atomic提供的 tagged 指针,每次修改版本号加1,CAS 时同时比较值和版本号;二是避免立即释放内存,采用延迟回收机制,比如使用hazard pointer

笔试题里一般不会让你完整实现,而是要求说清楚 ABA 问题的成因和解决办法。能画图说明、举出具体场景的候选人,在面试官眼中是真正写过无锁代码的。我在实际项目中确实踩过一次 ABA 的坑,用自以为聪明的无锁设计实现了有锁中简单的功能,结果线上出现偶发数据错乱,排查耗时很久。后来老老实实改用带版本号的 CAS,问题消除。

5.3 回调函数与线程安全

看到热搜词里有“c++回调函数例子”,这也是搜狗这类笔试的常客。回调函数本质是把函数指针或可调用对象作为参数传递,在另一个上下文里执行。C++里有三种常见写法:

  • 函数指针:void (*callback)(int)
  • std::functionstd::function<void(int)> callback
  • 函数对象(仿函数):重载operator()

使用回调时的一个重要考点是线程安全:如果回调在另一个线程中被执行,那么回调里访问的共享数据必须加锁,否则会出现数据竞争。另一个考点是回调中调用虚函数是否会发生多态,答案是会的,因为调用回调本质上是普通函数调用,对象是完整的,与构造函数里的特殊阶段无关。

还有一个隐藏很深的坑:回调函数内部如果持有某个对象的原始指针,而对象已经析构,调用回调就会崩溃。安全的做法是改用std::weak_ptrshared_ptr捕获,在回调开始时先lock()判断对象是否存活。

6. 新标准特性与综合实战

6.1 C++11/14/17 必考新特性速查

这套题里有一道概念题:constexpr 是哪个版本的C++引入的?答案是 C++11。C++11 是C++历史上变化最大的一次标准更新,笔试题的覆盖面非常大。

常考的 C++11 特性包括:

  • auto:自动类型推导
  • nullptr:替代 NULL 的类型安全空指针
  • 右值引用与移动语义:&&std::move
  • std::unique_ptrstd::shared_ptrstd::weak_ptr
  • lambda 表达式
  • constexpr:编译期求值
  • static_assert:编译期断言
  • std::thread和原子库

C++14 放宽了constexpr函数内可以包含的语句限制,允许在constexpr函数中使用局部变量、循环和分支。C++17 主要引入了std::optionalstd::variant、结构化绑定、if constexpr等。

有一个常被追问的点:constexprconst的区别。const 表示“在程序运行期间不会修改”,但它的值可能直到运行时才确定;constexpr表示“可以编译期求值”,它必须满足更严格的条件。如果面试官问“constexpr 一定比 const 高效吗”,正确的回答是不一定,constexpr的主要价值是让某些计算在编译期完成,减少运行时开销,但前提是编译器真的会在编译期求值,否则它只是强制了可编译期求值的性质。

另外,关于 C++ 的运行库,经常有人搞混。如果你在 Windows 上写 C++,会看到 "Microsoft Visual C++ Redistributable" 这个程序,它提供运行时组件,很多 C++ 编译出来的程序依赖它,但这不是语言标准的一部分。这类问题偶尔出现在招聘答疑帖里,不属于笔试题范畴,就不展开了。

6.2 综合实战题:手写线程安全单例并分析

如果把这套题的所有知识点串起来,最好的载体就是“手写一个线程安全的单例,并分析内存序”。这道题可以用来同时考察面向对象、并发和新标准。

我第一次在面试官面前写单例,写的是最经典的 DCLP(Double-Checked Locking Pattern)版本:

class Singleton { public: static Singleton* getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(mutex_); if (instance == nullptr) { instance = new Singleton(); } } return instance; } private: Singleton() {} static Singleton* instance; static std::mutex mutex_; };

如果面试官不再追问,这版本看着还行。但只要追问一句“这个版本在C++11之前有什么问题”,就知道考点在哪里了:不加volatile时,编译器可能对指令重排,导致instance先被赋值,而构造函数还没执行完,另一个线程就拿到了一个半成品对象的指针。C++11 之后,用std::atomic<Singleton*>配合acquire/release内存序可以修正,或者干脆用上面提到的static局部变量版本,因为它的初始化已经由标准保证为线程安全。

遇到这种追问时,能主动说出“最推荐的是 Meyers Singleton,C++11 以后它简洁又线程安全”的候选人,往往能赢得面试官的认可。这题背后考察的其实是你在真实工程里对线程并发、指令重排、对象生命周期的理解,而不是会不会背代码。

6.3 从笔试题到工程能力:这些考点为什么重要

整理完这套题,你可能会觉得它有点“学院派”:谁会真的在日常代码里手写快排?谁会去考虑虚函数表的内存布局?但换个角度看,这些知识点恰好构成了一条完整的底层认知链。指针和引用让你理解变量和内存的关系;虚函数表让你理解运行时多态的代价;内存管理让你理解智能指针为什么存在;并发同步让你理解为什么有锁和无锁需要权衡;新标准特性让你知道如何写出更现代、更安全的代码。

我自己的体会是,笔试只是第一关,真正的分水岭在面试追问里。能完整讲出“为什么”的候选人,通常也在实际项目中踩过坑。比如你用过std::atomic,就会更理解 ABA;你被迭代器失效坑过,就会更清楚 vector 和 list 的适用边界。所以这套题最好的打开方式不是背答案,而是把每个考点当成一个引子,顺着它去查资料、写例子、看源码。等你能不看资料把每个点都讲透,再去面对笔试面试,底气会完全不一样。

最后分享一个我自己刷题时的习惯:每道题做完,强制自己写一段总结,说明“这题在真实开发中对应什么场景”。比如const成员函数对应接口设计时的只读约束,new/malloc 区别对应内存池或对象池的实现,constexpr对应高性能场景下的编译期优化。带着这种视角刷题,你不会觉得自己在看一堆零散语法,而是在补一张完整的知识拼图。搜狗这套笔试题能流传这么多年,恰恰说明它考察的正是C++工程师长期受用的核心能力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 4:46:16

开源餐饮小程序系统:从扫码点餐到外卖配送的完整解决方案

简介&#xff1a;这是一套面向餐饮行业开发者与中小商户的技术人员的开源扫码点餐外卖配送小程序系统源码&#xff0c;旨在提供从顾客扫码下单、商家接单管理到骑手配送调度的一站式轻量级解决方案。资源包共2000个文件&#xff0c;含957个PHP后端逻辑文件、148个JS前端交互脚本…

作者头像 李华
网站建设 2026/8/30 4:45:09

STM32N657 FSBL工程链接失败?HAL驱动源文件缺失的排查与解决

STM32CubeMX 生成的 STM32N657 FSBL 工程编译时链接失败&#xff0c;报错内容直指缺少 HAL 驱动源文件。这个问题我最近在评估 N6 系列平台时也遇到过&#xff0c;折腾了小半天才把根因和解决方案理清楚。别看报错就一行&#xff0c;背后的逻辑其实牵扯到 CubeMX 的工程生成机制…

作者头像 李华
网站建设 2026/8/30 4:44:52

Python学习日记11

数据库支持13.1 数据库概述13.1.1 为什么需要数据库当程序需要存储和管理大量结构化数据时&#xff0c;普通文件&#xff08;如 CSV、JSON&#xff09;存在明显局限&#xff1a;13.1.2 Python 数据库 API&#xff08;DB API&#xff09;Python 提供了统一的数据库接口规范 DB A…

作者头像 李华
网站建设 2026/8/30 4:43:43

AI+Obsidian搭建爆款案例库:从素材收集到灵感创作全流程

你有没有过这样的经历&#xff1a;看到一篇爆款文章&#xff0c;当时觉得“这个思路真好”&#xff0c;随手点了收藏&#xff0c;等到自己写内容的时候&#xff0c;却怎么也想不起来它好在哪、结构怎么搭、钩子怎么埋。手机里截图存了上百张&#xff0c;微信收藏里塞满了文章&a…

作者头像 李华
网站建设 2026/8/30 4:43:13

ISM330DHCX机器学习内核深度解析:从决策树原理到低功耗应用实践

我最近在评估ISM330DHCX这颗六轴惯性传感器时&#xff0c;发现很多资料都在强调精度高、功耗低&#xff0c;却很少把真正的杀手锏——机器学习内核&#xff08;MLC&#xff09;讲透。这颗芯片能自己完成从原始信号到分类结果的全部计算&#xff0c;而主机只需要去读一个寄存器&…

作者头像 李华
网站建设 2026/8/30 4:42:57

Referer 校验原理

在 Web 安全与资源管控体系中&#xff0c;Referer 校验是一种成本极低、落地简单的来源身份校验机制&#xff0c;广泛应用于静态资源防盗链、接口访问管控、CSRF 辅助防护等场景。它依托 HTTP 协议原生的请求头字段实现&#xff0c;无需额外开发复杂的认证体系&#xff0c;就能…

作者头像 李华