跳转到内容

C++ 面试

阅读本书无疑会为你的 C++ 职业生涯打开局面,但在开出高薪之前,雇主仍然会希望你先证明自己。不同公司的面试方法各不相同,不过技术面试的许多方面其实是可以预期的。一位严谨的面试官通常会考察你的基础编码能力、调试能力、设计与风格能力,以及解决问题的能力。你可能被问到的问题范围相当广。本附录将介绍你在面试中可能遇到的几类问题,以及如何更有策略地拿下那份你心仪的高薪 C++ 编程工作。

本附录将按本书各章的顺序展开,讨论每一章中最有可能在面试场景中出现的内容。每一节也会进一步说明可用于检验这些技能的题型,以及应对这些问题的最佳方式。

技术面试中通常会包含一些基础的 C++ 问题,用来筛掉那些只是因为“听说过这门语言”就把 C++ 写进简历的候选人。这类问题可能出现在 电话初筛 中,也就是开发人员或招聘人员在邀请你参加现场面试之前先打电话给你时提出的问题;也可能通过电子邮件或当面提问。回答这类问题时,要记住,面试官主要是在确认你是否真的学过并用过 C++。通常你不必把每个细节都答得丝毫不差,依然可以获得很高的评价。

  • 函数的使用
  • 统一初始化
  • 模块的基本用法
  • 标准命名模块 std 的使用(C++23)
  • 如何使用现代的 std::print()println() 函数将文本输出到屏幕(C++23)
  • 如何使用 std::cout 将文本输出到屏幕
  • 命名空间与嵌套命名空间的使用
  • 语言基础,例如循环语法(包括基于范围的 for 循环)、条件语句、条件运算符以及变量
  • 三路比较运算符的使用
  • 枚举
  • 栈与自由存储区之间的区别
  • const 的多种用法
  • 什么是指针与引用,以及它们之间的区别
  • 引用在声明时必须绑定到某个变量上,且这种绑定不能改变
  • 按引用传递相较于按值传递的优势
  • 结构化绑定
  • auto 关键字,以及它与结构化绑定配合使用、推导表达式类型或函数返回类型的方式
  • std::arrayvector 等标准库容器的基本使用
  • std::pairoptional 的使用
  • 类型别名和 typedef 的工作方式
  • 属性(attributes)背后的总体思路

基础 C++ 问题常常会以“术语解释”的形式出现。面试官可能会让你定义一些 C++ 术语,例如 autoenum class。她也许在寻找教科书式的答案,但如果你能给出示例用法或补充更多细节,往往还能额外加分。比如,当被要求解释 auto 关键字时,如果你不仅说明它可用于定义变量,还能进一步说明它在函数返回类型推导和结构化绑定中的用途,就会更出彩。

另一类考查基础 C++ 能力的问题,是让你当着面试官的面写一小段程序。面试官可能会先给你一个热身题,例如“用 C++ 写一个 Hello, World”。面对这种看似简单的问题时,一定要尽可能把能拿的加分项都拿到手:展示你对命名空间有概念,也熟悉最新标准;也就是说,用现代的 std::print()println() 函数,而不是 C 风格的 printf();并且知道只需导入一次 std 就能访问整个标准库。不过,用每一个新的 C++ 标准去升级代码库需要时间,因此很多公司并不总是采用最新的 C++ 标准。这意味着你仍然需要知道如何用旧标准完成同样的事情。对于 “Hello, World” 程序,你也应该展示自己能用 std::cout#include 来写,而不仅仅会用 std::println()import

让候选人解释 const 是一道经典的 C++ 面试题。这个关键字本身就像一个滑动标尺,方便面试官评估答案的深度。比如,一般水平的候选人会谈到 const 变量;较好的候选人会解释 const 成员函数、按引用传递 const,以及为什么这比按值传递更高效;优秀的候选人则可能会谈到 const 与线程安全之间的关系(见第 27 章“C++ 多线程编程”),说明如何定义 static const 数据成员(见第 9 章“精通类与对象”),或者区分 constconstexpr(见第 9 章)。

本章介绍的某些主题,也常常会以“找 bug”问题的形式出现。要特别留意对引用的误用。比如,设想有一个类把引用作为数据成员:

class Gwenyth
{
private:
int& m_caversham;
};
int main()
{
Gwenyth g;
}

main() 中的语句无法通过编译,编译器会报错,指出它试图引用一个已删除的函数。由于 m_caversham 是引用,它在类构造时必须绑定到某个变量上。编译器自动生成的默认构造函数做不到这一点,因此编译器会隐式删除 Gwenyth 的默认构造函数。你需要自己提供构造函数,并在构造函数初始化列表中初始化这个引用。这个类可以把要绑定的变量作为构造函数参数传入:

class Gwenyth
{
public:
explicit Gwenyth(int& i) : m_caversham { i } { }
private:
int& m_caversham;
};

第 2 章和第 21 章:字符串与字符串视图,以及字符串本地化与正则表达式

Section titled “第 2 章和第 21 章:字符串与字符串视图,以及字符串本地化与正则表达式”

字符串非常重要,几乎所有类型的应用程序都会用到。面试官很可能至少会问一个与 C++ 字符串处理相关的问题。

  • std::stringstring_view
  • 函数返回类型应优先使用 const string&string,而不是 string_view
  • C++ 的 std::string 类与 C 风格(char*)字符串之间的差异,以及为什么应避免使用 C 风格字符串
  • 字符串与整数、浮点数等数值类型之间的相互转换
  • 使用 std::format() 进行字符串格式化
  • 使用 std::print()println() 打印字符串(C++23)
  • 如何一次性格式化并打印整个范围(C++23)
  • 原始字符串字面量
  • 本地化的重要性
  • Unicode 背后的思想
  • locale 和 facet 的高层概念
  • 什么是正则表达式

面试官可能会让你解释如何把两个字符串拼接起来。通过这个问题,面试官想看出你是以 C++ 程序员的方式思考,还是仍然停留在 C 程序员的思维上。如果遇到这样的问题,你应该解释 std::string 类,并展示如何用它拼接两个字符串。顺便提到 string 类会自动为你处理所有内存管理,并拿它与 C 风格字符串作对比,也很有价值。

多数面试官不会追问本地化的具体细节。如果你确实被问到自己在本地化方面的经验,一定要提到:从项目一开始就考虑全球范围内的使用场景非常重要。

你也可能被问到 locale 与 facet 的基本思想。大概率不需要解释精确语法,但你应该说明,它们能够让你按照特定语言或国家的规则格式化文本与数字。

你或许还会被问到 Unicode,不过几乎可以肯定,这类问题关注的是 Unicode 的基本思想和核心概念,而不是实现细节。因此,你要确保自己理解 Unicode 的高层概念,并能在本地化语境下解释它们的用途。你还应该知道 UTF-8、UTF-16 等 Unicode 字符编码方案,但不必记住具体细节。

正如第 21 章所讨论的,正则表达式的语法可能相当让人望而生畏。面试官不太可能追问正则表达式的零碎细节。不过,你应该能够解释正则表达式的概念,以及它们适合完成哪些类型的字符串处理任务。

凡是在专业环境中写过代码的人,几乎都遇到过写代码一团糟的同事。公司并不希望招来这样的人,因此面试官有时会尝试判断候选人的代码风格能力。

  • 风格很重要,即使是在那些并非明确考查风格的面试题中也是如此。
  • 写得好的代码不需要大量注释。
  • 注释可以用来传达元信息。
  • 分解(decomposition)是把代码拆分成更小部分的实践。
  • 重构(refactoring)是重新组织代码的过程,例如清理先前写下的代码。
  • 命名技巧很重要,因此要认真对待变量、类等的命名方式。

风格类问题可能有几种不同形式。我有位朋友曾被要求在白板上写一个相对复杂算法的代码。他刚写下第一个变量名,面试官就叫停并告诉他通过了。那道题根本不是在考算法,只是个烟雾弹,用来观察他的变量命名能力。更常见的情况是,你可能会被要求提交自己写过的代码,或者谈谈你对代码风格的看法。

当潜在雇主要求你提交代码样本时,一定要谨慎。你很可能在法律上无权提交为前雇主所写的代码。你还必须选出一段既能体现自身能力、又不需要太多背景知识的代码。例如,如果你面试的是数据库管理岗位,就不应该提交自己关于高速图像渲染的硕士论文代码。如果公司明确要求你写一个特定程序,那正是展示你从本书中学到内容的绝佳机会。如果潜在雇主没有指定程序内容,你可以考虑专门为这家公司写一个小程序提交。与其从既有代码中挑一段,不如从零开始写一份与岗位相关、并能体现良好风格的代码。

如果你手头有自己写过、且可以公开发布的文档——也就是说不涉及保密内容——那就拿出来展示你的沟通能力;这会为你加分。你搭建或维护过的网站,以及你发表在 Stack Overflow(stackoverflow.com)、CodeGuru(codeguru.com)、CodeProject(codeproject.com)等平台上的文章,都是很有价值的材料。它们能告诉面试官:你不仅会写代码,也能向别人讲清楚如何有效地使用这些代码。

如果你正在为活跃的开源项目做贡献,例如 GitHub(github.com)上的项目,也能获得额外加分。更理想的情况是,你有自己持续维护的开源项目。这是展示代码风格和沟通能力的绝佳机会。对某些雇主来说,GitHub 之类网站上的个人主页本身就会被视作简历的一部分。

面试官不仅想确认你掌握了 C++ 语言本身,也想确认你擅长把它用好。你未必会被直接问到“设计题”,但优秀的面试官往往会用各种方式,把设计考察悄悄嵌入别的问题里,下面你就会看到。

潜在雇主还会想知道,你是否能够处理并非自己亲手编写的代码。如果你在简历中列出了第三方库,那么就应该准备好回答与它们相关的问题。如果你没有列出具体的库,对“库为什么重要”有一个总体认识通常也就足够了。

  • 设计具有主观性。面试时要准备好为自己的设计决策辩护。
  • 面试前,回想一下自己过去做过的某个设计细节,以防被要求举例。
  • 要准备好把设计以可视化方式画出来,包括类层次结构。
  • 要能清楚说明代码复用的好处与坏处。
  • 理解库的概念。
  • 了解从零构建与复用现有代码之间的取舍。
  • 了解大 O 记号的基础,或者至少记住 O(n log n) 优于 O(n2)。
  • 了解 C++ 标准库所包含的功能范围。
  • 知道设计模式的高层定义。

设计类问题对面试官来说并不容易命题;在面试场景中你能当场设计出来的程序,通常都过于简单,难以真实展示现实世界中的设计能力。设计题可能会以更模糊的形式出现,比如“请讲讲设计一个好程序的步骤”或者“请解释代码复用的基本原则”。它们也可能更不显山露水。比如,当你谈到上一份工作时,面试官可能会问:“你能给我讲讲那个项目的设计吗?”不过要小心,不要泄露前工作的知识产权。

如果面试官问的是某个具体库,他通常更关注这个库在高层面上的特点,而不是技术细节。例如,你可能会被问到,从库设计的角度看,标准库的优点和缺点是什么。最优秀的候选人会提到:标准库的覆盖面广、实现标准化,这是优点;而其用法有时较为复杂,则是缺点。

你也可能会被问到一类起初听起来与库无关、实则密切相关的设计问题。比如,面试官可能会问你如何设计一个从网络下载 MP3 音乐并在本地计算机播放的应用程序。这个问题表面上没有直接提到库,但它真正想问的是开发过程。

你应该先谈自己会如何收集需求、制作初步原型。由于题目提到了两种具体技术,面试官也会想知道你打算如何处理它们。这正是库发挥作用的地方。如果你告诉面试官,你会自己写网络相关的类和播放 MP3 的代码,你不会因此直接失败,但对方很可能会追问你:为什么要花时间和成本重新发明这些工具。

更好的回答是:你会先调研现有的网络与 MP3 功能库,看看是否存在适合该项目的方案。你甚至可以提及一些起步会考察的技术,例如 Linux 下用于网络获取的 libcurl(curl.haxx.se),或 Windows 下用于音乐播放的 Windows Media 库。

如果你还能顺带提到一些提供免费库的网站,以及这些网站大致提供什么内容,也可能会获得额外加分。例如,Windows 库可参考 codeguru.comcodeproject.com,跨平台 C++ 库可参考 boost.orggithub.com,等等。若你还能举出一些开源软件常见许可证的例子,例如 GNU General Public License、Boost Software License、Creative Commons license、MIT license、OpenBSD license 等,也可能拿到附加分。

面向对象设计题是用来区分两类人的:一类是只知道“类是什么”的 C 程序员,另一类是真正会使用这门语言面向对象特性的 C++ 程序员。面试官不会想当然;即使你已经用了多年面向对象语言,他们仍可能希望看到你真正理解这种方法论的证据。

  • 过程式范式与面向对象范式之间的区别
  • 类与对象的区别
  • 如何从组件、属性和行为的角度来表达类
  • is-a 与 has-a 关系
  • 多重继承涉及的权衡

面向对象设计问题通常有两种问法:一种是让你定义某个面向对象概念,另一种是让你画出一个面向对象层次结构。前一种相对直接。记住,举例往往能为你加分。

如果让你勾勒一个面向对象层次结构,面试官通常会给出一个简单应用场景,比如纸牌游戏,让你为它设计类层次。面试官爱问游戏类设计题,是因为大多数人对这类应用比较熟悉;与数据库实现之类的问题相比,它也能让气氛稍微轻松一些。当然,你最终画出的层次结构会随着具体游戏或应用而不同。下面这些点值得注意:

  • 面试官想看到的是你的思考过程。大声思考、头脑风暴,并把面试官拉进讨论中,非常重要。不要害怕擦掉重来,换个方向继续!
  • 面试官可能默认你熟悉这个应用。如果你从没听说过二十一点,却被问到了相关问题,就应当请面试官澄清或更换题目。
  • 除非面试官指定了描述层次结构的格式,否则一般建议把类图画成继承树,并为每个类粗略列出成员函数与数据成员。
  • 你可能需要为自己的设计辩护,或者根据新增需求修订设计。尽量判断面试官是真的看到了你设计中的缺陷,还是只是想把你逼到防守位置,观察你的说服能力。

面试官很少会专门问“如何设计可复用代码”。这其实有些可惜,因为如果一个编程组织里的程序员只会写一次性代码,往往会带来不小的损害。偶尔你会遇到真正懂代码复用、并在面试中问及此事的公司。遇到这种问题,某种意义上也说明那可能是家值得去工作的公司。

  • 抽象原则
  • 子系统与类层次结构的创建
  • 良好接口设计的一般规则:接口不应暴露实现细节,也不应包含 public 数据成员
  • 何时使用模板实现多态,何时使用继承

面试官可能会让你解释抽象原则及其好处,并给出一些具体示例。

关于复用的问题,几乎一定会围绕你做过的过往项目来展开。比如,如果你曾在一家同时开发消费级和专业级视频编辑应用的公司工作,面试官可能会问这两款应用之间如何共享代码。即使对方没有显式问到代码复用,你也可以顺势带出来。在介绍过去做过的工作时,可以主动告诉面试官,你写的模块是否被其他项目复用过。即便是在回答那些看上去只是纯编码的问题时,也要记得思考并提及接口设计。和往常一样,也要注意不要泄露前工作的知识产权。

你几乎可以肯定,面试官会问你一些与内存管理相关的问题,包括你对智能指针的掌握情况。除了智能指针,还会涉及更底层的问题。其目的在于判断:C++ 的面向对象特性是否让你离底层实现细节太远。内存管理问题正好给你一个机会,证明自己清楚程序底层究竟发生了什么。

  • 知道如何画出栈和自由存储区;这有助于理解实际发生的情况。
  • 避免使用底层内存分配与释放函数。在现代 C++ 中,不应再出现 newdeletenew[]delete[]malloc()free() 等调用,而应改用智能指针。
  • 理解智能指针;默认使用 std::unique_ptr,在需要共享所有权时使用 shared_ptr
  • 使用 std::make_unique() 创建 unique_ptr
  • 使用 std::make_shared() 创建 shared_ptr
  • 永远不要使用 auto_ptr;它自 C++17 起已被移除。
  • 如果你确实需要使用底层内存分配函数,应使用 newdeletenew[]delete[],绝不要用 malloc()free()
  • 如果你有一个“指向对象的指针数组”,仍然需要为数组中的每个指针单独分配内存并释放内存——数组分配语法并不会替你处理指针所指向的对象。
  • 了解内存分配问题检测器的存在,例如 Valgrind,它可以帮助暴露内存问题。

“找 bug”类问题中经常包含内存问题,例如重复释放、new/delete/new[]/delete[] 混用,以及内存泄漏。当你追踪大量使用指针和数组的代码时,应当边看代码边画出并更新内存状态图。

另一个判断候选人是否真正理解内存的好办法,是问“指针和数组有什么区别”。这个问题一开始可能会让你愣一下。如果真是这样,就再快速回顾一遍第 7 章中关于指针与数组的讨论。

在回答内存分配相关问题时,提到智能指针及其在自动清理内存和其他资源方面的优势,通常总是个好主意。你也应该提到:相比 C 风格数组,使用 std::vector 这类标准库容器要好得多,因为标准库容器会自动帮你处理内存管理。

第 8 章和第 9 章:熟练掌握类与对象,以及精通类与对象

Section titled “第 8 章和第 9 章:熟练掌握类与对象,以及精通类与对象”

围绕类与对象的问题几乎没有边界。有些面试官非常执着于语法,可能会甩给你一段相当复杂的代码;另一些人则不太在意实现细节,更关心你的设计能力。

  • 类定义的基本语法。
  • 成员函数与数据成员的访问说明符。
  • this 指针的用法。
  • 名称解析的工作方式。
  • 对象的创建与销毁,包括在栈上和自由存储区上的情况。
  • 编译器会在什么情况下为你生成构造函数。
  • 构造函数初始化列表。
  • 拷贝构造函数与赋值运算符。
  • 委托构造函数。
  • mutable 关键字。
  • 成员函数重载与默认参数。
  • const 成员。
  • 友元类与友元成员函数。
  • 在类中管理动态分配内存。
  • static 成员函数与数据成员。
  • 内联成员函数,以及 inline 关键字只是给编译器的一个提示,而编译器可以忽略这个提示这一事实。
  • 将接口类与实现类分离这一核心思想:接口应只包含 public 成员函数,尽可能保持稳定,并且不应包含任何数据成员或 private/protected 成员函数。这样接口可以保持稳定,而其下方的实现则可以自由变化。
  • 类内成员初始化器。
  • 显式 defaultdelete 的特殊成员函数。
  • 右值与左值之间的区别。
  • 右值引用。
  • 通过移动构造函数与移动赋值运算符实现的移动语义。
  • copy-and-swap 惯用法及其用途。
  • 零法则与五法则。
  • 运算符重载的基本语法。
  • 类的三路比较运算符。
  • 什么是显式对象参数(C++23)。
  • 什么是 constexprconsteval 函数与类。

像“关键字 mutable 是什么意思?”这样的问题,非常适合用于电话初筛。招聘人员手里可能会有一份 C++ 术语清单,并根据候选人答对了多少术语来决定是否进入下一轮。你未必能答出所有抛给你的术语,但要记住,其他候选人面对的也大概率是同样的问题,而这也是招聘人员手中为数不多的量化指标之一。

“找 bug”风格的问题深受面试官和课程教师的共同喜爱。你会看到一段乱七八糟的代码,然后被要求指出其中的问题。面试官常常苦于找不到量化分析候选人的办法,而这恰好是少数可行的方式之一。总体来说,你的策略应该是逐行阅读代码,把自己的疑虑说出来,并大声进行头脑风暴。可能出现的 bug 类型大致可以分为几类:

  • 语法错误: 这类问题较少见;面试官知道你可以用编译器找出编译期错误。
  • 内存问题: 例如泄漏和重复释放。
  • “你不会这么干”的问题: 这类问题在技术上也许没错,但并不推荐。例如,你不会使用 C 风格字符数组,而会改用 std::string
  • 风格错误: 即便面试官不把它算作 bug,也要指出糟糕的注释或变量名。

下面是一个“找 bug”题,几乎涵盖了上述所有方面:

class Buggy
{
Buggy(int param);
˜Buggy();
void turtle(int i = 7, int j);
protected:
double fjord(double val);
int fjord(double val);
int param;
double* m_graphicDimension;
};
Buggy::Buggy(int param)
{
param = param;
m_graphicDimension = new double;
}
Buggy::˜Buggy()
{
}
double Buggy::fjord(double val)
{
return val * param;
}
int Buggy::fjord(double val)
{
return (int)fjord(val);
}
void Buggy::turtle(int i, int j)
{
cout << "i is " << i << ", j is " << j << endl;
}

请仔细审视这段代码,然后再看看下面这个改进后的版本:

import std; // 导入标准库功能。
class Buggy final // 标记为 final,或提供虚析构函数。
{
public: // 这些很可能应该是公开的。
explicit Buggy(int param); // 构造函数应该是显式的。
// 不需要析构函数,因为没有需要清理的内容。
void turtle(int i, int j); // 只有最后的参数可以有默认值。
private: // 默认使用私有成员。
// int 版本无法编译。重载成员函数
// 不能仅在返回类型上有所不同。
double fjord(double val);
int m_param; // 数据成员命名。
double m_graphicDimension; // 使用值语义!
};
Buggy::Buggy(int param)
: m_param{ param } // 优先使用构造函数初始化列表。
{
}
void Buggy::turtle(int i, int j)
{
// 命名空间 + 使用 std::println()。
std::println("i is {}, j is {}", i, j);
}
double Buggy::fjord(double val)
{
return val * m_param; // 更改了数据成员名称。
}

你应当解释,为什么把 m_graphicDimensiondouble* 指针改成了 double 值。如果你确实需要使用指针,也应该说明:凡是表示所有权的指针,都不应使用原始指针,而应使用智能指针。

继承相关的问题通常与类相关的问题形式相近。面试官也可能要求你实现一个类层次,以确认你对 C++ 足够熟悉,能够在不翻书的情况下写出派生类。

  • 继承的语法
  • 从派生类视角看 privateprotected 的区别
  • 成员函数重写与 virtual
  • 重载、重写与隐藏之间的区别
  • 为什么基类析构函数应当是 virtual
  • 链式构造函数
  • 向上转型和向下转型的各种细节
  • C++ 中不同类型的强制类型转换
  • 多态原理
  • virtual 成员函数与抽象基类
  • 多重继承
  • 运行时类型信息(RTTI)
  • 继承构造函数
  • 类上的 final 关键字
  • 成员函数上的 overridefinal 关键字

继承类问题中的许多陷阱,都在于细节是否写对。当你编写一个基类时,不要忘记把相关成员函数声明为 virtual。如果你把所有成员函数都标成 virtual,也要准备好为这一决定辩护。你应当能够解释 virtual 的含义及其工作原理。另外,在派生类定义中,别忘了在父类名前写上 public 关键字(例如 class Derived : public Base)。面试中不太可能要求你进行非公有继承。

更有挑战性的继承问题,往往与基类和派生类之间的关系有关。一定要弄清不同访问级别的工作方式,尤其是 privateprotected 的区别。还要记住一种称为 slicing 的现象:某些类型转换会导致对象丢失其派生类信息。

第 11 章:模块、头文件与其他主题

Section titled “第 11 章:模块、头文件与其他主题”

本章主要聚焦于模块,但也讨论了头文件、链接、核心语言特性的特性测试宏、static 关键字、C 风格可变长度参数列表,以及预处理器宏。

  • static 的多种用法
  • 什么是模块,以及为什么相比头文件更推荐使用模块
  • 头文件语法与 #include
  • 预处理器宏的概念及其缺点
  • #define#pragma once 的使用
  • 不同类型的链接:无链接、外部链接、内部链接和模块链接
  • 为什么不应使用 C 风格可变长度参数列表

让候选人解释 static 是一道经典的 C++ 面试题。由于这个关键字有多种用途,它非常适合用来衡量你知识面的广度。你当然应该谈到 static 成员函数和 static 数据成员,并给出恰当示例。如果你还能解释 static 链接和 static 函数局部变量,就能获得额外加分。

模块是让代码具备复用性、并强化职责清晰分离的理想方式。你应当清楚如何使用模块,也应当知道如何编写一个基本模块。

第 12 章和第 26 章:使用模板编写泛型代码,以及高级模板

Section titled “第 12 章和第 26 章:使用模板编写泛型代码,以及高级模板”

模板是 C++ 中最“玄学”的部分之一,因此非常适合让面试官把 C++ 新手和老手区分开来。虽然大多数面试官会原谅你记不住某些高级模板语法,但你参加面试时至少应该把基础掌握扎实。

  • 如何使用类模板或函数模板
  • 如何编写一个简单的类模板或函数模板
  • 简写函数模板语法
  • 函数模板参数推导
  • 类模板参数推导(CTAD)
  • 别名模板,以及为什么 using 优于 typedef
  • concepts 背后的思想及其基本用法
  • 什么是可变参数模板和折叠表达式
  • 模板元编程背后的思想
  • 类型特征(type traits)及其用途

许多面试题一开始只是个简单问题,然后逐步增加复杂度。面试官通常已经准备好了几乎无穷无尽的复杂升级,只是想看看你到底能走到哪一步。例如,面试官可能先让你创建一个类,为固定数量的 int 提供顺序访问。接着,这个类需要扩展到支持任意数量的元素;再往后,它还得能处理任意数据类型,这时模板就登场了。之后,面试官还可以把问题引向不同方向:要求你使用运算符重载提供类似数组的语法,或者继续沿着模板这条路线追问,例如为模板类型参数提供默认类型,或者为其加上类型约束。不过,大多数面试官理解模板语法确实比较难,因此通常会原谅你的语法错误。

面试官也可能会问一些与模板元编程相关的高层问题,以确认你是否听说过它。在解释时,你可以举一个小例子,例如在编译期计算某个数字的阶乘。不必担心语法是否完全正确;只要你能说清它本应做什么,就已经足够了。

如果你面试的是 GUI 应用开发岗位,那么关于 I/O 流的问题可能不会太多,因为 GUI 程序往往使用其他 I/O 机制。不过,流也可能出现在其他问题中,而且作为 C++ 的标准组成部分,它完全在面试官可以考察的范围内。

  • 流的定义
  • 使用流进行基本输入与输出
  • 操纵器(manipulator)的概念
  • 流的类型(控制台流、文件流、字符串流等)
  • 错误处理技术
  • 标准文件系统 API 的存在

I/O 可能出现在任何问题的上下文中。例如,面试官可能让你读取一个包含测试分数的文件,并把这些分数放入 vector 中。这道题同时考查基础 C++、标准库和 I/O 能力。

管理者有时会回避把关键岗位(通常也意味着高薪岗位)交给应届毕业生或初级程序员,因为人们常常假设他们写不出具备生产质量的代码。你可以通过在面试中展示自己的错误处理能力,让面试官相信你的代码不会莫名其妙地突然崩掉。

  • 异常的语法
  • 以指向 const 的引用来捕获异常
  • 为什么异常层次结构优于少数几个通用异常
  • 当异常被抛出时,栈展开如何工作的基本原理
  • 如何在构造函数和析构函数中处理错误
  • 当异常抛出时,智能指针如何帮助避免内存泄漏
  • std::source_location 类可作为某些 C 风格预处理器宏的替代品
  • std::stacktrace 类可在程序执行期间的任意时刻获取栈回溯,并检查单个栈帧(C++23)

如果面试官主动提起错误处理,你要准备好讨论它。但不要强行把错误处理塞进讨论里、逼着面试官跟你聊这个话题——尤其是在对方真正想聚焦的是数据结构或算法等其他内容时。

面试官可能会问你不同的错误处理策略。此外,也可能要求你在不涉及实现细节的前提下,从高层面概述一下异常抛出时栈展开是如何工作的。

当然,并不是所有程序员都欣赏异常。有些人甚至会出于性能原因而对异常持有偏见。如果面试官要求你在不使用异常的前提下完成某件事,你就必须回退到传统的 nullptr 检查和错误码方案。

如果你了解 source_locationstacktrace 类,以及它们最重要的用例,会为你加分。

面试官也可能问你:是否应该因为异常会影响性能而避免使用异常。你应当解释,使用现代编译器时,真正“抛出异常”这件事可能会带来性能损失,但代码仅仅“具备处理潜在异常的能力”,其性能开销几乎为零。

你在面试中有可能——虽然概率不算太高——被要求做一些比简单运算符重载更难的事。有些面试官喜欢准备一道他们其实并不指望任何人答对的高阶问题。运算符重载的复杂细节非常适合这类“几乎不可能”的题目,因为很少有程序员能在不查资料的情况下把语法写对。因此,在面试前复习这部分内容是很值得的。

  • 流运算符重载,因为它们是最常见的一类被重载运算符,而且在概念上也很独特
  • 什么是函数对象(即可调用对象),以及如何创建一个函数对象
  • 将类的函数调用运算符设为 static 的好处是什么(C++23)
  • 如何在成员函数运算符和全局函数之间做选择
  • 某些运算符如何用其他运算符来表达(例如可通过对 operator> 的结果取反来实现 operator<=
  • 多维下标运算符及其用途(C++23)
  • 你可以定义自己的用户定义字面量,但不必掌握其语法细节

你拿到的具体问题无法精确预测,但运算符的数量毕竟是有限的。只要你见过那些“值得重载”的运算符各自的重载示例,基本就不会有太大问题。

一种可能的问题是让你写一个简单类,例如用于保存数学分数的 Fraction 类,然后面试官可能会要求你为其添加加法、减法等运算符支持。如果你不确定分数如何相加减,可以直接问面试官,因为那并不是题目的重点;重点在于写出重载运算符。

除了要求你实现重载运算符,也可能会问一些关于运算符重载的高层问题。“找 bug”题中,还可能包含一个被重载成某种概念上并不适合该运算符语义的操作符。除了语法外,也要牢记运算符重载的使用场景与理论基础。

正如你已经看到的,标准库中的某些内容确实不太容易使用。除非你声称自己是标准库专家,否则几乎没有面试官会期待你把标准库类的细节背得滚瓜烂熟。如果你知道自己要面试的工作会大量使用标准库,那么前一天写一些标准库代码、唤醒一下记忆,会是个不错的主意。否则,对标准库的高层设计及其基本用法有清晰认识,通常就足够了。

  • 各类容器,以及它们与迭代器之间的关系
  • vector 的使用,它是使用频率最高的标准库类
  • span 类以及为什么应当使用它
  • 什么是 mdspan(C++23)
  • 关联容器的使用,例如 map
  • 关联容器(例如 map)、无序关联容器(例如 unordered_map)以及平坦关联容器适配器(C++23,例如 flat_map)之间的差异
  • 如何使用函数指针、函数对象(可调用对象)和 lambda 表达式
  • 什么是透明运算符函数对象
  • 标准库算法的目的,以及部分内置算法
  • lambda 表达式与标准库算法结合使用的方式
  • remove-erase 惯用法
  • 很多标准库算法都提供并行执行选项,以提升性能
  • 你可以通过哪些方式扩展标准库(通常不需要细节)
  • 什么是范围(ranges)、投影(projections)、视图(views)和范围工厂(range factories)
  • Ranges 库的表达能力
  • 你自己对标准库的看法

如果面试官铁了心要问标准库细节题,那题型几乎没有上限。不过,如果你对具体语法没有把握,面试时不妨坦率地说出显而易见的事实:“现实工作中,我当然会去查 Professional C++,但我很确定它大概是这么工作的……” 至少这样一来,面试官会被提醒:只要你理解了基本思路,就不应过度苛求细节。

关于标准库的高层问题,常常被用来判断你究竟在多大程度上真正使用过标准库,而不必让你回忆全部细节。例如,标准库的普通使用者可能熟悉关联容器和非关联容器;稍微更进阶一些的使用者,则能够定义什么是迭代器、描述迭代器如何与容器协作,以及解释 remove-erase 惯用法。其他高层问题还可能围绕你使用标准库算法的经验,或你是否定制过标准库展开。面试官也可能会考察你对函数对象和 lambda 表达式的了解,以及它们如何与标准库算法配合使用。谈到 lambda 表达式时,如果你能进一步解释 auto 关键字在定义泛型 lambda 表达式中的作用,还能再加分。

你也可能会被问到使用 ranges 库的好处。记住,它的核心价值在于:它能让你写出描述“要做什么”而不是“如何去做”的代码。

C++ 标准库提供了 chrono 库,可用于处理日期与时间。面试官不太可能对这部分功能提出非常细致的问题,但她可能会借此判断你是否听说过标准库中的这部分内容。

  • 编译期有理数
  • 什么是 duration、clock 和 time point
  • 什么是日期与日历
  • 可以在不同时区之间转换日期与时间

面试官不太会问你 chrono 库的具体细节,而更可能描述一个涉及日期和时间的问题,然后问你会如何处理。如果你解释说自己会实现一套处理日期与时间的类,就要准备好说明为什么。更好的做法是解释:你会使用 chrono 库已经提供的功能。别忘了,代码复用是一项重要的编程范式。

在软件中生成高质量随机数本身就是一个复杂话题,面试官也知道这一点。他们不会追问语法细节,但你需要了解 <random> 库的基本知识和核心概念,它是 C++ 标准库的一部分。

  • <random> 库作为生成随机数的首选技术
  • 随机数引擎与分布如何协同工作以生成随机数
  • 什么是播种(seeding),以及它为何重要

面试官可能会给你看一段使用 C 函数 rand()srand() 生成随机数的代码,并让你评价这段代码。你应当解释,这些 C 函数如今已不再推荐使用,并说明为什么使用 <random> 提供的功能更好。

在与随机数相关的问题中,解释真随机数与伪随机数之间的区别也很重要。如果你还能说明:可以用 random_device 为伪随机数生成器生成真正随机的种子,以及为什么你不会始终只使用 random_device 而不用伪随机数生成器,那你会获得额外加分。

本章讨论了 C++ 标准库提供的一些额外词汇类型。面试官可能会挑其中几个话题,借此判断你对标准库知识面的宽度。

  • std::variantany 这两种词汇型数据类型,以及它们如何补充 optional
  • 作为 pair 泛化形式的 std::tuple
  • 什么是 std::expected 词汇类型,以及如何使用它(C++23)
  • optionalexpected 的单子操作(monadic operations)是什么(C++23)

你可能会被问到 variantanytuple 这些数据结构的使用场景。在解释 variantany 时,你也可以顺便把它们与第 1 章中的 optional 词汇类型做个对比。

如果你能解释 C++23 的 expected 数据类型,并用一个小例子展示其用法,通常会让面试官印象深刻。

如今几乎所有系统——从服务器到笔记本电脑,甚至手机——都配备了多核处理器。多线程编程对于榨干这些核心的性能至关重要。面试官可能会问你几个多线程相关问题。C++ 自带标准线程支持库,因此了解它如何工作很有必要。

  • 什么是竞争条件和死锁,以及如何防止它们
  • 使用 std::jthread 启动线程,以及为什么它可能优于 std::thread
  • 原子类型与原子操作
  • 互斥的概念,包括如何使用不同的互斥量和锁类在线程之间提供同步
  • 条件变量,以及如何使用它们向其他线程发出信号
  • 信号量、闩锁和屏障的概念
  • futures 和 promises
  • 跨线程边界复制并重新抛出异常
  • 什么是协程,包括其工作方式的高层概览
  • 标准 std::generator awaitable(C++23)

多线程编程本身很复杂,因此除非你面试的是专门的多线程编程岗位,否则不必预期会遇到非常细的题目。

更可能的是,面试官会让你解释多线程代码中可能遇到的各种问题:例如竞争条件、死锁和 tearing。她可能会让你说明为什么需要原子类型和原子操作。你也可能被要求解释多线程编程的一般概念。这是个很宽泛的问题,但它能帮助面试官了解你对多线程的掌握程度。若你能进一步解释互斥量、信号量、闩锁和屏障这些概念,就会拿到额外分数。你还可以提到,很多标准库算法都提供并行运行选项,以提升性能。

自己编写协程是复杂的,但从 C++23 起,标准库提供了标准的 std::generator 类型。如果你能结合一个小例子解释 generator 的工作方式,也会获得额外加分。

第 28 章:最大化软件工程方法的效益

Section titled “第 28 章:最大化软件工程方法的效益”

如果你完整走完一家公司的面试流程,却发现面试官完全没有问任何流程相关的问题,你就应该有所警惕——这可能意味着他们根本没有流程,或者并不在乎流程。当然,也有另一种可能:他们不想用自己那套庞然大物般的流程把你吓跑。

拥有一套定义明确的流程非常重要。同样,版本控制对于任何规模的项目来说都应当是必不可少的。

大多数时候,你都会有机会向公司提问。我建议你把“公司采用什么样的工程流程”和“使用什么版本控制方案”列入自己的固定问题清单。

  • 传统生命周期模型
  • 不同模型之间的权衡
  • 极限编程(Extreme Programming)的主要原则
  • 以 Scrum 作为敏捷流程的一个例子
  • 你过去使用过的其他流程
  • 什么是版本控制

最常见的问题,是让你描述前雇主采用的开发流程。不过要小心,不要泄露任何机密信息。回答时,你应当提到哪些地方运作良好、哪些地方失败了,但尽量不要猛烈抨击某一种特定方法论。因为你所批评的方法,很可能正是面试官当前正在使用的方法。

如今几乎每个候选人都会把 Scrum/Agile 写进技能列表。如果面试官问你 Scrum,她多半不是想听你机械背诵教科书定义——她知道你会翻 Scrum 书的目录。更好的做法是,从 Scrum 中挑出几条你真正认同的理念,分别解释给面试官听,并说说你的看法。尽量把对话引向面试官感兴趣的方向,根据她给出的暗示继续深入。

如果你被问到版本控制,基本也会是高层问题。你应当解释为什么应当使用版本控制,以及它能带来哪些好处。你也可以进一步解释本地式、客户端/服务器式和分布式方案之间的区别,甚至说明你前雇主是如何实施版本控制的。

效率类问题在面试中非常常见,因为许多组织都面临代码可扩展性问题,因此需要熟悉性能优化的程序员。

  • 语言层面的效率很重要,但它的作用终究有限;真正影响更大的,往往还是设计层面的选择。
  • 应避免复杂度很差的算法,例如平方级算法。
  • 引用参数更高效,因为它避免了拷贝。
  • 对象池可以减少对象创建和销毁的开销。
  • 性能剖析(profiling)至关重要,它能帮助你确定究竟哪些操作真的最耗时,从而避免把精力浪费在并非性能瓶颈的代码上。

面试官常常会以自己公司的产品为例,来引出效率相关问题。有时她会描述一个较旧的设计,以及自己遭遇的一些性能症状,让候选人提出一个缓解问题的新设计。不幸的是,这类问题有一个大难点:当问题当年在公司里真正被解决时,你恰好想出与他们相同方案的概率能有多大?既然这种概率很小,你就必须格外注意为自己的设计提供充分理由。你未必会提出他们当时采用的那个方案,但你依然可能给出一个正确的,甚至比公司后来设计更好的答案。

其他效率类问题,可能会要求你为了性能去调整某段 C++ 代码,或者对某个算法进行迭代优化。例如,面试官可能会给你看一段存在多余拷贝或低效循环的代码。

面试官还可能要求你从高层描述一下性能分析工具(例如 gprof 或 Visual C++),说明它们的好处,以及为什么应该使用它们。

潜在雇主很看重扎实的测试能力。由于你的简历通常不会明显体现测试技能——除非你有明确的质量保证(QA)经验——因此你可能会在面试中遇到测试相关的问题。

  • 黑盒测试与白盒测试的区别
  • 单元测试、集成测试、系统测试和回归测试的概念
  • 更高层级测试的技术
  • 你以前工作过的测试与 QA 环境:哪些地方有效,哪些地方无效?

面试官可能会要求你在面试过程中写一些测试,但由于面试中展示的程序通常不会复杂到足以支撑有趣的测试,因此这种情况并不常见。更可能的是,你会被问到一些高层测试问题。你要准备好描述上一份工作中测试是如何开展的,以及哪些方面你喜欢、哪些方面你不喜欢。再次提醒:不要泄露任何机密信息。在回答完面试官关于测试的问题后,你自己也可以反问一句:他们公司是如何做测试的。这很可能会引出一段关于测试的交流,也能帮助你更好地了解潜在工作环境。

工程组织会寻找这样的人:既能调试自己写的代码,也能调试从未见过的代码。技术面试往往会试图掂量一下你的调试功力。

  • 调试不是等 bug 出现时才开始;你应提前为代码加上必要的可观测性,这样 bug 出现时你才有准备。
  • 日志和调试器是你最好的工具。
  • 你应该知道如何使用断言。
  • bug 表现出来的症状,可能看上去与真正原因毫无关系。
  • 对象图在调试中会很有帮助,尤其是在面试场景中。

在面试中,你可能会碰到一个相当晦涩的调试问题。要记住,最重要的是过程,而面试官大概率也明白这一点。即便你没能在面试现场找出 bug,也要确保面试官知道:为了追踪这个问题,你会采取哪些步骤。如果面试官递给你一个函数,并告诉你它在执行时会崩溃,那么只要你能够有条理地说明自己定位 bug 的步骤,即使没有立刻找出真正的问题,也应当得到同样多、甚至更多的分数。

第 32 章中介绍的每一种技术,都足以成为一道不错的面试题。与其在这里重复你在那一章已经读过的内容,我更建议你在面试前快速浏览一遍第 32 章,确保自己能够理解其中每一种技术。

如果你面试的是 GUI 相关工作,那么你应当知道 MFC、Qt,可能还有其他一些框架的存在。

由于设计模式在专业开发世界中很受欢迎(很多候选人甚至把它写成技能点),你很可能会遇到希望你解释某种模式、给出某种模式的使用场景,或直接实现某种模式的面试官。

  • 设计模式作为可复用的面向对象设计思想这一基本概念
  • 你在本书中读到的模式,以及你在工作中用过的其他模式
  • 你和面试官可能会对同一种模式使用不同称呼,因为模式有数百种,而且名称往往彼此冲突

回答设计模式相关问题通常是件轻松的事,除非面试官希望你掌握人类已知的每一个设计模式的全部细节。好在,大多数真正重视设计模式的面试官只是想和你聊聊这些模式,并听听你的看法。毕竟,与其死记硬背,不如查书或查资料本身也是一种很好的“模式”。

第 34 章:开发跨平台与跨语言应用程序

Section titled “第 34 章:开发跨平台与跨语言应用程序”

很少有程序员提交的简历只列出一种语言或一种技术,也很少有大型应用完全依赖单一语言或单一技术。即使你面试的只是一个 C++ 岗位,面试官仍然可能会问到其他语言的问题,尤其是它们与 C++ 的关系。

  • 平台之间可能存在哪些差异(体系结构、整数大小等)
  • 面对某项任务时,你应尽量寻找跨平台库来完成,而不是自己从零开始为不同平台分别实现功能
  • C++ 可以与其他语言互操作,例如 C#、Java、脚本语言等

最常见的跨语言问题,是比较两种不同语言的异同。即使你非常喜欢或非常讨厌某门语言,也应避免只说它的优点或只说它的缺点。面试官想知道的是:你是否能看见取舍,并据此做出判断。

跨平台问题更可能出现在你介绍过往工作经历时。如果你的简历显示你曾经编写过运行在某种定制硬件平台上的 C++ 应用,那么你就应该准备好谈谈自己使用的编译器,以及那个平台带来的挑战。