GameHollywood 面试笔记

GameHollywood 面试笔记

Intro

面试的职位是 C++开发工程师,主要聊的仍是C++。在过程当中自我感受面得还行,至少没上次那么蠢。前端

聊的内容主要集中在STL和线程安全、资源管理的层面。面试

惯例的,填完面试信息表并简历一块儿上交,而后等面试官来客套完,就开始聊技术了。算法

注意,面试官的提问并不是原话,有修饰和脑补。编程

0. 预热:你用哪一个版本的C++?

客套话什么的就略了。后端

面试官:...行,那咱们就聊聊C++吧。你经常使用哪一个版本的C++?数组

我:我比较经常使用的是C++11。安全

C++版本这个问题面试里应该很少见,不过做为引入的话题还行,标准之神会瞑目的。数据结构

对于C++版本这个词,很大几率上你们说的应该就是C++标准委员会WG21制定的C++标准了,最新版本的标准文档是C++17定稿N4659,制定中的C++20标准文档能够访问WG21/docs/papers/2018查阅。多线程

须要注意的是,若是答成了我用VC6之类的骚话,很大几率会留下很差的映像——或者对方也是忠实的VC6神教教徒的话,达成共识也说不定。架构

闲话少叙。

1. 起手式:std::shared_ptr

面试官:说说std::shared_ptr是怎么实现的?通常怎么去使用它?

答:shared_ptr是经过引用计数实现的,它能够做为容器元素,在程序里传递blabal.....并且shared_ptr不是线程安全的,它不能跨线程传递,要额外作一层包装blabla......

正巧最近有想写一篇智能指针相关的博客,面试官的第一问就提到了。

说到智能指针,就必须提一下RAII了。

1.1 异常安全和RAII

std::shared_ptr和其余智能指针类型都在<memory>头文件里定义,主要的做用是实现自动化的资源管理,基于RAII的理念设计和实现。

RAII指的是获取资源即初始化,英文全写是Resource Acquisition Is Initialization,属于一种面向对象编程语言中常见的惯用法。

它的思路是这样子的:初始化即获取资源,离开做用域就自动销毁。

RAII解决的问题是,当异常发生时,如何确保资源释放。这是个异常安全的问题。

常见的非RAII风格代码里,若是要确保资源被正确释放,就要用try {} catch() {} finally {}块捕获异常,而后执行资源释放的代码,再将异常从新抛出。

而RAII的理念是,让资源的生命周期和一个栈上的对象严格绑定,确保栈上对象被析构的时候,资源也就被一同释放了。

在C++中,有大量的代码都是以RAII风格进行设计的,其中智能指针也是。

1.2 std::shared_ptr的实现

引用计数,大概了解过智能指针的人都能回答得出来。

虽说实现方式并无规定只能是引用计数,但实际上你们都是这么写的,万一哪天有个GC实现的std::shared_ptr也别太震惊。

实现思路也挺简单。

全部指向同一实例的std::shared_ptr应当持有同一个引用计数,来保持全部std::shared_ptr计数同步,因此它们共同拥有一个计数器指针long *p

在复制时,shared_ptr管理的对象指针和引用计数器指针被同时复制,而后引用计数器指针保存的引用计数+1——销毁同理,减小引用,直到删除。

1.3 std::shared_ptrCopyAssignable

std::shared_ptr知足CopyContructiableCopyAssignableLessThanComparable这些标准库的具名要求,所以能够做为STL容器的元素。

顺便一提 Concept 有很大可能出如今 C++20 标准里。

1.4 线程安全性

std::shared_ptr不是线程安全的,否则不知足C++对Zero Cost Abstraction的要(吹)求(逼)。

依据官方说法,多线程访问不一样的std::shared_ptr实例是没问题的(大多容器也是);多线程访问同一个std::shared_ptr实例,可是只调用const方法,那么也是没问题的(多线程读);多线程访问同一个std::shared_ptr实例,调用非const方法,那么会产生数据竞争(多线程读写)。

若是但愿在线程间传递 std::shared_ptr 得靠 STL 提供的原子操做库std::atomic

std::atomic能够快速帮助包装一个线程安全的对象或者指针,不过这东西对std::shared_ptr的特化是目前还在制定的C++20标准的一部分,因此能不用则不用,直到标准制定完成稳定,而且各编译器支持完善后再行考虑。

除此以外,若是确实有这方面的考虑,引入boost是一个不错的选择。

不管如何,跨线程使用std::shared_ptr我不怎么支持。

跨线程传递std::shared_ptr自己就是个很是危险的行为。std::shared_ptr做为标准库的一员,背负了C++的历史包袱,它随时可能被取出裸指针使用,或者意外复制了一次或几回,而这些对线程安全几乎就是意味着做死的行为却没有任何管束。

1.5 其余智能指针

  • std::auto_ptr
  • std::weak_ptr
  • std::unique_ptr

其中std::auto_ptr已经被扫进历史的垃圾堆了,做为替代者,std::unique_ptr有更明确的语义和更高的可定制性。

std::weak_ptr是对于std::shared_ptr的补充,对于但愿使用std::shared_ptr做为使用了指针的数据结构之间的链接方式,又不但愿产生循环引用恶劣状况的一个解决方案。弱指针的存在不影响引用计数工做。

最后是std::unique_ptr,它的语义是明确惟一持有某一资源,依照约定,被std::unique_ptr持有的资源不该该再有第二人持有,std::unique_ptr是惟一访问该资源的入口。

这些智能指针都有一个共同点:为了兼容C代码,因此它们随时能够被取出裸指针而不影响自身的工做,但这种使用方式形成的一切后果自负。

2. std::vector

面试官:...知道std::vector吧?讲讲它是怎么实现的。

我:vector保存了一个必定长度的buffer,当插入时能够避免插入一次就分配一次空间blabla...当插入长度超过了buffer长度,buffer会依照内部算法来从新分配一次内存,扩张长度。

回答不全对。其实面试官以后又强调了一次,但面试时没有听出来。

面试官:那以前分配的buffer呢?

我:以前分配的buffer先复制到新的buffer里,而后旧buffer会被释放。

这里对于释放旧buffer的说法实际上是有问题的,能够具体看看下面。

2.1 内存布局

std::vector内存布局是连续的,这一点除了几乎每一个人都有所了解以外(...),标准给出的要求也能够看出点端倪。

26.3.11.1 Class template vector overview

A vector is a sequence container that supports (amortized) constant time insert and erase operations at the end; insert and erase in the middle take linear time. Storage management is handled automatically, though hints can be given to improve efficiency.

关键点集中在这里:

... constant time insert and erase operations at the end;

末端插入和删除是常数时间

... insert and erase in the middle take linear time.

中间插入和删除须要线性时间(就是 O(n))。

典型的数组插入和删除的特征,不一样的是std::vector能够变长,因此真正插入大量数据的时候会有屡次从新分配内存和复制的操做。

2.2 CopyAssignable的约定

std::vector要求储存的对象知足DefautConstructibleCopyContructiableCopyAssignable的具名要求,文档参考26.3.11.1第2节。

26.3.11.1

A vector satisfies all of the requirements of a container and of a reversible container (given in two tables in 26.2), of a sequence container, including most of the optional sequence container requirements (26.2.3), of an allocator-aware container (Table 86), and, for an element type other than bool, of a contiguous container (26.2.1).

其中提到的Table 86中列出了DefaultConstructibleCopyAssignableCopyConstructiable

发挥一下脑洞,这些要求完美符合了以前对于从新分配内存的猜想对不对?

对象要能够被默认构造,由于vector的实现多是new了一个新的对象数组(更多是字节数组,到时候再placement new);对象要能够被复制构造,由于对象可能被从旧数组移动到新数组;对象要能够被复制构造.....

固然更可能的缘由是vector自己是可复制的,上面的就当我吹逼吧。

除此以外还有CopyInsertableMoveInsertable的具名需求,就像其字面意义那样,很少作解释。

2.3 内存从新分配的方式

对C稍有经验的人应该知道C语言有一个API叫作realloc,它作的事情是这样的:

  1. 若是可能的话,扩张原先分配的内存的长度。
  2. 不然从新分配一块内存,而后把旧的内存复制过去,释放旧内存,返回新指针。
  3. 若是找不到足够长度的连续内存,则返回NULL,不释放旧内存。

C++天然不会少。

面试时没有想起来,原本认为是一种优化方案,但STL自己就算是优化方案了吧(...)。正确的解答应该是

用realloc的方式尝试扩展buffer长度,若是没法扩展长度,则拷贝旧buffer到新buffer,再释放旧buffer。

还行,失误就是失误,认错复习一遍。

3. 比较三个容器:vector,map,list

面试官:说说看vectorlistmap有什么不一样,分别在什么样的上下文环境里去使用它们吧。

我:vector能够被随机访问,支持随机访问迭代器,迭代器算法有些不适用在listmap上blabla...list一般是链表实现,在插入删除的性能上有优点blabla......

顺便一提还没说到map,面试官就换话题了。

这一题我大概又没有 get 到面试官的 point,单谈论容器的话可说的东西很多,我以为面试官可能更想了解下我对这些容器的性能和内存方面的认知,惋惜我答的有些太浅白了。

3.1 迭代器

先从迭代器的角度比较三个容器。

vector是个典型的随机访问容器,显然支持forward iteratorreversible iteratorrandom access iterator。典型的实现是dynamic array

list是个线性结构容器,支持forward iteratorreversible iterator。典型的实现是链表。

map是个树形容器,支持forward iteratorreversible iterator。典型的实现是红黑树。

3.2 内存布局和访问效率

讨论常见实现。

vector是连续分配,访问成本低,插入和删除的成本高,会重分配内存。

list是不连续分配,访问成本高,任意位置插入删除成本相对低,插入删除不会致使从新分配整块内存。

map是不连续分配,插入删除访问成本不该和线性容器比较,毕竟它是关联容器。插入删除的成本都比较高,由于须要从新平衡树。访问时间在标准中的要求是对数时间复杂度,插入时间懒得继续翻标准文档了。

3.3 使用上下文

显而易见vector适合高频读,而list适合大量插入删除,map和前面两个迭代器都搭不上调,在须要复杂索引的地方再合适不过了。

3.4 线程安全性

这些容器都不是线程安全的。

依照标准,多线程访问不一样的容器实例一切都安好,访问同一个实例的const方法也ok,可是非const方法就会引发数据竞争。

尤为注意迭代器的选择,这玩意儿有时候不比指针好多少。

4. 如何管理内存资源

面试官:你在项目里通常是怎么管理内存的呢?

我:一个是尽量用智能指针,而后是须要频繁构造对象的场合下能够用placement new blabla...

内存管理是一个很是广阔的话题,个人回答太过于浅显了。常见的内存管理策略有不少,智能指针只能算是RAII这种常见的范式,placement new 算是内存池/对象池的一种写法大概,还有其余不少策略我并不了解也未能涉及。

4.1 再论 RAII

RAII的范式能够确保异常安全,避免手贱忘记回收内存以及底层设计变动抛出的异常没法处理时致使意外的资源泄露。

诸如此类等等。

有一些约定能够关注一下。

4.1.1 获取资源失败抛异常

首先RAII的全写是获取资源即初始化,连资源都没能获取的话,构造理应失败,而不是静默给出一个无效的对象。

4.1.2 析构毫不抛异常

很好理解,若是析构又抛个异常出来的话,这个对象还析构不析构?父类还析构不析构?

4.2.3 常见设计

在STL里除了智能指针以RAII设计之外,还有加锁解锁相关的内容也是:std::lock_guard

诸如此类的guard模式也在其余语言中有出现:好比说C#的using (var file = File.Open(...)) {}

4.2 内存池和对象池

内存池和对象池算是常见的设计范式,基本考虑到大量对象的构造删除的状况都会考虑到使用这两个模式,由于真的很好用(

内存池的模式主要是预先分配内存,而后在这片内存上构造对象,主要的适用场景是大量频繁构造小对象,构形成本低,生命周期短,内存分配成本居高不下的状况。固然,不只是这里提到的场景,根据具体业务逻辑可能还会有不一样的理由去选择内存池模式。

对象池区别于内存池的地方在于,对象池的对象构形成本要更高,频繁构造和析构是没法接受的,这种时候就须要一个候选备用的对象池,对象池实现须要对象自己容许被复用在不一样的地方,通常来讲性能会比较好。内存池则没这个顾虑:反正你须要就构造一个呗。

这两个池均可以用factory模式来提供构造对象的服务,而工厂的消费者不须要了解对象是怎么构造出来的。结合RAII的话,内存池、对象池里的对象还能够用一层RAII设计的“智能指针”封装,使其完成使命后能自动返还资源,等待下一个工厂访客。

5. 玩过哪些游戏,对游戏制做流程了解多少?

面试官:喜欢玩游戏吗?都玩过哪些游戏?

我:个人话...主要玩的是音游,和贵公司业务可能并无太多关联。

面试官:除了音乐游戏,有玩过RPG、ARPG类型的游戏吗?

我:像是辐射啊,老滚啊这些...开放世界类型的游戏游戏性没那么好,比起来我更喜欢电影式的游戏,好比说最近比较火的《底特律:变人》。

面试官:......(你丫来捣乱的是吧)

面试官:说说你对游戏行业的见解吧。

我:游戏行业前景好啊blablabla...娱乐崛起blabla...经济增加blabla....

面试官:......(????)

面试官:你上一家公司也是制做游戏的吧?就是说,大家游戏制做啊,都有哪方面的人在负责作什么东西,大概是怎么个分工合做的样子。(提醒+强调) 我:哦!哦哦,大概就是一我的负责策划整个游戏的玩法和系统,设计每一个细节,而后程序负责去实现,自动测试blabla...内部试玩blabla...

还行,这波操做其实我也是挺佩服本身的。

5.1 陷阱:玩过哪些游戏

我注意到一件事:在屡次面试游戏行业的职位时,都提到这这个问题:

你玩过哪些游戏?

也许形式上有所区别:

你玩过的游戏里,有哪些特别喜欢的?

换位思考,若是我是面试官,我为何要问这个问题?我想知道什么?

熟悉游戏吗?

知道游戏有哪些元素吗?

能理解(咱们招你进来要作的游戏)要你作什么吗?

没必要太过刻意地表达出对游戏行业的崇拜或者抬高之类的,这一关主要的目的仍是引出下文,聊聊对游戏制做流程的理解。若是对面试的公司出的产品有所了解的话可能算是加分项。

可是,从一个游戏玩家的角度出发,表现出很差的情绪容易留下坏映像——特别是,绝对不要明显地表达出对国产网游、手游、页游的鄙视!!

从一个玩家的角度出发,我也不喜欢大部分国产的页游手游,可是当着游戏行业公司的面试官的面,表现出我看不起你的态度,知道什么叫做死吗?

更况且并非全部国产游戏都是屎,举例来讲我如今超喜欢 MUSE DASH 这款国产音游的,手感比兰空 voze、节奏大师之类的好得多,界面也没有像节奏大师那样糊成屎,要不是个人 Unity3D 水平太差我真想给这家 pero pero game 工做室(公司?)投个简历看看。

除此以外还有就是抱着拯救国产游戏的想法或者态度,又或者劳资教大家什么才是真正的游戏这样的想法或者态度,做死无极限啊。

比较稳妥的回答方案应该是常见的几个网游,好比说LOL,DNF,王者荣耀,诸如此类。实际上玩过没玩过.....咳,不被戳穿就无所谓了。

5.2 游戏行业

加班是屡见不鲜,好像全部游戏行业的公司都会这么说。

大概了解下几个术语,算是加班界的黑话吧。

一个是996。什么意思呢?上午9点上班,晚上9点下班,一周上6天,加班费不用考虑了,不存在的,最多给调休。

再有一个是大小周。一周上6天,一周上5天,如此循环。一样,大周加班不算加班费,给调休。

另外就是调休。若是加班一天,未来某天就能够不扣工资休息一天,直白吧。攒下半年的调休而后一口气给本身放6个月假这种事情仍是作梦比较好,调休基本上就等于无偿加班了,忙起来的时候劝你别休,否则人手就不够了;那闲下来的时候还能让你一周休6天?你敢休公司也不敢让你随便休啊,其余员工怎么看。

发薪日。网上有人总结,发薪日越接近月中的,或者超过月中的,大多都是怕员工流失的公司,而这些公司每每都不是什么好公司。听起来仍是挺有道理的(

固然,最后仍是要靠本身的眼睛去确认这一点。

5.3 游戏的制做流程

以前待得确实是一家小公司,甚至算得上工做室级别的超小初创公司,游戏制做方面的知识储备不算充足,写这篇博客的时候又去补习了一下。

主要的工种分为策划、美术、程序。

细分的话,策划可能有数值方面的,世界背景人物背景方面的,对话文本方面的,甚至可能有长篇幅的资料啊故事啊这方面的需求。

美术有UI方面的,人物、场景的原画师,3d模型制做,动画制做,骨骼制做,特效制做,等等方面的。程序常常须要和美术方面的沟通交流。

程序的话主要分先后端和测试,再加上运维和DBA之类的角色。

细分的话前端根据开发平台不一样也有不一样的技术栈,图像特效上可能会有更专业的大牛负责,team leader带队设计架构,分配工做,诸如此类。后端也同样,根据不一样的技术抉择,可能总体的人员配置也有所区别,但你们都是程序嘛。

测试算是比较独立的,编写测试代码是一件很痛苦的事情(

因此这份疼痛有专人负责承受了:)

持续集成啊什么的也被承包了,测试或者运维会去负责的。

DBA通常公司也用不到,运维多少会两手SQL,规模更大的公司可能会设置这个专门职位。

流程上来讲,策划给出游戏方案,美术可能会配合作个初稿效果图之类的(更多是策划本身作个简单的效果图之类的方便说明),程序疯狂实现(崩溃-爆发-认命 循环),测试则配合给出反馈,让程序的脱发情况持续恶化,最后发布,项目黄了。

哦不是,我是说项目火了,程序们一跃成为CTO,迎娶白富美,走上人生巅峰。

(并无)

6. 尾声

其实此次面试的自我感受仍是不错的,没有犯下太蠢的错误,可是能够改进的地方依然不少,语言组织能力须要进一步提升。

这篇博客的目的是自我检讨,可是此次自我检讨的效果并不算好,由于面试官的问题基本上都戳在我懂,但又没真正去深刻挖掘的领域。平常使用天然没有问题,但理解却谈不上了。

若是面试官在细节上稍做追究:好比说placement new和user-defined new 之类的话题上深刻,异常安全,或者问个map用红黑树实现,红黑树什么原理,那么此次我基本又要挂了。

关于给出的待遇的问题......我其实很好奇......

由于我真的才工做一年,不懂啊...

一年工做年限,C++我也不知道算什么水平,不知道怎么去横向对比,要8k是要多了么...

初级职位的意思是待遇初级仍是能力初级啊...

还有主程通常指的是 team leader 对吗,游戏行业程序是否是干到 team leader 就算到头了...只能转管理岗了...