投了半个月的简历,在51终于联系到一个cpp/lua的面试,这段时间能找到愿意给机会面试的公司还蛮难得的。今天刚面完,公司名就先码了。
这次面试对我来说算比较新鲜,之前接过的面试都没有发几张试卷做题的环节。最早面过一次也只是口头提了要求在白纸上写代码。
而面试环节中,讨论的求职理念、愿景、短期目标这些话题,很值得深思。
笔试部分
先说笔试部分。笔试分五个部分,简答、叙述、逻辑、算法、分析。这里挑几个例子。
简答题
简单介绍你的游戏经历。
玩过哪些网络游戏?游戏时长?充值金额?
这类题目的主要目的都是考察是否对申请的职位有基本的了解。如果从未玩过某一类型的游戏,而公司的产品正好是你没玩过的这类游戏,那么你在工作中就可能会遇到沟通上的问题。无论是面对面问答还是笔试,这类问题的回答策略实事求是即可。
基础题
定义数组
int a[100]
,求sizeof(a)
和strlen(a)
的值分别是什么。
sizeof(a)
返回的是数组大小,即400
。我没注意数据类型是int
,答成了100
。实际上int
的大小也不一定是4字节,我没记错的话按照标准文档的定义是不小于2字节,例如Win16 平台。现代32-bits/64-bits的体系结构基本能保证32-bits以上。
strlen(a)
则是一个坑,按照题干中描述的int a[100]
的写法,数组a
是未初始化的,数组内容依照标准规定是undefined behavior。strlen
函数在遇到NUL
的时候返回,这是典型的数组越界访问情形。而越界访问也是典型的undefined behavior。所以这题的正确答案应该是undefined behavior。
简述 new,delete,new[],delete[] 的联系和区别。
说到new
可能是C++里最重要的内容之一了,new
表达式有两种重要的形式
- new (placement) type initializer
- new type initializer
第一种形式称之为placement new,在一片指定的内存上构造对象。第二种形式就是大家熟知的常规用法了。举个栗子。
// 使用placement new必须引入这个标准头文件
#include <new>
#include <cassert>
struct Student {
int id;
char name[16];
};
int main() {
// example 1
// 常规 new 表达式
auto student = new Student;
// example 2
// placement new,在一片已经分配的内存上构造对象
auto mem = new char[sizeof(Student)];
auto student = new(mem) Student;
assert(static_cast<void*>(mem) == static_cast<void*>(student));
return 0;
}
placement new有很多细节可以在文档查阅到,放个链接在这里。cpp reference - new expression。
题目问 new
、new[]
、delete
、delete[]
之间的关系,其实是摆明车马考new
和delete
配对、new[]
和delete[]
配对这个知识点了。这里要记住new
是operator new()
,而new[]
是operator new[]()
,这是两个不同的操作符。这里引用cppreference上对new数组的说明来解释下为什么new[]
出来的内容不能用delete
去删除。
数组的分配中可能带有一个未指明的开销(overhead),且两次调用 new 的这个开销可能不同,除非选择的分配函数是标准非分配形式。new 表达式所返回的指针等于分配函数所返回的指针加上该值。许多实现使用数组开销存储数组中的对象数量,它为 [delete] 表达式所用,以进行正确数量的析构函数调用。
此外诸如重载new
运算符之类的技巧这里不作赘述,了解了placement new基本上重载new
运算符就不是什么大问题了。上面的链接可以查到如何调用自定义的new
运算符。
逻辑题
有A、B、C、D四个人,在夜里过一座桥,分别耗时1、2、5、8分钟,只有一支手电,桥同时只能走两个人。如何安排使四个人过桥时间最短?
我不确定这题做对了没。我的答案是让A走三个来回,把B、C、D都带过去。合计就是2+1+5+1+8一共17分钟。
分析题
下面的代码输出是?
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "A::A()" << endl;
init();
}
virtual ~A() {
cout << "A::~A()" << endl;
}
virtual void init() {
cout << "A::init()" << endl;
}
virtual void method() {
cout << "A::method()" << endl;
}
};
class B: public A {
public:
B(): A() {
cout << "B::B()" << endl;
}
~B() {
cout << "B::~B()" << endl;
}
void init() {
cout << "B::init()" << endl;
}
void method() {
cout << "B::method()" << endl;
}
};
int main() {
A* a = new B();
a->method();
delete a;
return 0;
}
一行一行开始分析,先看构造的过程。
B():A()
显然调用了父类构造函数,按顺序应该是父类构造完毕再构造子类,在父类A::A()
里调用了init();
,init
是一个虚函数......所以调用的还是A::init
。注意,在构造和析构函数中调用虚函数,调用的并不是派生类的覆盖函数,而是当前类中的最终覆盖函数。
这里我答题的时候做错了。
当从构造函数或从析构函数中直接或间接调用虚函数(包括在类的非静态数据成员的构造或析构期间,例如在成员初始化器列表中),且对其实施调用的对象是正在构造或析构中的对象时,所调用的函数是构造函数或析构函数的类中的最终覆盖函数,而非进一步的派生类中的覆盖函数。 换言之,在构造和析构期间,进一步的派生类并不存在。
所以A* a = new B();
的输出是:
A::A()
A::init()
B::B()
再看a->method();
,A::method
是一个虚函数,在B类重写了,所以这里调用的是B::method
。
输出是:
B::method()
最后是delete a;
析构函数从子类开始按继承链往回析构,先析构B再析构A。输出是:
B::~B()
A::~A()
考虑情境,用C++写出实现代码。
情境:猫大叫一声,所有老鼠开始逃跑,主人被惊醒。
要求:
- 具有联动性,老鼠和主人的行为是被动的
- 考虑扩展性,猫的叫声可能引发其他联动效应
这题实际写的代码会很长,就不贴了。实际面试中也只是写了一点pseudo code大概说一下思路。
算法题
已知前序遍历结果abcdefgh,中序遍历cbdaegfh,画图表示这棵树并写出后序次序。
经典数据结构题,根据遍历还原二叉树,虽然说基础但我没答上来。这里复习下。
前序遍历的顺序是根左右,中序遍历的顺序是左根右,后序遍历是左右根。注意这个前中后是指根在遍历中的次序,最先遍历根的是前序,最后遍历根的是后序。人肉还原二叉树的过程基本是这样。
从前序遍历结果分析出根节点是a,中序判断出左子树三个节点cbd,b是左子树的根。然后看右子树的前序是efgh,右子树根是e,中序看到e前面没有节点,所以e的左子树是空的,右子树是gfh。前序是fgh,f是e的右子树的根,左节点g,右节点h。这样就人肉重建完成了。
写一个函数,找出整数数组中第二大的数。
我的代码如下。
#include <iostream>
#include <vector>
using namespace std;
int find_second(const vector<int>& container) {
int max=0;
int second=0;
for(const auto& i: container) {
if(i>max) {
second=max;
max=i;
}
if(i<max && i>second) {
second = i;
}
}
return second;
}
int main() {
cout << "second is " << find_second(vector<int> {1,2,3,4,5,6,7,9,10}) << endl;
}
当然你要刷leetcode这样是不行的,因为是笔试,拿笔写代码很蛋疼,所以尽可能简略了一下。边上写明了假设输入数字都大于0,长度不足时返回0。这类边界情况注意说明下你注意到了即可。大概。
面试部分
要不然你们等我下一篇博客补充吧?