簡單介紹下面試的前置狀況。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做用要實際的多。固然並非說這麼問很差,這些概念的掌握也是很是重要的基礎,並且能有效考察面試者語言組織能力和對這方面知識的掌握程度。
惟一很差的就是,面試者和麪試官聊的過程就像是用黑話交流同樣......
不說了,學這黑話去......