c++是一个强大的, 从后端到嵌入式开发,通用编程语言, 桌面应用程序, 它们所运行的操作系统. 它是为数不多的经受住时间考验的语言之一, 因此,它当之无愧的受欢迎和尊重. 但是,尽管ISO c++标准委员会和社区做出了努力,使其对程序员更友好, 可以说,英语仍然是最难掌握的语言之一.
c++有什么特别之处?
A 伟大的c++开发人员 主要是 伟大的软件开发人员:具有很强的解决问题能力和抽象思维的人, 能够找到合适的工具和框架, 以及对计算机科学的热情.
的re 是 plenty of interview questions that 是 language independent 和 designed to check 的 engineering prowess of 的 c和idate; on a more basic level, 的 FizzBuzz问题 在过滤一般编码能力方面是否非常有效. 但我们目前的重点将非常特定于c++.
如果候选人也熟悉应用程序开发 其他语言,这是一个让面试过程更有成效的机会. 在这种情况下, 这三个问题将引发关于不同语言哲学的有趣讨论, 其基本结构, 以及由此产生的优点和缺点.
问:什么是RAII,它与没有RAII的事实有什么关系 最后
关键字在c++异常处理?
RAII代表“资源获取即初始化”,是一种c++特有的资源管理技术. 它是基于c++有析构函数的事实,并保证当对象超出作用域时,析构函数会被调用, 即使在异常处理中.
我们不需要 最后
来处理我们的资源,因为我们可以用RAII包装它们,并确保析构函数将被调用, 多亏了 堆栈解除.
问:什么是恒定正确性? 它的目的(价值)是什么?
不变正确性是将变量和成员函数指定为的实践 常量
如果它们不被修改,或者分别修改对象的状态. 最好的做法是使用 常量
只要有可能.
常量
程序员和编译器之间是否存在一种契约——它不会以任何方式显示在生成的机器码中. 这是 一种优雅的方式 为代码编制文档,使其可读性更强,更不易出错. 考虑以下代码片段:
void PaySalary(常量员工Id 员工_id)
{
员工& 公司员工=.Find员工 (员工_id);
Const 汽车 base_salary = 员工.GetBaseSalary ();
Const 汽车per为mance_rating = 员工.MonthlyPer为mance ();
Const汽车债务=公司.GetDebt(员工);
常量汽车 total_pay = CalculateSalary(base_salary, per为mance_rating,债务);
公司.PaySalary(员工,total_pay);
}
在这个函数中,我们获取一个对象的引用,并执行一些进一步的操作. 有人可能想知道这个对象会发生什么——特别是,什么时候可以修改它?
这一事实 GetBaseSalary
和 MonthlyPer为mance
是 常量
成员函数告诉我们 员工
在这些调用之后不会改变它的状态. GetDebt
需要一个 常量员工&
,这再次说明它没有被修改过. 的 PaySalary
然而,函数接受 员工&
,所以我们会在那里挖掘.
在设计类时,应该特别注意常量正确性, 因为它包含了给将要使用它的开发人员的消息.
较新的c++标准
c++在过去十年中以前所未有的速度发展. 自2011年以来,我们每三年就有一个新的标准:c++ 11、c++ 14、c++ 17和c++ 20. 相对于c++ 11, c++ 14只是一个小的更新,但是其他版本都有一些重要的新特性. 这些特性的知识显然是必要的, 但了解它们对旧标准的替代方案也很重要.
尽管新标准中的绝大多数变化都是添加的(弃用和删除的情况非常少见),但将代码库切换到新标准远非易事. 一个适当的开关,充分利用新特性,将需要大量的重构.
而且仍然有大量的c++ 98和c++ 03代码需要持续的关注和注意. c++ 11和c++ 14也是如此. 除了知道 常量expr如果
(随c++ 17而来), 一个优秀的c++程序员也应该知道如何在使用老标准时获得相同的结果.
一个有趣的开始讨论的方法是问候选人他们最喜欢的特性. 一个强大的候选人应该能够列出给定标准的最重要的特性, 选择一个最喜欢的, 并给出他们选择的理由. 当然, 答案没有对错之分, 但它很快就能显示出候选人对这门语言的感觉.
c++ 11/14引入了几个重要的特性. 你认为哪一个带来了最大的改善,为什么?
的 汽车
关键字和基于范围 为
循环
的 汽车
关键字使我们不必在编译器能够推断变量类型时显式地指定变量类型. 这将产生更干净、更可读、更通用的代码. 基于范围的 为
对于最常见的用例,Loop是一种语法糖.
/ / c++之前11
为 (std::向量::常量_iterator it = vec.begin(); it != vec.end(); ++it) {
/ / c++后11
(常量汽车& Val: vec) {
Lambda函数
这是一种新的语法,允许开发人员就地定义函数对象.
std:: count_if (vec).开始(),矢量.end(), [](常量 int value) { return value < 10; });
移动语义
这允许我们显式地定义一个对象获得另一个对象的所有权意味着什么. 因此,我们得到了仅移动类型(std:: unique_ptr
, std::线程
),从临时文件中高效的浅拷贝,等等. 这是一个相当大的主题Scott Meyers的 有效的现代c++: 42种提高你使用c++ 11和c++ 14的方法.
智能指针
c++ 11引入了新的智能指针: unique_ptr
, 要查看
, weak_ptr
. unique_ptr
有效地使 汽车_ptr
过时了.
对线程的内置支持
无需与操作系统本机库和c风格库作斗争.
问:你觉得c++ 17的哪个特性最有用,为什么?
结构化的绑定
这个特性增加了可读性.
/ / c++之前17
(常量汽车& Name_和_id: name_to_id) {
常量汽车& name = name_和_id.第一个;
常量汽车& id = name_和_id.第二个;
/ / c++后17
(常量汽车& [name, id]: name_to_id) {
编译时 if
与 如果常量expr
,我们可以根据编译时条件启用代码的不同部分. 这使得模板元编程(TMP) enable_if
魔术更容易和更好的实现.
内置的文件系统库
就像c++ 11中的线程支持一样, 这个内置库继续减少了c++开发人员编写特定于操作系统的代码的需求. 它提供了一个接口来处理文件本身(而不是它们的内容)和目录, 允许开发人员复制它们, 删除它们, 递归地遍历它们, 等等.
平行STL
c++ 17包含了许多常用STL算法的并行替代方案——自从多核处理器在台式机上变得普遍以来,这是一个重要的特性.
问:你认为c++ 20的哪个特性是一个很好的补充,为什么?
对于开发人员来说,至少有两个很有前途的c++ 20特性.
约束和概念
这个特性提供了丰富的新语法,使TMP更结构化,其构造可重用. 如果使用得当,它可以使代码更具可读性和文档更好.
范围
背后的c++程序员 ranges-v3图书馆 提倡将其包含在c++ 20中. 使用Unix类似管道的语法, 可组合结构, 懒惰的范围组合子, 和其他特性, 这个库的目的是给c++一个现代的, 功能看.
std::向量 numbers = …;
为(自动号码:数字
视图| std::::变换(abs)
视图| std::::过滤器(is_prime)) {
…
}
//没有范围的替代
对于(汽车 orig_number: numbers) {
自动编号= abs(orig_number);
if (!is_prime(数量)){
继续;
}
…
}
在上面的例子中, 变换
和 过滤器
在运行时执行操作,而不影响原始容器的内容.
最初, 内定, c++的创造者, 把它命名为“C with classes ?,"动机是支持面向对象编程(OOP). 对OOP概念和设计模式的一般理解是在该语言之外的, 所以在大多数情况下,检查c++程序员的OOP知识并不需要是c++特有的. 然而,有一些小的特殊性. 一个非常常见的面试问题是:
Q: c++中动态多态性(虚函数)是如何工作的?
在c++中,动态多态性是通过虚函数实现的.
类的音乐家
{
公众:
void 玩() = 0;
};
级钢琴家:公众音乐家
{
公众:
virtual void 玩() override {
/ /钢琴演奏的逻辑
}
};
级吉他手:公共音乐家
{
公众:
void 玩() override {
/ /以吉他演奏专业逻辑
}
};
空白AskTo玩(音乐家*音乐家)
{
音乐家->玩();
}
多态性(源自希腊语 聚,意思是“许多”和 变形意思是“形状”) AskTo玩
根据对象的类型以不同的方式表现 音乐家
指向(可以是a 钢琴家
或者一个 吉他手
). 问题是,c++代码必须编译成一个有固定指令集的机器码, 包括,特别是 AskTo玩
函数. 这些指令必须选择在运行时跳转(调用)到哪里 钢琴家:玩
or 吉他手:玩
.
对于编译器来说,最常见和最有效的方法是使用 虚拟表 (or 虚表). 虚表是虚函数的地址数组. 每个类都有自己的虚函数表,该类的每个实例都有一个指向它的指针. 在我们的示例中,对 玩
实际上变成了对一个函数的调用,该函数驻留在虚函数表的第一个条目中所写的地址 *音乐家
指出. 如果它被实例化为 钢琴家
,它指向vtable的指针将被设置为 钢琴家
’s的虚函数表,我们最终会调用正确的函数.
另一个特性来自于c++支持多重继承的事实,并且没有正式的区别 接口 和一个 class. 经典的问题 钻石的问题 这是讨论这个问题的良好开端吗.
是一种多范式编程语言, c++也支持函数式编程。, 在c++ 20标准中引入范围后,这已经成为一个更大的问题. 它使c++与其他语言的距离更近了, 从函数代码的表达性和可读性的角度来看,它是更加fp友好(或聚焦于fp)的语言. 一个起始问题的例子是:
问:lambda函数是如何工作的?
函数,编译器生成函数类 操作符()
的主体, 以及使用成员存储lambda所捕获变量的副本或引用. 这里有一个 例子.
这和另一个问题是一样的 虚函数当前位置应聘者要么已经深入了解了公司的运作机制,知道它是如何运作的, 或者,这是一个开始一般性讨论的好地方,可以问一些后续问题,比如 它们主要在哪些方面派上用场? 和 在lambda函数之前,c++编程是什么样的?.
模板元编程是c++支持的另一种范式. 它包含了许多有用的特性, 哪些是伴随较新的标准引入的更好的替代方案. 新功能 常量expr
和 概念
s使TMP不那么令人费解,但话说回来,仍然有许多带有谜题的生产代码. 一个著名的TMP难题是:
Q:如何在编译时计算第n个斐波那契数?
在新的标准下,这就像拥有一个 常量expr
说明符在递归实现的开头.
conexpr int fibo(int n)
{
if (n <= 1) {
返回n;
}
返回fibo(n - 1) + fibo(n - 2);
}
没有它,它将是:
template
struct Fibo
{
static 常量 int value = Fibo::value + Fibo::value;
};
template <>
struct Fibo<1>
{
Static 常量 int value = 1;
};
template <>
struct Fibo<0>
{
Static 常量 int value = 0;
};
STL,数据结构和算法
标准模板库(STL)是c++内建的一个库. 它在c++中已经存在了很长时间,现在很难将它从核心语言中分离出来. STL为c++开发者带来了四种特性:
- 容器——一些基本数据结构的实现,比如
向量
, 列表
, map
, u也不dered_map
- 算法——一些基本算法的实现,比如
排序
, binary_search
, 变换
, 分区
- 迭代器——一种用于算法的容器抽象
- 函子——一种定制算法的方法
STL的设计和理念是独特和广泛的. 任何优秀的c++程序员都必须对STL有很强的了解,但问题是,了解到什么程度? 第一层是所谓的 用户级 知识. 这时候选人至少知道最流行的容器和算法, 如何以及何时使用它们. 接下来的三个经典问题可以验证这一点:
问:这两者有什么区别 列表
和 向量
? c++开发人员应该如何选择它们呢?
这两个 列表
和 向量
顺序容器是基于不同的数据结构吗. 列表
是基于双链表,然而 向量
包含原始数组.
优势由他们平分. 向量
连续存储数据的优点是什么, 没有内存开销和常量时间索引访问. 相比之下, 列表
具有常量时间插入和移除在任何位置,并支持像 拼接
, 合并
,就地 排序
和 反向
.
作为一个结果, 向量
更受欢迎和大多数时候是正确的选择吗. 列表
在某些特殊情况下,这会是更好的选择吗, 比如在处理重拷贝对象时, 这样开发者就可以让它们保持有序, 或者通过操作节点连接从一个容器移动到另一个容器.
Q:如何从a中移除所有_42_s 向量
? 一个是什么 列表
?
最简单的方法是遍历容器,并用 擦除
成员函数,两个容器都具有. 这在a的情况下是完全有效的 列表
; in fact, its 删除
成员函数的作用与此基本相同.
但这是有原因的 向量
没有这样一个 成员 函数. 这种方法非常低效,因为 向量
的底层数据结构. 的元素 向量
在内存中是连续的,所以删除一个元素意味着移动 所有 随后的元素返回一个位置. 这将导致大量的额外复制.
最好的方法是 擦除-删除 成语:
vec.擦除(std::删除(vec).开始(),矢量.结束(),42),矢量.结束());
//初始- {1,2,42,3,4,42,5,6}
//删除后- {1,2,3,4,5,6, ?, ?, }
//删除后- {1,2,3,4,5,6}
首先,我们运用 删除
在整个范围内,这不会移除 42
S从它开始,它只是把其他的移到开始,这是最有效的方式. 而不是移动的 6
向左移了两个位置(如果我们单独擦除就会发生这种情况) 42
S),它只会移动 6
一次,两个位置. 与 擦除
然后,我们擦掉最后的“浪费”.
这个习语在Scott Meyers的传说中的Item 32中有很好的描述 有效的STL:提高标准模板库使用的50种具体方法.
c++中有哪些不同类型的迭代器? 哪些是有用的 std::类
?
STL迭代器有六种类型: input, output, 向前, 双向, 随机存取, 连续的.
迭代器的类别表示它的功能,这些类别是包含的. 这意味着,'的迭代器 集
is 双向’实际上意味着它也是 向前
但无论是 随机存取 也不 连续的.

类别的差异是由容器的数据结构造成的. A 向量
迭代器是一种 随机存取 1,意思是,不像a 集
迭代器,它可以在常数时间内跳转到任意点. std::类
要求其参数为 随机存取 迭代器.
这些可以说是STL中最受欢迎的部分, 所以任何有至少一年经验的c++程序员都应该和他们一起工作过. 但这还不够. 一个高质量的c++开发人员不仅要知道所有的STL容器以及如何使用它们,还要知道它们背后的抽象数据结构, 各有优缺点.
而且,他们不仅要知道大部分的算法,还要知道他们的 渐近的复杂性. 这意味着c++开发人员应该理解, 大0而言, 标准算法的性能及其使用的数据结构, 以及两者背后的理论. 问题可以非常具体:
STL容器基本面
而不是穷尽地测试这些知识, 审计风格的一些事实的选择——如果需要的话更多——应该足够清楚地表明候选人是否精通这些常见的需要的细节.
- [一个给定的容器,e.g.,
向量
)实现?
- 哪一类是[给定容器]的迭代器?
- 一个给定的操作,e.g.,
删除
在[给定容器]上?
向量
使用大小标记将元素存储在基础数组中. 当它满, 一个新的, 分配更大的存储空间, 元素从旧存储中复制, 然后被释放. 的迭代器 随机存取 类别. insert的渐近复杂度(push_back方法
) is 平摊常数时间,索引访问为 常数时间,删除 线性.
列表
实现一个双链表,存储指向头节点和尾节点的指针. 它的迭代器 双向. insert的复杂度(push_back方法
和 push_front
)及移除(pop_back
和 pop_front
) 常数时间,而索引访问是 线性.
集
和 map
是在平衡二叉搜索树上实现的吗, 更确切地说,是红黑树, 从而保持元素排序. 他们的迭代器 双向 也. insert、find(查找)和删除的复杂性如下 对数. 使用迭代器删除元素(擦除
成员函数) 平摊常数时间 复杂性.
u也不dered_集
和 u也不dered_map
哈希表数据结构的实现. 通常,它是一个所谓的存储区数组,它是简单的链表. 根据元素的散列值和bucket的总数为元素选择bucket(在插入或查找期间). 当容器中有如此多的元素,以至于平均桶大小大于实现定义的阈值(通常为1.0),桶的数量增加,元素被移动到正确的桶. 这个过程叫做 再处理,增加了插入的复杂性 平摊常数时间. 找到并删除 常数时间 复杂性.
这些是最基本的容器——必须了解上述细节, 但是否每一个细节都经过测试,取决于面试官的直觉.
Q:[给定算法]的渐近复杂度是多少??
一些著名的STL算法的渐近复杂性是:
<的ad>
算法 |
复杂性 |
的ad>
std::类 |
O (N log (N)) |
std:: nth_element |
O(N) |
std::推进 |
O (1) 随机访问迭代器, O(N)为其他 |
std:: binary_search |
O (log (N)) 随机访问迭代器, O(N)为其他 |
std:: 集_intersection |
O(N) |
或者,一个简单的问题就能涵盖一切:
问:哪些容器可以 插入 操作使迭代器失效?
这个问题的优雅之处在于它很难记住, 正确的答案需要对带有某些逻辑的容器的底层数据结构有广泛的了解.
的 插入 操作可能使上的迭代器失效 向量
, 字符串
, 双端队列
, u也不dered_集 /地图
(在扩展/重列的情况下). 为 列表
, 集
, map
但是,由于它们基于节点的数据结构,情况并非如此.
在许多开发人员面试中,标准做法是提供一个计算问题,然后期望候选人开发一个算法来解决它. 有很多优秀的平台可以进行竞争性的c++编程,比如LeetCode, HackerRank等等. 充满了这样的问题.
这些问题很有价值,因为它们可以一次检查很多东西. 大多数面试官会故意用不明确的问题陈述来测试应聘者处理问题的技巧和提出正确问题的能力. 然后, 候选人必须解决这个问题, 编写代码,, 最重要的是, 进行复杂性分析.
在使用STL容器和算法时,善于使用后者并对数据结构有足够的了解就足以做出明智的决定, 以及写新的. 有些人可能会走得更远,查看实际的实现. 每个人都在某一时刻以STL代码结束,要么是偶然的,要么是在调试时,或者是自愿的. 如果候选人去了那里,并设法弄懂了那些胡言乱语, 这说明了他们的经历, 好奇心, 和毅力. 例如:
Q:哪一种排序算法呢 std::类
实现?
的 ISO C++ st和ard doesn’t specify 的 algorithm; it only 集s 的 requirement of 渐近的复杂性. 然后,选择取决于执行(i.e.即微软vs. GNU.)
它们中的大多数不会只用一个算法来解决,比如 归并排序 or 快速排序. 一种常见的方法是将它们混合使用,或者根据大小选择一种. 例如,GCC实现 intro排序,它是 快速排序 和 堆排序. 它从第一个开始,当长度小于实现定义的阈值时切换到堆排序.
c++:久经考验,但开发团队必须善于避免其缺陷
本指南中概述的问题涵盖了c++编程的一些基本和一些棘手的方面. 但就像它的可能性一样, 语言中隐藏的惊喜是无限的, 这使得在面试中很难涵盖c++的所有内容. 因此, 评估候选人的能力是很重要的, 技能, 对c++有深刻的理解,能够清晰生动地表达自己的想法.
我们希望这些问题能帮助你在全职或兼职的岗位上寻找一个真正高质量的c++专家. 这些稀有的精英可能很难获得, 但是对于你的c++编程团队来说,它们将明显地从包的其余部分中脱颖而出.