24个基本的c++面试问题 *
最好的c++开发人员和工程师可以回答的全面来源的基本问题. 在我们社区的推动下,我们鼓励专家提交问题并提供反馈.
现在就雇佣一个顶级的c++开发人员面试问题
类和结构之间的唯一区别是访问修饰符. Struct members are public by default; class members are private. 当您需要一个具有方法和结构的对象时,当您有一个简单的数据对象时,使用类是一种很好的做法.
下面这行代码将打印出什么?为什么?
#include
Int main(Int argc, char **argv)
{
std::cout << 25u - 50;
return 0;
}
The answer is not -25. 相反,假设32位整数,答案(这将使许多人感到惊讶)是4294967271. Why?
In C++, 如果两个操作数的类型不同, 然后,具有“低类型”的操作数将提升为“高类型”操作数的类型, 使用以下类型层次结构(此处从最高类型到最低类型列出), double, float, 无符号长整型, long int, unsigned int, int (lowest).
所以当两个操作数是,就像我们的例子一样, 25u
(unsigned int)和 50
(int), the 50
被提升为一个无符号整数(i.e., 50u
).
此外,操作的结果将是操作数的类型. 因此,结果 25u - 50u
它本身也是一个无符号整数吗. 所以结果是 -25
当提升为无符号整数时转换为4294967271.
下面代码中的错误是什么,应该如何纠正?
my_struct_t *栏;
/* ... 做的东西,包括设置条指向一个已定义的my_struct_t对象 ... */
Memset (bar, 0, sizeof(bar));
最后一个参数 memset
should be sizeof(*bar)
, not sizeof(bar)
. sizeof(bar)
的大小。 bar
(i.e., the pointer itself)而不是指结构的大小 bar
.
因此,代码可以通过使用 sizeof(*bar)
作为调用的最后一个参数 memset
.
一个犀利的候选人可能会指出使用 *bar
是否会导致解引用错误 bar
还没有分配. 因此,更安全的解决方案是使用 sizeof (my_struct_t)
. 然而,一个更敏锐的候选人必须知道在这种情况下使用 *bar
在通话范围内是否绝对安全 sizeof
, even if bar
还没有初始化,因为 sizeof
是编译时构造吗.
以上代码执行后, i
等于6,但是 j
will equal 5.
理解其中的原因是理解一元增量(++
)及减量(--
)操作符在c++中工作.
当这些运算符 precede 一个变量,变量的值首先被修改和 then 使用修改后的值. 例如,如果我们将上面的代码片段修改为 int j = ++i;
, i
会增加到6和 then j
会被设置为修改后的值,所以两者最终都等于6.
然而,当这些操作符 follow 一个变量,该变量的未修改值被使用和 then 它是递增的或递减的. 这就是为什么,在声明中 int j = i++;
在上面的代码片段中, j
第一个设置为未修改的值 i
(i.e., 5) and then i
加到6.
Assuming buf
是一个有效的指针,什么是问题在下面的代码? 有什么替代方法可以避免这个问题?
size_t sz = buf->size();
while ( --sz >= 0 )
{
/*做某事*/
}
上面代码中的问题是 --sz >= 0
will always 要真实,这样你就永远不会离开 while
循环,因此您可能最终会破坏内存或导致某种类型的内存冲突或出现其他一些程序故障, 取决于你在循环中做什么).
原因是 --sz >= 0
will always 真正的是那种 sz
is size_t
. size_t
只是一种基本无符号整数类型的别名吗. 因此,自 sz
是无符号的,可以吗 never 小于零(因此条件永远不可能为真).
可以避免此问题的替代实现的一个示例是使用 for
循环如下:
for (size_t i = 0; i < sz; i++)
{
/*做某事*/
}
考虑下面两个用于打印向量的代码片段. 一比一有什么优势吗. the other? Explain.
Option 1:
vector vec;
/* ... .. ... */
For (auto itr = vec.begin(); itr != vec.end(); itr++) {
itr->print();
}
Option 2:
vector vec;
/* ... .. ... */
For (auto itr = vec.begin(); itr != vec.end(); ++itr) {
itr->print();
}
尽管这两种选择将完成完全相同的事情, 从性能的角度来看,第二个选项更好. 这是因为自增后操作符(i.e., itr++
)比预增量运算符(i.e., ++itr
). 后自增操作符的底层实现在对元素进行自增之前先复制该元素,然后返回该副本.
That said, 许多编译器会自动优化第一个选项,将其转换为第二个选项.
这个问题测试你对c++模板的理解. 有经验的开发人员会知道这已经是c++ 11 std库(std::is_base_of
)或c++ boost库的一部分(boost:: is_base_of
). 即使是一个只有及格知识的受访者也应该写一些类似的东西, 最有可能涉及到一个助手类:
template
类IsDerivedFromHelper
{
class No { };
class Yes { No no[3]; };
静态是测试(B*);
static No Test( ... );
public:
enum { Is = sizeof(Test(static_cast(0))) == sizeof(Yes) };
};
template
bool IsDerivedFrom() {
return IsDerivedFromHelper::Is;
}
template
struct is_same
{
静态常量bool值= false;
};
template
struct is_same
{
静态常量bool值= true;
};
template
bool IsSameClass() {
return is_same::value;
}
尽管您可以从内联函数内部调用它, 编译器可能不会生成内联代码,因为编译器无法在编译时确定递归深度. 具有良好优化器的编译器可以内联递归调用,直到在编译时固定一定深度(例如三或五个递归调用)。, 并在编译时插入非递归调用,以防在运行时超出实际深度.
此程序将异常终止. throw 32
会开始展开堆栈并销毁A类吗. 类A析构函数将在异常处理期间抛出另一个异常, 哪个会导致程序崩溃. 这个问题是测试开发人员是否有处理异常的经验.
给你上如下的图书馆课:
类{
public:
Something() {
topSecretValue = 42;
}
bool somePublicBool;
int somePublicInt;
std:: string somePublicString;
private:
int topSecretValue;
};
实现一个方法来获取任何给定Something*对象的topSecretValue. 该方法应该是跨平台兼容的,不依赖于sizeof (int, bool, string).
创建另一个类,其中Something的所有成员的顺序相同, 但是有额外的返回值的公共方法. 你的副本Something类应该是这样的:
类SomethingReplica {
public:
int getTopSecretValue() { return topSecretValue; }
bool somePublicBool;
int somePublicInt;
std:: string somePublicString;
private:
int topSecretValue;
};
然后,要获取值:
Int main(Int argc, const char * argv[]) {
Something a;
SomethingReplica* b = reinterpret_cast(&a);
std::cout << b->getTopSecretValue();
}
在最终产品中避免这样的代码是很重要的, 但在处理遗留代码时,这仍然是一种很好的技术, 因为它可用于从库类中提取中间计算值. (注意:如果外部库的对齐方式与您的代码不匹配, 您可以使用#pragma pack来解决这个问题.)
实现一个void函数F,它接受指向两个整数数组的指针(A
and B
) and a size N
as parameters. 然后繁殖 B
where B[i]
是一切的产物吗 A[j]
where j != i
.
For example: If A = {2, 1, 5, 9}
, then B
would be {45, 90, 18, 10}
.
乍一看,这个问题似乎很简单,所以粗心的开发人员可能会这样写:
void F(int* A, int* B, int N) {
int m = 1;
for (int i = 0; i < N; ++i) {
m *= A[i];
}
for (int i = 0; i < N; ++i) {
B[i] = m / A[i];
}
}
这将适用于给定的示例, 但是当你在输入数组a中加入一个0, 程序会因为除以0而崩溃. 正确的答案应该考虑到这种边缘情况,看起来像这样:
void F(int* A, int* B, int N) {
//设置prod为中性的乘法元素
int prod = 1;
for (int i = 0; i < N; ++i) {
//对于元素“i”,将B[i]设置为A[0] * ... * A[i - 1]
B[i] = prod;
//与A[i]相乘,将prod设置为A[0] * ... * A[i]
prod *= A[i];
}
//重置prod并将其用于正确的元素
prod = 1;
for (int i = N - 1; i >= 0; --i) {
//对于元素“i”,将B[i]乘以A[i + 1] * ... * A[N - 1]
B[i] *= prod;
//与A[i]相乘,设置prod为A[i] * ... * A[N - 1]
prod *= A[i];
}
}
上面给出的解决方案的复杂度为O(N)。. 虽然有更简单的解决方案可用(这些解决方案将忽略需要采取 0
考虑到这一点,这种简单是以复杂性为代价的,通常运行速度会慢得多.
虽然完全避免虚拟继承是理想的(你应该知道你的类将如何被使用),但对虚拟继承的工作原理有一个坚实的理解仍然很重要:
所以当你有一个类(类a)它继承了双亲(B和C), 它们都有一个共同的父母(D类), 如下所示:
#include
class D {
public:
void foo() {
std::cout << "Foooooo" << std::endl;
}
};
类C: public D {
};
类B: public D {
};
类A: public B, public C {
};
Int main(Int argc, const char * argv[]) {
A a;
a.foo();
}
如果在这种情况下不使用虚拟继承, 你会在A班拿到两份D:一份来自B,一份来自C. 要解决这个问题,你需要将类C和B的声明改为virtual,如下所示:
类C:虚拟公共D {
};
类B:虚拟public D {
};
下面代码的输出是什么?
#include
Int main(Int argc, const char * argv[]) {
Int a[] = {1,2,3,4,5,6};
std::cout << (1 + 3)[a] - a[0] + (a + 1)[2];
}
上面的命令将输出8,因为:
(1+3)[a]等于a[1+3] == 5
a[0] == 1
(a + 1)[2]等于a[3] == 4
这个问题是测试指针算术知识, 以及指针方括号背后的魔力.
虽然有些人可能会认为这不是一个有价值的问题,因为它似乎只是测试阅读C结构的能力, it’s still important for a candidate to be able to work through it mentally; it’s not an answer they’re expected to know off the top of their head, 而是讨论他们得出的结论以及如何得出的结论.
下面代码的输出是什么?
#include
class Base {
virtual void method() {std::cout << "from Base" << std::endl;}
public:
virtual ~Base() {method();}
void baseMethod() {method();}
};
类A: public Base {
void method() {std::cout << "from A" << std::endl;}
public:
~(){方法();}
};
Int main(void) {
Base* Base = new A;
base->baseMethod();
delete base;
return 0;
}
上面的命令将输出:
from A
from A
from Base
这里需要注意的重要事项是类的销毁顺序和方式 Base
的方法返回到它自己的实现一次 A
已经被摧毁了.
这个循环要执行多少次? 解释你的答案.
Unsigned char half_limit = 150;
for (unsigned char i = 0; i < 2 * half_limit; ++i)
{
// do something;
}
如果你说300,你 would 是正确的 i
已被宣布为 int
. However, since i
被宣布为 unsigned char
,正确答案是 这段代码将导致无限循环.
Here’s why:
The expression 2 * half_limit
会被提升到 int
(based on c++转换规则),其值为300. However, since i
is an unsigned char
, 它由一个8位值表示, 达到255之后, 是否会溢出(因此它会返回0),因此循环将永远进行下去.
Implement foo(int, int)
…
Void foo(int a, int b) {
// whatever
}
并通过模板删除所有其他的:
template void foo(T1 a, T2 b) = delete;
Or without the delete
keyword:
template
void f(T arg1, U arg2);
template <>
Void f(int arg1, int arg2)
{
//...
}
下面的代码有什么问题?
class A
{
public:
A() {}
~A(){}
};
B类:公共的
{
public:
B():A(){}
~B(){}
};
int main(void)
{
A* a = new B();
delete a;
}
该行为未定义,因为 A
的析构函数不是虚函数. From the spec:
( C++11 §5.3.5/3),如果要删除的对象的静态类型与其动态类型不同, 静态类型应该是要删除的对象的动态类型的基类,并且静态类型应该具有虚析构函数或者行为未定义.
指定其变量和函数的生命周期和作用域的类称为存储类.
c++中支持以下存储类: auto
, static
, register
, extern
, and mutable
.
但是请注意,关键字 register
was 在c++ 11中已弃用. 在c++ 17中,它是 删除并保留以备将来使用.
Using an extern "C"
declaration:
//C code
Void函数(int i)
{
//code
}
输出int(int i)
{
//code
}
//C++ code
extern "C"{
Void函数(int i);
Void print(int i);
}
Void myfunc(int i)
{
func(i);
print(i);
}
下面程序的输出是什么?
#include
struct A
{
int data[2];
A(int x, int y): data{x, y} {}
虚拟void f() {}
};
Int main(Int argc, char **argv)
{
A a(22, 33);
Int *arr = (Int *) &a;
std::cout <
的实例 struct A
被视为整数值的数组. 在32位体系结构上,输出将是33,在64位体系结构上,输出将是22. 这是因为存在虚方法 f()
在结构体中,使编译器插入一个指向虚表的VPTR指针(指向类或结构体的虚函数的指针表)。. 在32位体系结构上,vptr占用struct实例的4个字节,其余的是数据数组, so arr[2]
表示对数据数组第二个元素的访问,该元素的值为33. 在64位体系结构中,vptr占用8字节 arr[2]
表示对数据数组第一个元素的访问,该数组包含22.
这个问题是测试虚函数内部的知识, 以及c++ 11特有的语法知识, 因为的构造函数 A
使用c++ 11标准的扩展初始化列表.
Compiled with:
g++ question_vptr.CPP -m32 -std=c++
g++ question_vptr.cpp -std=c++11
A const
成员函数是不允许修改调用它的对象的成员的函数. A static
成员函数是指不能为特定对象调用的函数.
Thus, the const
modifier for a static
成员函数是没有意义的,因为没有对象与调用相关联.
对这个原因的更详细的解释来自C编程语言. 在C语言中,没有类和成员函数,所以所有的函数都是全局的. 成员函数调用被转换为全局函数调用. 考虑这样一个成员函数:
Void foo(int i);
像这样的呼叫:
obj.foo(10);
被翻译成这样:
foo(&obj, 10);
这意味着成员函数 foo
有一个隐藏的第一个参数类型 T*
:
void foo(T* const this, int i);
如果成员函数是const, this
is of type const * const this
:
void foo(const * const this, int i);
静态成员函数没有这样的隐藏参数,所以没有 this
pointer to be const
or not.
The volatile
关键字通知编译器变量可能在编译器不知道的情况下发生变化. 变量声明为 volatile
不会被编译器缓存,因此总是从内存中读取.
The mutable
关键字可用于类成员变量. 可变变量允许在类的const成员函数中进行更改.
这意味着我们不能使用多重继承和分层继承来创建混合继承.
让我们考虑一个简单的例子. 一所大学有附属于它的人. 有些是学生,有些是教员,有些是管理人员,等等. 因此,一个简单的继承方案可能有不同类型的人担任不同的角色, 所有这些都继承自一个共同的“Person”类. Person类可以定义一个抽象 getRole()
方法,然后由其子类重写以返回正确的角色类型.
但是如果我们想要模拟助教的角色会发生什么呢?? 通常,助教是 both a grad student and a faculty member. 这就产生了多重继承的经典菱形问题,以及由此产生的关于TA的歧义 getRole()
method:
(注意上面继承图的菱形,因此得名.)
Which getRole()
实现应该继承TA? 教员或研究生的证言? 简单的答案可能是让TA类覆盖 getRole()
方法并返回名为“TA”的新定义角色。. 但这个答案也是不完美的,因为它会掩盖一个事实,即助教是, in fact, 既是教员也是研究生.
面试不仅仅是棘手的技术问题, 所以这些只是作为一个指南. 并不是每一个值得雇佣的“A”候选人都能回答所有的问题, 回答所有问题也不能保证成为A级考生. 一天结束的时候, 招聘仍然是一门艺术,一门科学,需要大量的工作.
Why Toptal
提出面试问题
提交的问题和答案将被审查和编辑, 并可能会或可能不会选择张贴, 由Toptal全权决定, LLC.
寻找c++开发人员?
Looking for C++ Developers? 查看Toptal的c++开发人员.
Julie Wetherbee
自由c++开发人员
Julie在为各种规模的企业构建软件应用程序和领导工程团队方面拥有超过20年的经验. 她是Java方面的专家, JavaScript, C, C++, and Perl, 并且熟悉许多流行的框架. 最近,Julie为沃尔玛设计并实现了一个大规模的Oracle数据库分片解决方案.com.
Show MoreMike Hutton
自由c++开发人员
Mike是一名软件架构师和开发人员,拥有超过25年的大型关键任务系统开发经验. 他目前专注于Java和J2EE开发, c++和C开发, 以及物联网的嵌入式系统. 此外,他是国际公认的彩票博彩系统领域的专家. 在过去的16年里,Mike一直在为不同地域的团队提供解决方案.
Show MoreToptal连接 Top 3% 世界各地的自由职业人才.
加入Toptal社区.