超全面的C++游戲開發面試問題總結


筆者拿到了騰訊IEG以及網易游戲的兩個客戶端研發offer(UE4/C++)。在面試前夕,筆者對C++進行了較為全面的復習和總結,樂觀估計可以涵蓋80%左右的面試基礎問題。如果你也是想從事游戲開發方面的工作,可以參考下,希望對大家有幫助!

個人覺得如果這些問題你全部搞懂的話,大部分面試官在C++上就拿你沒什么辦法或者說不會再進一步為難你了。不過想徹底理解所有內容也並不容易,這里面涉及到操作系統、數據結構、計算機系統原理、匯編等基礎內容,涉及到的書籍包括《C++ Primer》《Inside the C++ Object Model》《Effctive C++》《More Effctive C++》《C++ Template》《The Design and Evolution of C++》《STL源碼剖析》《深入理解計算機系統》等。

問:了解const么?哪些時候用到const?與宏定義有什么差異?(提問概率:★★★★)

簡單理解,const的目的就是定義一個“不會被修改的常量”,可以修飾變量、引用、指針,可以用於函數參數、成員函數修飾。成員變量。使用const可以減少代碼出錯的概率,我們通常要注意的是區分常量指針(指向常量的指針)和指針常量(地址是常量,指針指向的地址不變)以及合理的在函數參數里面使用。具體的情況可以參考下面的書籍與資料。

參考書籍與資料:《Effctive C++》

問:reference和pointer的區別?哪些情況使用pointer?(提問概率:★★)

1.指針可以為空,而引用不可以指向空值。
2.指針可以不初始化,引用必須初始化。這意味着引用不需要檢測合法性
3.指針可以隨時更改指向的目標,而引用初始化后就不可以再指向任何其他對象
根據上面的情況我們知道大概知道哪些時候需要使用指針了。不過還有一種情況,在重載如[]符號的時候,建議返回引用,這樣便於我們書寫習慣也方便理解。因為平時我們都是這樣使用, a[10] = 10;而不是 *a[10] = 10;

參考書籍與資料:《More Effctive C++》

問:inline的優劣(提問概率:★★)

優點:減少函數調用開銷
缺點:增加函數體積,exe太大,占用CPU資源,可導致cache裝不下(減小了cache的命中) ,不方便調試debug下一般不內聯, 每次修改會重新編譯頭文件增加編譯時間
注意:inline只是一個請求,編譯器有權利拒絕。有7種情況下都會拒絕,虛調用,體積過大,有遞歸,可變數目參數,通過函數指針調用,調用者異常類型不同,declspec宏等
forceinline字面意思上是強制內聯,一般可能只是對代碼體積不做限制了,但是對於上面的那些情況仍然不會內聯,如果沒有內聯他會返回一個警告。 構造函數析構函數不建議內聯,里面可能會有編譯器優化后添加的內容,比如說初始化列表里面的東西。

問:final和override的作用,以及使用場合(提問概率:★★)

final:禁止繼承該類或者覆蓋該虛函數
override:必須覆蓋基類的匹配的虛函數
場合(final):不希望這個類被繼承,比如vector,編碼者可能不夠了解vector的實現,或者說編寫者不希望別人去覆蓋某個虛函數,顧名思義,final就是最終么
場合(override):第一種,在使用別人的函數庫,或者繼承了別人寫的類時,想寫一個新函數,可能碰巧與原來基類的函數名稱一樣,被編譯器誤認為要重寫基類的函數。第二種情況是想覆寫一個基類的函數,但是不小心參數不匹配或者名字拼錯,結果導致寫了一個新的虛函數

參考書籍與資料:《C++ Primer》

問:The rule ofthree是什么?為什么這么做?(提問概率:★)

If you need to explicitly declare either the destructor,copy constructor or copy assignment operator yourself, you probably need toexplicitly declare all three of them.(析構函數,拷貝構造函數,賦值運算符盡可能一起聲明。如果你只定義一個,編譯器會幫助你定義另外兩個,而編譯器定義的版本也許不是你想要的)

問:C++03/98有什么你不習慣或不喜歡的用法?C++11有哪些你使用到的新特性?(提問概率:★★★★★)

這個問題最簡單的辦法就是看下一個版本的C++有哪些特性,新的特性肯定是有意義的。

如:

auto,有一些迭代器或者map嵌套類型,遍歷時比較麻煩,auto寫起來很方便。

vector以及其他容器的列表初始化,原來想要像數組一樣初始化的話,需要一個一個來,很麻煩。

類內初始值問題,總是需要放到構造函數里面初始化,初始化列表倒是不錯,但是初始化數據太多就不行了。

nullptr,C++11前的NULL一般是是這樣定義的 #define NULL 0,這可能會導致一些函數參數匹配問題。而nullptr可以避免這個問題。

thread,不需要再使用其他的庫來寫多線程了。

智能指針shareptr,一定程度上解決內存泄露問題。

右值引用,減少拷貝開銷。

lambda function,簡化那些結構簡單的函數代碼。
當然,你要是能說出一些還沒有改正或者有待考慮的問題就更好了,比如內存管理的困難(沒有GC),沒有反射以及一些C#,java里面有而C++沒有的特性等,要能深入一點說那就更好了

參考書籍與資料:《C++ Primer》

問:Delete數組的一部分會發生什么?為什么出現異常?(提問概率:★★★★)

VC下是異常,實際刪除的時候整個數組的內存不僅僅是數據大小還包括CRTHeader,數組長度等信息。如果刪除一部分會從數量的位置開始傳入,是有問題的。VC下數組的內存布局參考下面公式,

公式1)_CrtMemBlockHeader + <Your Data>+gap[nNoMansLandSize];這類數據用delete和delete[]都一樣!

公式2)_CrtMemBlockHeader +數組元素個數+ <Your Data>+gap[nNoMansLandSize];

如果其他編譯器,有可能不會報錯。但是只釋放一個數組對象也是有問題的,其他的對象既沒有釋放也沒有析構。

問:系統是如何知道指針越界的?(提問概率:★★)

VC下有一個結構體_CrtMemBlockHeader,里面有一個Gap屬性,這個Gap數組放在你的指針數據的后面,默認為0xFD,當檢測到你的數據后不是0xFD的時候就說明的你的數據越界了。

問:C++編譯器有哪些常見的優化?聽說過RVO(NRVO)么?(提問概率:★★★)

1.常量替換如int a = 2; int b = a; return b;可能會優化為 int b=2; return b; 進一步會優化為return 2;

2.無用代碼消除比如函數返回值以及參數與該表達式完全無關,直接會優化掉這段代碼

3.表達式預計算和子表達式提取常量的乘法會在編譯階段就計算完畢,相同的子表達式也會被合並成一個變量來進行計算

4.某些返回值為了避免拷貝消耗,可能會被優化成一個引用並放到函數參數里面,如RVO,NRVO。

RVO:函數返回的對象如果是新構造的值類型就直接通過一個引用作為參數來構造,進而避免創建一個臨時的“temp”對象。

NRVO:相比RVO進一步優化。對於RVO,如果函數在返回前創建了一個臨時變量,這個臨時變量還是會被構造的,參考下面代碼

Point3d Factory()

{

Point3d po(1,2, 3);

return po;

}

//RVO優化后

void Factory(Point3d &_result)

{

Point3d po(1,2,3);

_result.Point3d::Point3d(po);

return;

}

//NRVO優化后

void Factory(Point3d &_result)

{

_result.Point3d::Point3d(1, 2, 3);

return;

}

NRVO則直接跳過臨時對象的構造。

(補充:上面的優化有的時候不同編譯器可能有差別,想一探究竟建議查看反匯編代碼。一般來說函數返回的臨時值類型對象是右值,通過寄存器存儲,所以獲取不到地址)

當然,優化還有很多,這里不一一列舉。由於這些優化,你在調試過程中可能無法設置斷點,所以需要關閉優化。還有一個小的技巧,static變量不會被優化。

參考書籍與資料:

《Inside the C++ Object Model》(深度探索C++對象模型)

問:聽說過mangling么?(提問概率:★★)

mangling 指編譯器給函數變量等添加很多的描述信息到名稱上用於傳遞更多信息。常用函數重載,編譯時可以把返回值類型等與原函數名稱進行組合達到區分的效果,具體規則看編譯器。

參考書籍與資料:《Inside the C++ Object Model》(深度探索C++對象模型)

問:成員函數指針了解么?可以轉換為Void*么?為什么?(提問概率:★★★)

不可以轉換成Void*,因為成員函數指針大小並不是4個字節(32位機器上),除了地址還需要this的delta,索引等信息。成員函數指針比較復雜,建議好好讀一下下面給出的文章。

寫法:函數指針 float (*my_func_ptr)(int, char *);

成員函數指針 float (SomeClass::*my_memfunc_ptr)(int,char *);

問:描述一下C/C++代碼的編譯過程?(提問概率:★★★★)

預處理——編譯——匯編——鏈接。預處理器先處理各種宏定義,然后交給編譯器;編譯器編譯成.s為后綴的匯編代碼;匯編代碼再通過匯編器形成.o為后綴的機器碼(二進制);最后通過鏈接器將一個個目標文件(庫文件)鏈接成一個完整的可執行程序(或者靜態庫、動態庫)。

參考書籍與資料:《深入理解計算機系統》

問:了解靜態庫與動態庫么?說說靜態鏈接與動態鏈接的實現思路(提問概率:★★★)

靜態庫:任意個.o文件的集合,程序link時,被復制到output文件。這個靜態庫文件是靜態編譯出來的,索引和實現都在其中,可以直接加到內存里面執行。

對於Windows上的靜態庫.lib有兩種,一種和上面描述的一樣,是任意個.o文件的集合。程序link時,隨程序直接加載到內存里面。另一種是輔助動態鏈接的實現,包含函數的描述和在DLL中的位置。也就是說,它為存放函數實現的dll提供索引功能,為了找到dll中的函數實現的入口點,程序link時,根據函數的位置生成函數調用的jump指令。(Linux下.a為后綴)

動態庫:包含一個或多個已被編譯、鏈接並與使用它們的進程分開存儲的函數。在程序編譯時並不會被連接到目標代碼中,而是在程序運行是才被載入。不同的應用程序如果調用相同的庫,那么在內存里只需要有一份該共享庫的實例,規避了空間浪費問題。(Linux下.so為后綴)

參考書籍與資料:《深入理解計算機系統》

問:知道內部鏈接與外部鏈接么?(提問概率:★★)

內部鏈接:如果一個名稱對於他的編譯單元是局部的,並且在鏈接時不會與其他的編譯單元中同樣的名字沖突,那么這個名稱就擁有內部鏈接。

外部鏈接:一個多文件的程序中,一個實體可以在鏈接時與其他編譯單元交互,那么這個實體就擁有外部鏈接。換個說法,那些編譯單元(.cpp)中能想其他編譯單元(.cpp)提供其定義,讓其他編譯單元(.cpp)使用的函數、變量就擁有外部鏈接

問:extern與static(提問概率:★★★)

extern 聲明一個變量定義在其他文件,這樣當前文件就可以使用這個變量,否則會編譯失敗,如果兩個全局變量名稱一樣會出現鏈接失敗。extern c的作用更重要,因為c++的編譯方式與c是不同的,比如函數重載利用mangling的優化。static變量,就是在全局聲明一個變量判斷是否初始化,是的話之后就不做操作了。static成員函數其實在編譯后與class完全沒有關系。static成員其實也沒關系,但是private的需要通過類去調用。static全局只能在本文件使用(內鏈接),與其他無關。全局函數變量是外鏈接,可以跨單元調用。

參考書籍與資料:《C++ primer》

問:delegate是什么?實現思路?與event的區別?(提問概率:★★★)

代理簡單來說就是讓對象B去代理A執行A本身的操作,本質上就是通過指向其他成員函數或者全局函數的函數指針去代理執行。而函數指針有兩種,成員函數指針與普通的函數指針,我們一般就是通過對這兩種指針的封裝來實現代理的效果。常見的實現方式有兩種,一種是通過多態接口,另一種是通過宏。代理也分為單播代理與多播代理,單播就是一個調用只代理執行一個函數功能,多播代理就是一個調用可以綁定多個代理函數,可以觸發多個代理的函數操作。
Event是一種特殊的多播delegate,只有聲明事件的類可以調用事件的觸發操作。最常見的也容易理解的就是MFC里面的按鈕的鼠標點擊事件了,他的調用只能在Button里面去執行。

問:使用過模板么?了解哪些特性?(提問概率:★★★★)

模板分為函數模板與類模板,其根本目的是將類型“參數化”,實現編譯時的“動態化”,避免重復代碼的書寫。另一種運行時的“動態化”就是多態。

模板使用常見的特性有“特化”,“偏特化”,“非類型模板參數”,“設置模板參數默認類型”,“模板中的typename的使用”,“雙重模板參數Template Template Parameters”,“成員模板Member Template”,理解這些內容我們就基本上可以看STL標准庫了。

另外,模板的實例化過程也是需要理解的。

參考書籍與資料:“STL源碼”,《C++ Template》,《C++ Primer》

問:聽說過轉發構造么?(提問概率:★★)

通過foward關鍵字可以同時考慮到參數為左值以及右值的情況,然后把函數的參數完美的轉發到其他函數的參數里面。這個里面涉及到左值、右值、move、forward、引用折疊等技術點。

參考書籍與資料:《C++ Primer》《Effective Modern C++》

移動語義(move semantic)和完美轉發(perfect forward)

問:描述一下函數調用過程中棧的變化(提問概率:★★★★)

回答這個問題需要對棧的使用過程,函數調用,匯編都有一定的理解才行。首先,要清楚一個概念“棧幀”。

棧幀(stack frame):機器用棧來傳遞過程參數,存儲返回信息,保存寄存器用於以后恢復,以及本地存儲。為單個過程(函數調用)分配的那部分棧稱為棧幀。棧幀其實是兩個指針寄存器,寄存器ebp為幀指針(指向該棧幀的最底部),而寄存器esp為棧指針(指向該棧幀的最頂部)。

然后我們再簡單描述一下函數調用的機制,每個函數有自己的函數調用地址,里面會有各種指令操作(這端內存位於“代碼段”部分),函數的參數與局部變量會被創建並壓縮到“棧”的里面,並由兩個指針分別指向當前幀棧頂和幀棧尾。當進入另一個子函數時候,當前函數的相關數據會被保存到棧里面,並壓入當前的返回地址。子函數執行時也會有自己的“棧幀”,這個過程中會調用CPU的寄存機進行計算,計算后再彈出“棧幀”相關數據,通過“棧”里面之前保存的返回地址再回到原來的位置執行前面的函數。參考下圖

參考書籍與資料:《深入理解計算機系統》

問:__cdecl/__stdcall是什么意思(提問概率:★★★)

常見的函數調用有如下

__cdecl/__stdcall/__thiscall/__fastcall。

cdecl按照c語言標准,從右到左,可以實現可變參數,調用者彈出參數。

stdcall(pascal調用約定)按照c++標准,函數參數從右到左,不支持可變參數,函數返回自動清空。但是有的時候編譯器會識別並優化成cdecl。

Pascal語言中參數就是從左到右入棧的不支持可變長度參數

(注:__stdcall標記的函數結束后,ret 8表示清理8個字節的堆棧,函數自己恢復了堆棧)

參考書籍與資料:“建議查看反匯編代碼”

問:C++中四種Cast的使用場景是什么?(提問概率:★★★★★

constcast,去掉常量屬性以及volatile,但是如果原來他就是常量去掉之后千萬不要修改;比如你手里有一個常量指針引用,但是函數接口是非常量指針,可能需要轉換一下;成員函數聲明為const,你想用this去執行一個函數,也需要用constcast

staticcast,基本類型轉換到void,轉換父類指針到子類不安全

dynamiccast,判斷基類指針或引用是不是我要的子類類型,不是強轉結果就返回null,用於多態中的類型轉換

reintercast,可以完成一些跨類型的轉換,如int到void*,用於序列化網絡包數據

參考書籍與資料:《C++ Primer》《The Design and Evolution of C++》(C++語言的設計與演化)

問:用過或很熟悉的設計模式有哪些?(提問概率:★★★★)

這個問題看好書寫寫代碼就可以自由發揮了,下面給幾個例子。

工廠模式,通過簡單工廠生成NPC對象,簡單處理的話可通過“字符串匹配”動態創建對象。如果有“反射機制”就可以直接傳class來實現。當然可以進一步使用抽象工廠,處理不同的生產對象。

單例,實現全局唯一的一個對象。構造函數、靜態指針都是私有的,使用前提前初始化或者加鎖來保證線程安全。

Adaptor適配器,代碼適配原來的相機移動最后調用的是原來的移動,現在加了適配器繼承里面放了當前引擎的攝像機,然后覆蓋原來攝像機的移動邏輯。

Observer,一個對象綁定多個觀察者,然后這個對象一旦有消息就立刻公布給所有的觀察者,觀察者可以動態添加或刪除。在UE4里面,行為樹任務節點請求任務后進入執行狀態,然后會立刻注冊一個觀察者observer到行為樹(行為樹本身就相當於前面提到的那個對象)的observer數組里面同時綁定一個代理函數。行為樹tick檢測消息發送給所有觀察者,觀察者收到消息執行代理函數。

參考書籍與資料:《Head First設計模式》《設計模式:可復用面向對象軟件的基礎》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM