简单介绍下面试的前置状况。ios
面试的公司是鲸鱼游戏,职位是后端开发工程师,开发语言C++。面试
这篇博文主要是为了记录面试中发现的自身不足。算法
此次面试里,由于面试约得比较匆忙,因此基本没作任何准备。讲道理的说我是有点盲目自信了,毕竟C/C++是个人第一语言来着,原本觉得考察语言的部分不会有什么问题,但没想到由于紧张而错漏百出。typescript
那么接下来就直接进入正题,如下是对面试中遇到的问题从新思考后的回答和想法。数据库
下面面试官的提问并不是原话,有通过脑补润色。编程
面试官:讲讲面向对象,继承,还有多态。咱们都知道程序设计有两种常见的范式,面向过程和面向对象,讲讲面向对象给咱们带来了什么好处?后端
实话说第一问就已经有点出乎意料,但想一想其实仍是在乎料之中。初级职位更注重于基础概念和技能,中高级职位可能会在数据结构和并发一类的问题上更深刻。api
答:抽象,归类blabla...易于维护blabla...数组
全错。安全
如今回忆起来,面试官想问的其实只有一点,就是那句封装。
封装是面向对象的核心概念之一。
封装使代码成为一个黑箱,让咱们没必要关注它的实现,而是关注它的行为和接口。
这产生了面向接口编程的概念,咱们再也不关注封装后的对象内部的逻辑,咱们给封装后的对象以输入,而后从封装后的对象里取出数据。
封装并不仅是一系列接口的集合,更包含了数据和状态,它就是一个微型化的服务,调用者告诉它去作什么事,而不关心它怎么作。
面试官:讲讲继承。
我:代码复用,blabla......
代码复用,这是核心。
代码复用是继承最主要的做用,你们都知道。面试官并无在这方面继续深刻,因此能答出代码复用其实已经差很少了。
除非再抠上语言相关的语法细节:多继承和单继承。
C++ 采用了多继承模型,即一个子类能够有多个父类。
Father ------| |====> child Mother ------|
多继承能够容许一些特殊的编程范式。好比说mixin
模式。可是多继承也存在其固有的复杂性,主要表如今运行时多态上。
举几个多继承上常见的问题。
典型场景以下
class ParentA { public: void func(){} }; class ParentB { public: void func(){} }; class Child: public ParentA,ParentB {}; int main() { Child c; c.func(); // error return 0; }
解决办法也很简单
int main() { Child c; c.ParentA::func(); return 0; }
之因此若是不调用 func
就不会出错,是由于 func
在编译后的ABI导出的名字并无产生冲突。但若是主动调用了func
,编译器则须要插入一个函数调用,但这里的func
语义倒是不明确的,因此编译阶段就会报告错误。
dynamic_cast
会改变指针dynamic_cast
是基于RTTI的运行时类型安全的标准类型转换,dynamic_cast
自己是一个关键字,这里就说一说dynamic_cast
的行为和多继承。
多继承下的dynamic_cast
会修改指针绝非危言耸听。事实上只要稍做思考就能得出这样的结论:多继承下的内存布局应该是什么样子的?
v Pointer to Child v Pointer to ParentB v Pointer to ParentA | ParentA | ParentB | Child | [-----------====================>>>>>>>>>>>>>>>>>]
C++ 鼓吹Zero cost abstraction
也不是一天两天的事情了,成果如何不予置评,但显然,专门为多继承下的指针附加类型信息,以容许ParentB*
类型的指针指向的地址和Child*
相同是不可能的。
遑论C++标准里根本没地址
这回事儿了,指针指向的是啥玩意儿都有可能。
单继承就简单得多,只容许一个父类存在,根据语言设计也可能容许实现多个接口。好比说Java
和C#
。以我比较熟悉的 Rust
为例(暂不提继承,由于Rust
就没继承这码事儿,全是Trait
),一个struct
能够实现多个Trait
,而后以Trait object
来实现对象多态。
单继承更可能是在多态、重载、接口等方面的取舍,就不细谈了。
面试官:知道多态吗?多态有什么好处?
答:多态就是...blabla...不去关注子类细节,归类成xxx......blabla
多态算是面向对象基本概念之一了。
多态最基本的解释就是同一个接口的不一样实现,但我理解中的多态解释则更趋向于类型擦除,即我不在意你是什么黑人、白人、黄种人、香蕉人,我只要你能作到某件事。本质上来讲,多态的主要做用就是擦除细节。
举个例子,我打算去面试一家公司,面试官想要的是什么呢?他想要的是能干活的人。
class Worker { public: const int declarePay; const int declareEfficiency; BOOL testWorkEfficiency(SomeShit); virtual ~Worker()=0; }; class Company { public: BOOL hire(Worker) { ... } }
面试者多是HardWorker
,FxxkWorker
都是Worker
实例,但他们也同时是Human
,多是Wife
,多是Husband
,也多是Father
、Mother
,可是这些咱们都不关心。
咱们不可能为每一个People某某某
各自定义一个BOOL hirePeople某某某() {}
,咱们关注的是工做能力,因此咱们要在类型里擦除掉这些无关的细节,保留关注的部分。
多态作的就是这样的一件事:我不在意你是谁,我在意你是否是能干好这件事的人。
这么说其实有些脱离主题了,由于这是面向接口编程的思想,而不是对多态的学术解释,但这确实就是我对多态的理解,它的主要做用就是隐藏差别,进而发展为擦除细节。
个人回答其实根本没到点上,也没Get到面试官的point,因此面试官很快就换了下一个问题。
面试官:虚函数的做用是什么?
答:啊?实现多态啊?...
能够说是最差的回答。
面试中没有反应过来问的啥,知道被拒绝了才忽然明白。
o( ̄ヘ ̄o#)
这已经问到语言细节了,因此我们就从语言出发来说。
首先虚函数是什么?虚函数是C++实现多态的手段,这么答没错,学过C++都知道。不过虚函数不只仅是这一点。
咱先从这一点讲起。
虚函数经过一个叫虚函数表的东西来实现多态,这个虚函数表是实现定义的,标准没有对vtable
作什么规定,好比说必须放在类指针的先后几个字节处啊什么的......不存在的。因此也不谈虚表是怎么实现的,这已是具体到平台和编译器上的差异了,要抠这个的话必须去读编译器和平台相关的各类文档了,PE格式啊DLL啊SharedObject啊什么的。
若是问起来的话......嗯......这个职位应该很厉害。
因此我就跳过了。
直接给个虚函数的实例,真的没什么好说的。
#include <iostream> class ParentA { public: virtual vFunc() { std::cout << "ParentA" << std::endl; } }; class Child: public ParentA { public: virtual vFunc() override { std::cout << "Child" << std::endl; // 顺便写调用父类的 ParentA::vFunc(); } };
C++虚函数的另外一个重要用途就是虚析构函数。
由于......C++对象模型中,析构函数的位置十分尴尬。
构造函数也就算了,不管如何也要显式调用一次。
析构函数则由于多态的存在而十分尴尬:给你一个父类指针列表,你显然不能一个一个检查这些指针指向是什么对象,而后再转回去,最后才 delete
它。
光是听起来就麻烦得要死,更别提有时候根本作不到。C++脆弱的RTTI
和基本不存在的Reflection
但是出了名的。
C++对这个问题的解决办法就是虚析构函数。
和通常的虚函数不一样,通常的虚函数一旦被override
,除非你主动调用指定父类的虚方法,不然调用的必然是继承链最后一个override
了这个虚方法的类的虚方法实现。
析构函数的话就稳了,它会链式的调用继承链上每一个类的析构方法,多继承的状况下则是按照继承的顺序调用析构方法。
不用主动写ParentA::~ParentA()
,是否是特别爽?
还行,这就是个语法糖。
最后是纯虚函数。
其实这玩意儿我更愿意称他为接口。
本质上来讲,纯虚函数规定了一个方法,这个方法接收固定的输入,并保证提供一个输出,相应的可能还有异常声明,来讲明这个方法可能抛出的异常。
怎么样,看起来眼熟不?
还没完,纯虚方法没有实现(你开心的话也能够写个实现),强制要求子类必须实现,而定义了纯虚方法的类被称之为抽象类。
我想就算是叫它接口类它也不会反对的吧。
纯虚函数能够类比于C#
的interface
,或者typescript
的interface
,总之就是各类语言的interface
。这些interface
在具体的规定上可能有所差别,好比说不容许写数据成员啦,数据成员写了不算在实现interface
的类上还要再声明一次啦,interface
的方法可不能够有个默认实现啦,这些都是细节。
还记得上面我说多态吗?多态的目的是擦除类型细节,因此这些长得各不相同百花齐放的interface
作的事情其实都是一回事:你能作啥,那么你是啥。
这里再说个细节,纯虚函数做为析构函数的时候,析构函数应该有个实现......
听起来挺奇怪的?不写纯虚析构函数实现的话,会报个连接错误...至于为何要这么作,其中的取舍就不得而知了。
C++的纯虚函数和抽象类很灵活,没有其余语言interface
种种限制,若是要追问纯虚函数
when? where? why?
那就要看到具体场景了,C++这些灵活的特性一不当心就会变成滥用,反正这么问我应该也就答interface
、mixin
以及其余具体需求的场景这样子了。
Mixin
模式在Python
里比较常见,不过C++也并非没有。经过定义纯虚析构函数,来给一个对象混入特定功能而又不容许本身被独立构建,算是个常见的范式。
举个例子,引用计数,若是发现本身引用归零了就释放资源,线程安全之类的问题先无论,仅仅是展现这个范式。
#include <iostream> class RcMixin { private: using deleter = ()->void; int *_rc = nullptr; deleter resDeleter; public: RcMixin(deleter resDeleter):resDeleter(resDeleter) { *_rc+=1; // 线程安全就先放一边 } RcMixin(const RcMixin& other) { resDeleter = other.resDeleter; *_rc+=1; } virtual ~RcMixin() = 0 { *_rc-=1; if(*_rc <= 0) { resDeleter(); } } }; // 虽然是个RcMixin可是外界并不须要知道它是RcMixin class SomeShit: private RcMixin { private: int* res = nullptr; public: SomeShit() : RcMixin([&this]() { std::cout << "" << std::endl; delete this.res; }) { res=new int(10); } virtual ~SomeShit() {} }; int main() { SomeShit a; auto b = a; auto c = b; }
代码没测过,反正大概就是这种感受,将某些功能混入一个现存的类,而不须要作太多的工做。在C++里没那么方便,强类型下的Mixin须要不少变通技巧才能愉快地混入新功能,而鸭子类型Duck typing
的语言则舒爽不少,固然,最好的仍是具备完善 Reflection
和 Attribute
支持的语言,彻底避免了对Mixin
类型的构造和须要利用的数据的绑定一类的没必要要的关注。
一样是 virtual
关键字,虚继承和虚函数关系就不怎么大了。
虚继承面对的问题是多继承时,多个父类继承自同一个基类这一问题。
听起来是否是有点奇怪?这些父类继承自同一个基类会有什么问题?
事实上,这个问题取决于写出多继承代码的人,也取决于这多个父类是否有对多继承方面作过考虑。
举个简单的例子,ParentA
和ParentB
都继承自DataA
,ParentA
修改了DataA
的数据,但ParentB
不知道。若是ParentB
须要根据DataA
的某些数据进行操做——很遗憾,这个行为可能与预期的不一样。
之因此引入虚继承,是为了解决要不要共享同一个基类实例的问题,选择虚继承,则选择共享基类实例。
共享基类实例的优点是,多个父类的功能能够无缝结合。ParentA
和ParentB
能够共享基类定义的Mutex
等状态资源——固然,前提是设计父类的人有过这方面的考虑。
否则的话,不共享基类实例是个保守但更安全,不易出现歧义的选择。
面试官:咱们聊一下数据结构方面吧.....讲一下数组和链表?能够从访问和删除两方面来讲。
答:数组容许随机访问,只须要一步就能找到对应元素,而链表须要......blabla,数组删除元素若是须要移动后续元素的话,会产生复制操做性能损失,链表只须要修改几个指针...blabla。
实际上答到这里我已经不知道本身在说啥了。
数组和链表的区别仍是挺大的,我应该算是Get到了几个点?下面是从新整理了语言后的回答。
数组和链表二者都是线性数据结构,表现上都是一条有头有尾的有序序列,可是储存方式上有区别。
数组的储存方式是一端连续的内存空间,索引只须要进行一次指针运算便可得到目标元素的位置,也能够理解为访问时间始终是O(1)
。
PS: 还能写出 0[array] 这样的骚写法,不怕被打死的话。
链表的内存布局则是分散的,一般的链表实现每每是插入元素时动态分配一个元素的空间,而删除的时候再释放,久而久之对内存是不友好的,容易产生内存碎片,致使分配较大空间时没法寻得足够长的连续内存片断而形成分配失败。
......固然,是长期才会产生的问题,并且是切实存在的问题。
对于数组来讲的话,能够理解成标准库的 std::array
,也能够理解成原始数组,但不变的是索引方式始终是O(1)
复杂度,并且支持随机访问迭代器。
对于链表来讲,不考虑优化后的变体,索引方式在本质上都是顺序访问迭代器——指针也算是概念上的迭代器。因此对于链表,访问时间的复杂度最坏状况应该是O(n)
,n
是链表长度。不用说,索引性能天然是不如数组的。
数组删除元素实际上是比较烦的,复杂度应该是O(n)
,n
是数组长度减去删除元素在数组中的位置。最麻烦的是万一数组很长,那么复制元素到上一个位置将会是噩梦。
固然也不是不能优化......把移动的操做推迟到插入新元素的时候就行了,用一个占位符表示这里已经被删除,同时记录前面有多少个元素被删除。这样一来索引性能会降低(由于要找到上一个被删除的元素,而后更新索引位置,直到找到正确的元素),删除性能提升(只要找到上一个被删除的元素而后记录本身做为被删除元素的位置就好),总体实现的复杂度提高,索引删除插入都要另外编写实现,感受得不偿失。
链表删除元素很简单,索引到须要删除的元素的时间复杂度是O(n)
,删除操做的时间复杂度是O(1)
,并且实现简单。
好吧,这个问题面试官没问到。
链表和数组结合一下能解决一部份内存碎片的问题,基本思路的话......咱预先分配100个元素,若是插入的元素超过了100个,咱再分配100个元素的空间,而后索引的时候再去找第二个池?
这个思路术语叫什么记不起来了。
猜一猜面试官到底想问些什么?
std::array
和std::list
。因此问的是啥呢...?提供的保证和implement specified
还有undefined behavior
吗?STL如今尚未concept
,可是早早就有了SFINAE
和enable_if
之类的东西,constexpr if
更是极大地强化了编译期元编程方面的能力。若是是问标准模板库方面的东西的话,我以为问标准库线程安全啊,迭代器算法之类的东西要合适得多。因此......大概也不是想问这个。面试官:讲一下数据库的索引有什么做用。
我:懵逼......
还行,直接懵了。
由于彻底没搞明白面试官的意图:索引指的是啥?面试官是想问数据库索引的方式吗?B+树该怎么实现?
回来路上我考虑了一下,这几方面可能能够做为回答的方向。
数据库索引的常见实现方式是 B+ 树,我数据结构学的很差,只知道 B+ 树是个很厉害的数据结构.....因此博文写到这里,不得不开始查资料了。
B+ 树是一种树数据结构,一般用于数据库和操做系统的文件系统中。B+ 树的特色是可以保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入,这与二叉树刚好相反。
若是问起B+树实现,或者让手写个B+树的话,我也只能望而兴叹了。
对于数据库的实现我了解很少。
大概就是创建个独立的 B+ 树索引......吧?
真想不出了...
面试官:说下主键的做用。
我:emmmmmm.....
到这里我基本已经萌的不行了。(无错字)
心里OS:我是谁?我在哪?我要干什么?
甚至连zhujian都听成了zujian
被面试官提醒了一下
面试官B:就是那个 key
我也没反应过来......
主键的话,具备惟一性的索引?
emmmmm,否则还有什么做用呢......
看来数据库必须下功夫学一学才行啊......
面试官:十动然拒。
我:理解理解,谢谢谢谢。
还行,回顾完整个面试流程,除了C++部分多是由于发挥失常以外,数据库方面的确是没有下够功夫,以致于连索引和PrimaryKey这两问都在持续懵逼。
并且实话说面试,确实有技巧这回事......
面试官提的问题也存在着范式——网络上面试真题什么的,看起来像是玩笑,但面试官提出这些问题的时候倒是认真的。
尽管......这种
聊聊xxxx(某技术/概念/工具),xxx的做用是什么
的提问确实让人不容易抓住重点......
考察基础的角度来讲,现场白板写一个程序,而后再深刻聊聊这么写的用意,有没有优化方案,考察对语言的理解和api设计、代码架构能力,比单纯的说说xxx,问xxx做用要实际的多。固然并非说这么问很差,这些概念的掌握也是很是重要的基础,并且能有效考察面试者语言组织能力和对这方面知识的掌握程度。
惟一很差的就是,面试者和面试官聊的过程就像是用黑话交流同样......
不说了,学这黑话去......