智臾科技_2.28面經(一面)


面試過程

22min

  • 自我介紹
  • 講一下虛函數
  • 與運行時多態相對的是什么
  • C++是否可以實現編譯時多態
  • 對虛函數底層實現有了解嗎 —— 不會
  • 有用過智能指針嗎?大概講解下
  • 怎么初始化unique_ptr指針 —— 不會
  • share_ptr指針為什么要計數?計數為0時會執行什么動作?
  • 對C++11還有什么了解嗎?
  • 有用過lambda函數嗎? —— 不會
  • unordered_map與map的區別?時間復雜度有什么區別?
  • vector的實現原理?底層是什么?在內存里是連續的嗎?
  • vector的push_back()的時間復雜度?為什么會是常量時間?
  • 對多線程有了解嗎?
  • 怎么解決並發問題?如兩個線程同時訪問一個資源?
  • 用鎖會有什么問題?
  • 什么時候會發生死鎖?
  • 平時開發是否用Linux?
  • 列舉一些排序算法?
  • 說一下快排的實現?
  • 項目
  • 反問

下面是問題答案整理

虛函數

如何理解虛函數

  • 對面向對象的程序設計而言,就是當對象接收到某一消息時,才去尋找和連接相應的方法,所以只能采用動態聯編。
  • 而 C++ 為了保持C語言的高效性,仍是編譯型的,采用靜態聯編。利用虛函數機制,C++ 可部分地采用動態聯編,C++通過虛函數實現運行時多態性
  • 如果某類中的一個成員函數被說明為虛函數,這就意味着該成員函數在派生類中可能有不同的實現。
  • 當子類重新定義了父類的虛函數后,當父類的指針指向子類對象的地址時,父類指針根據賦給它的不同子類指針,動態的調用子類的該函數,而不是父類的函數

虛函數和純虛函數的區別

  • 定義一個函數為虛函數,不代表函數為不被實現的函數。
  • 定義他為虛函數是為了允許用基類的指針來調用子類的這個函數。
  • 定義一個函數為純虛函數,才代表函數沒有被實現。
  • 定義純虛函數是為了實現一個接口,起到一個規范的作用,規范繼承這個類的程序員必須實現這個函數。

編譯時多態

C++編譯期多態與運行期多態

對模板參數而言,多態是通過模板具現化和函數重載解析實現的。以不同的模板參數具現化導致調用不同的函數,這就是所謂的編譯期多態。

虛函數底層實現

編譯器將為實現了虛函數的基類和覆蓋了虛函數的派生類分別創建一個虛函數表(Virtual Function Table,VFT)。也就是說Base和Derived類都將有自己的虛函數表。實例化這些類的對象時,將創建一個隱藏的虛表指針VFT*,它指向相應的VFT。可將VFT視為一個包含函數指針的靜態數組,其中每個指針都指向相應的虛函數。

舉個例子:基類對象包含一個虛表指針,指向基類中所有虛函數的地址表。派生類對象也將包含一個虛表指針,指向派生類虛函數表。看下面兩種情況:

  • 如果派生類重寫了基類的虛方法,該派生類虛函數表將保存重寫的虛函數的地址,而不是基類的虛函數地址。
  • 如果基類中的虛方法沒有在派生類中重寫,那么派生類將繼承基類中的虛方法,而且派生類中虛函數表將保存基類中未被重寫的虛函數的地址。注意,如果派生類中定義了新的虛方法,則該虛函數的地址也將被添加到派生類虛函數表中。

image

智能指針

為什么要使用智能指針

使用智能指針可以很大程度上的避免內存泄露問題,因為智能指針就是一個類,當超出了類的作用域是,類會自動調用析構函數,析構函數會自動釋放資源。

unique_ptr

unique_ptr 用於取代 c++98 的 auto_ptr

  • 在 c++98 的時候還沒有移動語義(move semantics)的支持,因此對於auto_ptr的控制權轉移的實現沒有核心元素的支持,但是還是實現了auto_ptr的移動語義,這樣帶來的一些問題是拷貝構造函數和復制操作重載函數不夠完美
  • auto_ptr不支持傳入deleter,所以只能支持單對象(delete object),而unique_ptr對數組類型有偏特化重載,並且還做了相應的優化,比如用[]訪問相應元素等.

unique_ptr是一個獨享所有權的智能指針,它提供了嚴格意義上的所有權,包括:

  • 1、擁有它指向的對象
  • 2、無法進行復制構造,無法進行復制賦值操作。即無法使兩個unique_ptr指向同一個對象。但是可以進行移動構造和移動賦值操作
  • 3、保存指向某個對象的指針,當它本身被刪除釋放的時候,會使用給定的刪除器釋放它指向的對象

unique_ptr可以實現如下功能:

  • 1、為動態申請的內存提供異常安全
  • 2、講動態申請的內存所有權傳遞給某函數
  • 3、從某個函數返回動態申請內存的所有權
  • 4、在容器中保存指針
  • 5、auto_ptr 應該具有的功能

如何初始化unique_ptr

unique_ptr的使用和陷阱

  • shared_ptr不同,unique_ptr沒有定義類似make_shared的操作,因此只可以使用new來分配內存,並且由於unique_ptr不可拷貝和賦值,初始化unique_ptr必須使用直接初始化的方式。
unique_ptr<int> up1(new int());    //okay,直接初始化
unique_ptr<int> up2 = new int();   //error! 構造函數是explicit
unique_ptr<int> up3(up1);          //error! 不允許拷貝
  • shared_ptr不同,unique_ptr擁有它所指向的對象,在某一時刻,只能有一個unique_ptr指向特定的對象。當unique_ptr被銷毀時,它所指向的對象也會被銷毀。因此不允許多個unique_ptr指向同一個對象,所以不允許拷貝與賦值。

unique_ptr的操作

  • unique_ptr<T> up 空的unique_ptr,可以指向類型為T的對象,默認使用delete來釋放內存
  • unique_ptr<T,D> up(d) 空的unique_ptr同上,接受一個D類型的刪除器d,使用刪除器d來釋放內存
  • up = nullptr 釋放up指向的對象,將up置為空
  • up.release() up放棄對它所指對象的控制權,並返回保存的指針,將up置為空,不會釋放內存
  • up.reset(...)參數可以為空、內置指針,先將up所指對象釋放,然后重置up的值.

share_ptr

  • 從名字share就可以看出了資源可以被多個指針共享,它使用計數機制來表明資源被幾個指針共享
  • 可以通過成員函數use_count()來查看資源的所有者個數。
  • 除了可以通過new來構造,還可以通過傳入auto_ptr, unique_ptr,weak_ptr來構造。
  • 當我們調用release()時,當前指針釋放資源所有權,計數減一。當計數等於0時,資源會被釋放

weak_ptr

  • weak_ptr是用來解決shared_ptr相互引用時的死鎖問題
  • 如果說兩個shared_ptr相互引用,那么這兩個指針的引用計數永遠不可能下降為0,資源永遠不會釋放。
  • 它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr

C++ 11

1. Auto

  • 第一,非常明顯,能夠避免重復輸入我們已經聲明的並且編譯器已經認識的類型名稱,這是非常方便的。
  • 第二,當有遇到一個你不知道或者無法用語言表達的類型時,auto就不僅僅是使用方便這么簡單了,比如,大多數lambda函數的類型,你不能夠容易的將其類型拼寫出來甚至根本就不能夠寫出來。

注意,使用auto並沒有修改代碼的語義。代碼仍然是靜態類型(statically typed),並且每個表達式都干凈利落;只是不再強制我們多余的重新聲明類型的名稱。

2. 智能指針,no delete

總是使用智能指針,不要用原生指針和delete。除非需要實現你自己的底層數據結構(把原生指針很好的封裝在類(class boundary)中

  • 如果你知道你是另外一個對象的唯一擁有者,使用unique_ptr來表示唯一的擁有權。一個"new T"表達式能很快的初始化一個擁有 這個智能指針的對象,特別是unique_ptr
  • 使用shared_ptr來表示共享所有權(shared ownership)。使用make_shared來創建共享對象更好。
  • 使用weak_ptr打破循環和表示可選性(比如實現一個對象緩存)
  • 如果你了解到另外一個對象比你的生存周期要長,並且你想觀察這個對象,那么使用原生指針(raw pointer)。

3. Nullptr

用nullptr來表示一個空指針,不要再使用數字0或者宏NULL來表示空指針了,因為這些是模棱兩可的,既能表示整形也可表示指針。

4. Range for

對一個范圍內的元素進行有序訪問,基於range的for循環會是更方便的用法。

5. 非成員begin和end

使用非成員函數begin(x)和end(x)(不是x.begin()和x.end()),因為begin(x)和end(x)是可擴展的,能同所有容器類型一塊工作——甚至數組也可以——並不是只針對提供了STL風格的x.begin()和x.end()成員函數的容器。

如果你正在使用一個非STL集合類型,這個類型提供迭代器但不是STL風格的x.begin()和x.end(),你可以對他的非成員函數begin()和end()進行重載,這樣你就可以使用同STL容器同樣的風格進行編碼。

6. Lambda函數

C++11中的匿名函數(lambda函數,lambda表達式)

Lambda表達式具體形式如下:

[capture](parameters)->return-type{body}

如果沒有參數,空的圓括號()可以省略.返回值也可以省略,如果函數體只由一條return語句組成或返回類型為void的話.形如:

[capture](parameters){body}
[](int x, int y) { return x + y; } // 隱式返回類型
[](int& x) { ++x; }   // 沒有return語句 -> lambda 函數的返回類型是'void'
[]() { ++global_x; }  // 沒有參數,僅訪問某個全局變量
[]{ ++global_x; }     // 與上一個相同,省略了()
[](int x, int y) -> int { int z = x + y; return z; }    // 顯示指定返回類型
  • Lambda函數中創建的臨時變量, 和普通函數一樣不會保存到下次調用
  • 什么也不返回的Lambda函數可以省略返回類型, 而不需要使用 -> void 形式.

Lambda函數可以引用在它之外聲明的變量,這些變量的集合叫做一個閉包

閉包被定義在Lambda表達式聲明中的方括號[]內,這個機制允許這些變量被按或按引用捕獲。

[]        //未定義變量.試圖在Lambda內使用任何外部變量都是錯誤的.
[x, &y]   //x 按值捕獲, y 按引用捕獲.
[&]       //用到的任何外部變量都隱式按引用捕獲
[=]       //用到的任何外部變量都隱式按值捕獲
[&, x]    //x顯式地按值捕獲. 其它變量按引用捕獲
[=, &z]   //z按引用捕獲. 其它變量按值捕獲

7. Move/&&

把move當作是對拷貝的優化最合適不過了,雖然它也包含其他方面的東西(像完美轉發(perfect forwarding))

move語義改變了我們設計API的方式。我們會越來越多的將函數設計成return by value。

8. 統一的初始化和初始化列表

  • 沒有發生變化的:當初始化一個non-POD或者auto的本地變量時,繼續使用熟悉的不帶額外花括號{}的=語法。
  • 在其他情況中(特別是隨處可見的使用()來構造對象),使用花括號{}會更好。

使用花括號{}能避免一些潛在的問題:

  • 你不會突然得到一個收縮轉換(narrowing conversions)后的值(比如,float轉換成int)
  • 也不會有偶爾突發的未初始化POD成員變量或者數組的存在
  • 也能避免在c++98中會碰到的奇怪事:你的代碼編譯沒問題,你需要的是變量但實際上你聲明了一個函數

最后,有時候傳遞不帶(type-named temporary)的函數參數是很方便的

9. 顯示重寫(覆蓋)override和final

  • 在 C++ 03中,很容易讓你在本想重寫基類某個函數的時候卻意外地創建了另一個虛函數。
  • C++11引入了 override 這個特殊的標識符意味編譯器將去檢查基類中有沒有一個具有相同簽名的虛函數,如果沒有,編譯器就會報錯!

C++11還增加了防止基類被繼承和防止子類重寫函數的能力。這是由特殊的標識符final來完成的,

需要注意的是,override和final都不是C++語言的關鍵字。他們是技術上的標識符,只有在它們被用在上面這些特定的上下文在才有特殊意義。用在其它地方他們仍然是有效標識符。

10. 新增容器std::array

  • std::array 保存在棧內存中,相比堆內存中的 std::vector,我們能夠靈活的訪問這里面的元素,從而獲得更高的性能。
  • std::array 會在編譯時創建一個固定大小的數組,std::array 不能夠被隱式的轉換成指針,使用 std::array只需指定其類型和大小即可:

11. 字符串類 新增與其他類型互換的方法

字符串類 字新增與其他類型互換的方法,如to_string(),stoi(),stol等

12. STL標准模板庫新增unordered_map

  • map的底層原理,是通過紅黑樹(一種非嚴格意義上的平衡二叉樹)來實現的,因此map內部所有的數據都是有序的,map的查詢、插入、刪除操作的時間復雜度都是O(logn)
    • 根結點是黑的。
    • 每個葉結點(葉結點即指樹尾端NIL指針或NULL結點)都是黑的。
    • 如果一個結點是紅的,那么它的兩個兒子都是黑的。
    • 對於任意結點而言,其到葉結點樹尾端NIL指針的每條路徑都包含相同數目的黑結點。
  • unordered_map的底層是一個防冗余的哈希表(采用除留余數法)。哈希表最大的優點,就是把數據的存儲和查找消耗的時間大大降低,時間復雜度為O(1);而代價僅僅是消耗比較多的內存。
  • 使用時map的key需要定義operator<。而unordered_map需要定義hash_value函數並且重載operator==。

數據結構與算法

vector

Vector 是一個封裝了動態大小數組的順序容器。跟任意其它類型容器一樣,它能夠存放各種類型的對象。可以簡單的認為,向量是一個能夠存放任意類型的動態數組

push_back

  • 該函數首先檢查是否還有備用空間,如果有就直接在備用空間上構造元素,並調整迭代器 finish,使 vector變大。如果沒有備用空間了,就擴充空間,重新配置、移動數據,釋放原空間。
  • 當執行 push_back 操作,該 vector 需要分配更多空間時,它的容量(capacity)會增大到原來的 m 倍。

現在我們來均攤分析方法來計算 push_back 操作的時間復雜度。

  • 假定有 n 個元素,倍增因子為 m。那么完成這 n 個元素往一個 vector 中的push_back 操作,需要重新分配內存的次數大約為 \(\log_m(n)\)
  • 第 i 次重新分配將會導致復制 \(m^i\) (也就是當前的vector.size() 大小)個舊空間中元素
  • 因此 n 次 push_back操作所花費的總時間約為 n*m/(m - 1),那么 n 個元素,n 次操作,每一次操作需要花費時間為 m / (m - 1),這是一個常量.

所以,我們采用均攤分析的方法可知,vector 中 push_back 操作的時間復雜度為常量時間.

快排

八大排序-快速排序(搞定面試之手寫快排)

快速排序的核心思想是分治:選擇數組中某個數作為基數,通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數都比基數小,另外一部分的所有數都都比基數大,然后再按此方法對這兩部分數據分別進行快速排序,循環遞歸,最終使整個數組變成有序。

以最常見的使用數組首元素作為基數進行快速排序原理說明。

  • 以第一個數字作為基數,使用雙指針i,j進行雙向遍歷:
  • 、i從左往右尋找第一位大於基數(6)的數字,j從右往左尋找第一位小於基數(6)的數字;‘’找到后將兩個數字進行交換。繼續循環交換直到i>=j結束循環;
  • 最終指針i=j,此時交換基數和i(j)指向的數字即可將數組划分為小於基數(6)/基數(6)/大於基數(6)的三部分,即完成一趟快排

多線程

死鎖

死鎖面試題(什么是死鎖,產生死鎖的原因及必要條件)

所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。

產生死鎖的必要條件:

  • 互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅為一進程所占用
  • 請求和保持條件:當進程因請求資源而阻塞時,對已獲得的資源保持不放
  • 不剝奪條件:進程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
  • 環路等待條件:在發生死鎖時,必然存在一個進程--資源的環形鏈。

Linux

gdb 調用棧命令

  • backtrace/bt  bt  用來打印棧幀指針,也可以在該命令后加上要打印的棧幀指針的個數,查看程序執行到此時,是經過哪些函數呼叫的程序,程序“調用堆棧”是當前函數之前的所有已調用函數的列表(包括當前函數)。每個函數及其變量都被分配了一個“幀”,最近調用的函數在 0 號幀中(“底部”幀)
  • frame  frame 1  用於打印指定棧幀
  • info reg  info reg  查看寄存器使用情況
  • info stack  info stack  查看堆棧使用情況
  • up/down  up/down  跳到上一層/下一層函數


免責聲明!

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



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