c++是一个强大的, 用于从后端到嵌入式开发的所有内容的通用编程语言, 到桌面应用程序, 到它们运行的操作系统. 它是少数几种经受住时间考验的语言之一, 因此,它当之无愧地受到欢迎和尊重. 但是,尽管ISO c++标准委员会和社区努力使其对程序员更友好, 它仍然可以说是最难掌握的语言之一.
c++的特别之处?
A 伟大的c++开发者 主要是 优秀的软件开发人员 具有很强的解决问题能力和抽象思维能力的人, 找到合适的工具和框架的能力, 以及对计算机科学的热情.
There are plenty of interview questions that are language independent and designed to check the engineering prowess of the candidate; on a more basic level, the FizzBuzz问题 在过滤一般编码能力方面是出了名的有效. 但我们目前的重点将非常具体地针对c++.
如果候选人也熟悉应用程序开发 其他语言 ,你就有机会让面试过程更有成效. In that case, 这前三个问题将开启一场关于不同语言哲学的有趣讨论, 它们的基本结构, 以及由此产生的优点和缺点.
问:什么是RAII ?它与没有RAII的事实有什么关系 finally
c++异常处理中的关键字?
RAII代表“资源获取即初始化”,是一种特定于c++的资源管理技术. 这是基于这样一个事实:c++有析构函数,并且保证当对象超出作用域时将调用它们, 甚至在异常处理中.
We don’t need finally
来处理我们的资源,因为我们可以用RAII包装它们,并确保析构函数将被调用, thanks to 堆栈解除 .
问:什么是常量正确性? 目的(价值)是什么??
const正确性是将变量和成员函数指定为 const
如果它们不打算被修改,则分别修改对象的状态. 最佳实践是使用 const
只要有可能.
const
程序员和编译器之间是否存在契约——它不会以任何方式在生成的机器代码中表现出来. It’s 优雅的方式 对代码进行文档化,使其更易读,不易出错. 考虑下面的代码片段:
void PaySalary(const员工Id employee_id)
{
Employee& 雇员=公司.FindEmployee (employee_id);
自动base_salary = employee.GetBaseSalary ();
Const auto performance_rating = employee.MonthlyPerformance ();
汽车债务=公司.GetDebt(员工);
const auto total_pay = CalculateSalary(base_salary, performance_rating, debt);
company.PaySalary(员工,total_pay);
}
在这个函数中,我们获取一个对象的引用并执行一些进一步的操作. 有人可能想知道这个对象发生了什么——特别是,它可能在什么时候被修改?
The fact that GetBaseSalary
and MonthlyPerformance
are const
成员函数告诉我们 employee
在这些调用之后不会改变它的状态. GetDebt
takes a const员工&
也就是说,它没有被修改过. The PaySalary
然而,函数需要一个 Employee&
这就是我们要深入研究的地方.
在设计类时,应该特别注意const的正确性, 因为它包含给将要使用它的开发人员的消息.
更新的c++标准
在过去十年中,c++以前所未有的速度发展. 自2011年以来,我们每三年就有一个新标准:c++ 11、c++ 14、c++ 17和c++ 20. c++ 14是对c++ 11的一个小更新,但其他所有版本都带来了重要的新特性. 了解这些特征显然是必要的, 但了解他们对旧标准的替代方案也很重要.
尽管新标准中的绝大多数更改都是添加的——弃用和删除的情况非常罕见——但将代码库切换到新标准远非易事. 适当的转换,充分利用新特性,将需要大量的重构.
而且仍然有大量的c++ 98和c++ 03代码需要持续的关注和关注. c++ 11和c++ 14也是如此. 除了了解 constexpr if
(c++ 17自带), 一个优秀的c++程序员也应该知道如何在使用旧标准的情况下获得同样的结果.
一个有趣的开始讨论的方法是问候选人他们最喜欢的特性是什么。. 强有力的候选人应该能够列出给定标准的最重要的特性, 选择一个最喜欢的, 并给出他们选择的理由. Of course, 没有正确或错误的答案, 但它很快就揭示了候选人对语言的感觉.
问:c++ 11/14引入了几个重要的特性. 你认为哪一个带来了最大的改善,为什么?
The auto
关键词:基于范围 for
Loops
The auto
关键字使我们不必在编译器能够推断变量类型时显式地指定它. 这将产生更清晰、更易读和更通用的代码. 基于范围的 for
循环是最常见用例的语法糖.
// c++ 11之前
for (std::vector::const_iterator it = vec.begin(); it != vec.end(); ++it) {
// c++ 11之后
For (const auto .& val : vec) {
Lambda函数
这是一种允许开发人员就地定义函数对象的新语法.
std:: count_if (vec).begin(), vec.end(), [](const int value) { return value < 10; });
Move Semantics
这允许我们显式地定义一个对象获得另一个对象的所有权意味着什么. 因此,我们得到了只能移动的类型(std:: unique_ptr
, std::thread
),有效的浅拷贝从临时等. 这是一个很大的话题在斯科特·迈耶斯的 有效的现代c++:提高c++ 11和c++ 14使用的42种具体方法 .
Smart Pointers
c++ 11引入了新的智能指针: unique_ptr
, shared_ptr
, and weak_ptr
. unique_ptr
有效地使 auto_ptr
obsolete.
对线程的内置支持
无需纠结于os原生和c风格的库.
问:你觉得c++ 17的哪个特性最有用?为什么?
结构化的绑定
这个特性增加了可读性.
// c++之前
For (const auto .& Name_and_id: name_to_id) {
const auto& Name = name_and_id.first;
const auto& Id = name_and_id.second;
// after C++17
For (const auto .& [name, id]: name_to_id) {
Compile-time if
With if constexpr
,我们可以根据编译时条件启用代码的不同部分. 这使得模板元编程(TMP) enable_if
魔术更容易和更好地实现.
内置文件系统库
就像c++ 11中的线程支持一样, 这个内置库继续减少c++开发人员编写特定于操作系统的代码的需要. 它提供了一个接口来处理文件本身(而不是其内容)和目录, 让开发者复制它们, remove them, 递归地迭代它们, and so on.
Parallel STL
c++ 17包含了大量使用的STL算法的并行替代方案,这是一个重要的特性,因为多核处理器甚至在桌面上也变得很常见.
问:你认为c++ 20的哪个特性是一个很好的补充,为什么?
对于开发人员来说,c++ 20至少有两个很有前途的特性.
约束和概念
该特性提供了丰富的新语法,使TMP更加结构化,并且其结构可重用. 如果使用得当,它可以使代码更具可读性和更好的文档化.
Ranges
背后的c++程序员 ranges-v3图书馆 提倡将其包含在c++ 20中. 使用Unix类管道语法, 可组合结构, 惰性范围组合子, 还有其他特点, 这个库的目的是给c++一个现代的, 功能看.
std::vector numbers = …;
对于自动号码:号码
视图| std::::变换(abs)
| std::views::filter(is_prime)) {
…
}
//不带范围的选项
For (auto orig_number: numbers) {
自动编号= abs(orig_number);
if (!is_prime(数量)){
continue;
}
…
}
在上面的示例中, transform
and filter
操作在运行时执行,不影响原容器的内容.
Initially, 内定, c++的创造者, 把它命名为“带类的C”,,其动机是支持面向对象编程(OOP). 对OOP概念和设计模式的一般理解是在语言之外的, 因此,在大多数情况下,检查c++程序员的OOP知识并不需要是特定于c++的. 然而,有一些小的特殊性. 一个很常见的面试问题是:
问:动态多态(虚函数)在c++中是如何工作的?
在c++中,动态多态是通过虚函数实现的.
class Musician
{
public:
虚拟void Play() = 0;
};
钢琴家:公共音乐家
{
public:
虚拟void Play() override {
//钢琴演奏逻辑
}
};
class吉他手:公共音乐家
{
public:
void Play() override {
//吉他演奏逻辑
}
};
void AskToPlay(音乐人*音乐人)
{
musician->Play();
}
多态性(源自希腊语 poly ,意为“许多”,以及 morph (意思是“形状”)就是这样 AskToPlay
根据对象的类型以不同的方式运行 musician
指向(它可以是a Pianist
or a Guitarist
). 问题是c++代码必须被编译成一种机器代码,每种代码都有一组固定的指令, 包括,尤其是 AskToPlay
function. 这些指令必须选择在运行时跳转(调用)到哪里 Pianist::Play
or 吉他手:玩
.
编译器最常用和最有效的方法是使用 virtual tables (or vtables ). 虚表是虚函数的地址数组. 每个类都有自己的虚函数表,该类的每个实例都有一个指向它的指针. 在我们的示例中,调用 Play
实际上变成了对一个函数的调用,该函数驻留在写在虚函数表的第一个条目中的地址上 *musician
points to. 如果它被实例化为 Pianist
,则其指向虚表的指针将被设置为 Pianist
我们最终调用了正确的函数.
另一个特性来自于c++支持多重继承的事实,并且两者之间没有正式的区别 interface and a class . 经典的问题 钻石的问题 这是一个好的开始吗.
作为一种多范式编程语言, c++还支持函数式编程(FP), 在c++ 20标准中引入范围之后,这个问题变得更加重要了. 它使c++更接近另一种语言, 从功能代码的表达性和可读性的角度来看,更多的fp友好(或关注fp)语言. 一个启动问题的例子是:
问:lambda函数是如何工作的?
编译器生成一个带有 operator()
用lambda的主体, 并使用成员来存储捕获的lambda变量的副本或引用. Here’s an example .
这和另一个关于 虚函数 要么候选人已经深入了解了内部,知道它是如何运作的, 或者这是一个很好的开始一般性讨论的地方,可以提出一些后续问题,比如 它们通常在什么地方派上用场? and 在lambda函数出现之前,c++编程是什么样的? .
模板元编程是c++支持的另一种范式. 它包含了许多有用的特性, 哪些已经在新标准中引入了更好的替代方案. 新功能包括 constexpr
and concept
让TMP不那么令人费解,但话说回来,仍然有很多产品代码带有谜题. 一个著名的TMP谜题是:
问:如何在编译时计算斐波那契数?
对于较新的标准,它就像拥有一个 constexpr
说明符在递归实现的开头.
conexpr int fibo(int n)
{
if (n <= 1) {
return n;
}
返回fibo(n - 1) + fibo(n - 2);
}
如果没有这个,它将是:
template
struct Fibo
{
static const int value = Fibo::value + Fibo::value;
};
template <>
struct Fibo<1>
{
Static const int value = 1;
};
template <>
struct Fibo<0>
{
静态常量int值= 0;
};
STL,数据结构和算法
标准模板库(STL)是一个内置在c++中的库. 它在c++中已经存在了很长时间,以至于很难将它从核心语言中分离出来. STL为c++开发人员带来了四种特性:
容器——一些基本数据结构的实现,比如 vector
, list
, map
, and unordered_map
算法——一些基本算法的实现,比如 sort
, binary_search
, transform
, and partition
迭代器——用于处理算法的容器的抽象
函子——一种更加定制算法的方法
STL的设计和理念是独特而广泛的. 任何优秀的c++程序员都必须对STL有很强的了解,但问题是,了解到什么程度? 第一层是所谓的 user level knowledge. 这时应聘者至少知道最流行的容器和算法, 如何以及何时使用它们. 接下来的三个问题是检验这一点的经典问题:
问:两者的区别是什么 list
and vector
? c++开发者应该如何选择呢?
Both list
and vector
顺序容器,但基于不同的数据结构. list
是基于双链表的,而 vector
包含一个原始数组.
优势被他们平分了. vector
连续存储数据的优点是什么, 没有内存开销和固定时间索引访问. In contrast, list
具有恒定时间插入和删除在任何位置,并支持功能,如 splice
, merge
, and in-place sort
and reverse
.
As a result, vector
是更受欢迎,大多数时候是正确的选择. list
在某些极端情况下会是更好的选择吗, 就像处理重拷贝对象一样, 这样开发人员就可以让它们保持有序, 或者通过操纵节点连接从一个容器移动到另一个容器.
问:如何从a中移除所有的_42_s vector
? What about a list
?
最简单的方法是遍历容器并使用 erase
成员函数,这两个容器都有. 这在a的情况下是完全有效的 list
; in fact, its remove
成员函数做的差不多.
但这是有原因的 vector
没有这样的 member function. 这种方法效率很低,因为 vector
底层数据结构. 的元素 vector
在内存中是连续的,所以删除一个元素意味着移动 all 后面的元素后退一个位置. 这将导致大量额外的复制.
最好的方法是 erase-remove idiom:
vec.擦除(std::删除(vec).begin(), vec.End (), 42).end());
//初始化- {1,2,42,3,4,42,5,6}
//删除后- {1,2,3,4,5,6, ?, ?, }
//擦除后- {1,2,3,4,5,6}
首先,我们申请 remove
在整个范围内,它不会移除 42
它只是把其他的移到最开始,而且是最有效的方式. 而不是移动 6
向左移动两个位置(如果我们单独擦除就会发生这种情况 42
S),它只会移动 6
一次,差两个位置. With erase
然后,我们擦掉最后的“浪费”.
这个习语在Scott Meyers的传奇故事第32条中有很好的体现 有效的STL:提高你对标准模板库使用的50种具体方法 .
问:c++中有哪些不同类型的迭代器? 哪些可以使用 std::sort
?
STL迭代器有六类: input , output , forward , bidirectional , random access , and contiguous .
迭代器的类别表示它的功能,这些类别是包含的. 这意味着,说的迭代器 set
is bidirectional ’实际上意味着它也是 forward
but neither random access nor contiguous .
类别的差异是由容器的数据结构造成的. A vector
iterator is a random access 1,意思是,不像a set
迭代器,它可以在常数时间内跳转到任意点. std::sort
要求其参数为 random access iterators.
这些可以说是STL中最受欢迎的部分, 所以任何有至少一年经验的c++程序员都见过他们并和他们一起工作过. 但这还不够. 一个高质量的c++开发人员不仅要知道所有的STL容器以及如何使用它们,还要知道它们背后的抽象数据结构, 各有优缺点.
而且,他们不仅要知道大部分的算法,还要知道他们的 渐近的复杂性 . 这意味着c++开发人员应该理解, 用大写字母表示, 标准算法的性能和它们使用的数据结构, 以及两者背后的理论. 问题可以非常具体:
STL容器基础
而不是详尽地测试这些知识, 对一些事实进行审计式的选择——如果需要的话还可以选择更多——应该足以表明候选人是否对这些普遍需要的细节有足够的了解.
一个给定的容器是怎样的.g., vector
] implemented?
[给定容器]的迭代器属于哪一类??
一个给定运算的渐近复杂度是多少.g., remove
在[给定的容器]上?
vector
使用大小标记将元素存储在基础数组中. 当它满的时候, a new, 分配更大的存储空间, 元素是从旧存储中复制的, 然后被释放. 它的迭代器是 random access category. ()的渐近复杂度push_back
) is 平摊常数时间 ,索引访问是 constant time , remove is linear .
list
实现一个双重链表,存储指向头节点和尾节点的指针. 它的迭代器是 bidirectional . insert()的复杂度push_back
and push_front
) and remove (pop_back
and pop_front
) are constant time ,而索引访问是 linear .
set
and map
是否在平衡二叉搜索树上实现, 更具体地说,是红黑树, 这样就保持了元素的排序. 它们的迭代器是 bidirectional as well. 插入、查找(查找)和删除的复杂性是 logarithmic . 使用迭代器删除元素(erase
成员函数)有 平摊常数时间 complexity.
unordered_set
and unordered_map
哈希表的实现是数据结构吗. 通常,它是一个所谓桶的数组,桶就是简单的链表. 为一个元素选择一个桶(在插入或查找期间)取决于它的哈希值和桶的总数. 当容器中的元素太多,平均桶大小大于实现定义的阈值(通常为1.0),则桶的数量增加,并将元素移动到正确的桶中. 这个过程叫做 rehashing ,这使得插入变得复杂 平摊常数时间 . 查找并删除 constant time complexities.
这些是最基本的容器—必须了解上述细节, 但是否每一个细节都经过测试取决于面试官的直觉.
问:给定算法的渐近复杂度是多少??
一些著名的STL算法的渐近复杂性是:
Algorithm Complexity std::sort
O(N log(N)) std:: nth_element
O(N) std::advance
O(1) for 随机存取迭代器 ,否则为0 (N) std:: binary_search
O(log(N)) for 随机存取迭代器 ,否则为0 (N) std:: set_intersection
O(N)
或者,用一个简单的问题来解决所有问题:
问:哪些集装箱可以使用 insertion 操作使迭代器失效?
这个问题的巧妙之处在于它很难被记住, 正确的答案需要对容器的底层数据结构和一些逻辑有广泛的了解.
The insertion 操作可能使上的迭代器失效 vector
, string
, deque
, and unordered_set /地图
(在扩展/重新散列的情况下). For list
, set
, and map
但是,由于它们基于节点的数据结构,情况并非如此.
在许多开发人员面试中,提供一个计算问题并期望候选人开发一种算法来解决它是标准的. 有很多优秀的c++编程平台,比如LeetCode、HackerRank等. 充满了这样的问题.
这些问题很有价值,因为它们可以一次检查很多事情. 大多数面试官会故意从一个不明确的问题陈述开始,以测试候选人解决问题的技巧和提出正确问题的能力. Then, 候选人必须解决问题, 编写代码, and, 最重要的是, 做一个复杂性分析.
在使用STL容器和算法时,擅长后者并拥有强大的数据结构知识足以做出明智的决策, 还有写新的. 有些人可能会更进一步,看看实际的实现. 每个人都会在某些时候使用STL代码,要么是偶然的,要么是在调试时,要么是自愿的. 如果候选人去了那里,并设法弄明白了那些胡言乱语, 这说明了他们的经历, curiosity, 和毅力. For example:
问:哪种排序算法可以 std::sort
implement?
The ISO C++ standard doesn’t specify the algorithm; it only sets the requirement of 渐近的复杂性. 然后,选择取决于实现(例如.e.微软vs .微软. GNU.)
它们中的大多数不会只用一个算法来解决,比如 mergesort or quicksort . 一种常见的方法是将它们混合使用,或者根据大小选择一种. 例如,GCC实现了 introsort ,这是一个混合的 quicksort and heapsort . 它从第一个开始,并在长度小于实现定义的阈值时切换到堆排序.
c++:久经考验,但开发团队必须善于避免它的陷阱
本指南中列出的问题涵盖了c++编程的一些基本方面和一些棘手的方面. 但就像它的可能性一样, 语言隐藏的惊喜是无限的, 这使得在面试中很难涵盖所有关于c++的内容. Therefore, 评估候选人的能力是必要的, skill set, 通过清晰生动地表达自己的想法,对c++有了深刻的理解.
我们希望这些问题能在你寻找全职或兼职的真正高质量的c++专家时起到指导作用. 这些罕见的精英可能很难得到, 但对于您的c++编程团队来说,它们将明显地从其他包中脱颖而出.