# 01 面試題筆記系列-「藍本本」

本文大約有 6385 字。

這是面試題筆記系列的第一篇文章,記錄一些常見的面試題。其中主要是一些一線互聯網大廠的面試題,大部分收集於 LeetCode、CSDN、知乎、GitHub 等技術社區。

答案皆根據我的理解進行編寫解答,水平有限,可能會存在一定的錯誤,歡迎指正。

我將這些面試題主要分爲三類:概念題、算法題、理解應用題。

01面試題來源

  • 騰訊 - 提前批 - 客戶端開發

  • 深信服 - 優招 - C++ 研發(物聯網方向)

  • 老虎 - 正式批 - Windows 開發一面

  • 360 - 提前批 - 軟件開發

02概念題

1. 函數傳值的方式?

按值傳遞、按引用傳遞、按指針傳遞。

 

2. 引用和拷貝的區別?

引用的效率要高於拷貝,這也是爲什麼如果函數參數是類對象,通常使用引用傳遞。若不想被改變,則加上 const 爲常引用。

 

3. 引用的優缺點和拷貝的優缺點?

拷貝需要大量原始數據的移動,帶來時間和空間上的消耗,這是它的缺點。

 

C++11 所支持的移動語義(move semantics),其思想是避免原始數據的移動,而只是修改記錄。move 的本質是一個轉換函數,將給定類型轉換爲右值引用。

 

引用的缺點可以從引用與指針的區別的角度來看。如:構造時必須初始化、不能爲 NULL、初始化後無法再改變、引用的創建和銷燬不會調用類的拷貝構造函數和析構函數 等等。

 

4. C++ 內存分區?未初始化的全局變量放在哪裏?

C++ 內存分區:堆、棧、自由存儲區、全局/靜態存儲區、常量存儲區。

 

關於自由存儲區(Free Store)和堆(Heap

 

 

  • 文本段:程序的二進制代碼,只讀。

  • 初始化數據段:global/static/const/extern 初始化的變量。

  • 未初始化數據段:未初始化的變量。

  • 初始化數據段和未初始化數據段統稱爲數據段。

  • 堆:當使用 malloc 在堆上分配的內存。

  • 棧:局部變量、函數返回所需的一些東西。

 

5. 野指針是什麼?有什麼可以檢測嗎?

  • 聲明指針但是沒有初始化⚠️。

  • 釋放空間後沒有置 NULL,產生懸掛指針,存在安全隱患⚠️。

  • 當返回一個基於棧分配的局部變量的地址時,num 是一個局部變量,函數結束後自動釋放其空間,編譯器會給出警告⚠️。

 

如何檢測野指針?方法是提高野指針的崩潰率,將訪問時的隨機崩潰變成訪問必然崩潰,儘早的定位到野指針。macOS 下 Xcode 提供了內存塗鴉(Malloc Scrobble)。

 

Linux 下有一個工具可以解決常見的 C++ 內存問題——「Valgrind 」,其中的 memcheck 工具提供:檢查程序中的內存問題,如泄漏、越界、非法指針等。

 

6. 進程間通信方式,知道互斥鎖和自旋鎖嗎?

  • 管道(Pipe

`| >` :簡單,阻塞,半雙工通信。

 

  • 消息隊列

發送數據塊,數據塊有長度限制,數據拷貝消耗大。

 

  • 共享內存

解決消息隊列的數據拷貝產生的性能消耗。兩個擁有獨立的虛擬內存空間,但是映射到相同的物理內存中。

 

  • 信號量

Semaphore。記錄型信號量(阻塞隊列);互斥信號量。信號量實現進程的互斥和同步。經典的生產者消費者問題。

 

  • Socket

程序進行 Socket 通信。

 

 

自旋鎖🔒

指當一個線程在獲取鎖時,此時的鎖已經被其他線程獲取,那麼該線程循環等待,然後不斷判斷是否能夠被成功獲取,知道獲取到鎖纔會退出循環。

核心:線程反覆檢查鎖變量是否可用,忙等待。

  •  
while (test_and_set(lock) == 1);

缺點:消耗 CPU、“飢餓”現象

優點:線程不會進入阻塞狀態、減少了不必要的上下文切換。

 

互斥鎖🔒

防止兩條線程同時對同一臨界資源進行讀寫的機制。類似互斥信號量機制提供的阻塞隊列,實現讓權等待。

 

7. 路由器(R)和二層交換機(S)的區別?

交換機,嚴格定義:以太網交換機。

  • 工作層不同:R - 網絡層,S - 數據鏈路層轉發機制:R - IP addr,S - MAC addr

  • 主要功能:

R - 隔離廣播域,S - 不能

R - 用於連接不同網絡,S - 組建局域網

R - 防火牆,配置複雜

交換機能幹的事情,路由器也都可以。

 

8. ARP 協議的功能幹嘛用的?

ARP:Address Resolution Protocol 地址解析協議。

所謂的地址解析,就是主機在發送幀前將目標 IP 地址轉換成目標 MAC 地址的過程。ARP 請求就是:根據目的主機的 IP 地址,獲得其 MAC 地址。

 

終端 PING 一個 IP 地址,使用 WireShark 關鍵字 arp 過濾,可以看到 ARP 請求。

 

9. C 和 C++ 的區別?

 

C++ 融合了 3 種不同的編程方式:C 語言代表的過程性語言、C++ 在 C 語言基礎上添加的類代表的面嚮對象語言、C++ 模板支持的泛型編程。

 

首先,C 語言編程原理:其理念是將大型程序分解成小型、便於管理的任務,是一種自頂向下的設計。結構化編程技術反映了過程性編程的思想,根據執行的操作來構思一個程序。即:數據 + 算法 = 程序。

 

其次,面向對象編程:OOP(Object-Oriented Programming)強調的是數據。在 C++ 中,類是一種規範,它描述了這種新型的數據格式,對象是根據這種規範構造的特定數據結構。OOP 程序設計方法首先設計類,它準確的表示了程序要處理的東西,並不僅僅是將數據和方法合併爲類的定義,OOP 還有助於構建可重用代碼。

 

OOP 特性:抽象、封裝和數據隱藏、多態、繼承、代碼的可重用性。

 

最後,C++ 和範型編程:範型編程(Generic Programming)。C++ 有多種數據類型 —— 整數、浮點數、字符、字符串、用戶定義的、由多種類型組成的複合結構。例如,要對不同類型的數據進行排序,通常必須爲每種類型創建一個排序函數。泛型編程可以只編寫一個泛型(即不是特定類型的)函數,並將其用於各種實際類型。C++ 模板(Template)提供了完成這種任務的機制。

 

10. 你對多態的理解?

多態(Polymorphism)是 OOP 編程的一個重要思想在面嚮對象語言中,一個接口,多種方法即爲多態。

 

C++ 的多態分爲兩種,靜態多態和動態多態。

 

靜態多態是通過函數重載實現的,在編譯過程完成後就知道了應該調用哪個函數。

 

動態多態即運行時的多態性,通過虛函數(virtual)來實現的。虛函數允許子類重新定義成員函數,而子類重新定義父類的做法稱爲覆蓋(Override),或者稱爲重寫。

 

多態最常見的用法就是聲明基類類型的指針,利用該指針指向任意一個子類對象,調用相應的虛函數,可以根據指向的子類的不同而實現不同的方法。

 

11. 講一下 tcp 的擁塞控制?

TCP 的擁塞控制主要來避免兩種現象,即包丟失和超時重傳。一旦出現了這些現象就說明,發送的速度太快了,要慢一些。

 

一條 TCP 連接開始,cwnd 設置爲一個報文段,一次只能發送一個;當收到這一個確認的時候,cwnd 加一,於是一次能夠發送兩個;當這兩個的確認到來的時候,每個確認 cwnd 加一,兩個確認 cwnd 加二,於是一次能夠發送四個;當這四個的確認到來的時候,每個確認 cwnd 加一,四個確認 cwnd 加四,於是一次能夠發送八個。可以看出這是指數性的增長

 

漲到什麼時候是個頭呢?有一個值叫做門限(ssthresh)爲 65535 個字節,當超過這個值的時候,就要小心一點了,就不能增長那麼快了。

 

於是,每收到一個確認後,cwnd 增加 1/cwnd,我們接着上面的過程來,一次發送八個,當八個確認到來的時候,每個確認增加 1/8,八個確認一共 cwnd 增加 1,於是一次能夠發送九個,變成了線性增長

 

但是線性增長還是增長,還是越來越多,直到發送擁塞。擁塞的一種表現形式是丟包,需要超時重傳。

 

有一種方式是,將 sshresh 設爲 cwnd/2,將 cwnd 設爲 1,重新開始慢啓動。。但是這種方式太激進了,將一個高速的傳輸速度一下子停了下來,會造成網絡卡頓。

 

快速重傳算法。當接收端發現丟了一箇中間包的時候,發送三次前一個包的 ACK,於是發送端就會快速的重傳,不必等待超時再重傳。TCP 認爲這種情況不嚴重,因爲大部分沒丟,只丟了一小部分,cwnd 減半爲 cwnd/2,然後 sshthresh = cwnd,呈線性增長。

 

TCP BBR 擁塞算法,它佔滿帶寬,但是不佔滿緩存,時延較低。

 

12. 左值引用和右值引用的區別?

  • 左值存放在對象中,有持久的狀態;

  • 而右值要麼是字面常量,要麼是在表達式求值過程中創建的臨時對象,沒有持久的狀態。

  • 右值引用的對象,是臨時的,即將被銷燬;

 

如果我們將一個對象的內存空間綁定到另一個變量上,那麼這個變量就是左值引用。在建立引用的時候,我們是「將內存空間綁定」,因此我們使用的是一個對象在內存中的位置,這是一個左值。

  •  
const int& qux = 42;  // OK: 42 是右值,但是編譯器可以爲它開闢一塊內存空間,綁定在 qux 上

右值引用也是引用,但是它只能且必須綁定在右值上。右值引用應用在語義移動上,語義移動避免了數據的再拷貝帶來的花銷,提供一種數據的所有權的轉交。

13. 四種 cast 強制類型轉換的區別和使用?

  • static_cast:

    普通的類型轉換

 

  •  
static_cast<type-name>(value);

不提供運行時的檢查,要在編寫程序時確認轉換的安裝性。

(1)把子類對象的指針/引用轉換爲父類指針/引用(上行安全),這種轉換是安全的。若把父類對象的指針/引用轉換成子類指針/引用(下行不安全),這種轉換是不安全的,需要編寫程序時來確認。

(2)基本的數據類型轉換。安全由程序編寫人員保證。

 

  • dynamic_cast

    用於處理多態

dynamic_cast<type-name>(expression);

指針轉換失敗,則返回空指針,若引用轉換失敗,則拋出異常。

把父類對象的指針/引用轉換成子類指針/引用,即基類轉換爲派生類時使用 dynamic_cast

 

  • const_cast

    用於移除類型的 const、volatile 屬性。

const_cast<type-name>(expression);

常量指針被轉換成非常量指針,並且仍然指向原來的對象;常量引用被轉換成非常量引用,並且仍然引用原來的對象。

說明:使得 *pb 成爲一個可用於修改 bar 對象值的指針,它刪除 const 標籤。

 

  • reinterpret_cast

reinterpret_cast<type-name>(expression);

 

可以轉換任何類型的指針,不安全。不改變括號中運算對象的值,而是對該對象從位模式上進行重新解釋。

 

14. 講一下智能指針?

  • auto_ptr

auto_ptr 會導致所有權的轉移,於此同時,原指針不再指向有效數據(後續再訪問會出現錯誤)。

 

  • unique_ptr

unique_ptr 是 auto_ptr 的升級,策略更加的嚴格。unique_ptr 同樣採取建立所有權(ownership)的概念,對於特定對象,只能有一個智能指針可擁有它,只有擁有對象的智能指針的構造函數會刪除該對象。

 

編譯階段即可發現錯誤, unique_ptr 相比 auto_ptr 有更加安全的機制

move 函數可以移交所有權。

 

  • share_ptr

shared_ptr 採用引用計數(reference counting)的策略。賦值時,計數加一,指針過期時,計數減一。僅當最後一個指針過期時,才調用 delete 。

 

  • weak_ptr

weak_ptr 可以維持一種弱引用。

  1. 對象被析構了,weak_ptr 會自動等於 nullptr

  2. weak_ptr 可以還原成 shared_ptr 而不會讓引用計數錯亂。

 

自己動手實現智能指針:

  • 模版化(Template)

  • 運算符重載(operator)【*  解引用】 | 【-> 指向對象成員】 | 【bool 用在布爾表達式】 

  • 拷貝構造和賦值:拷貝時轉移指針所有權(捨棄 auto_ptr,選擇 unique_ptr

  • 引用計數

 

15. 瞭解完美轉發嗎?

一個函數某一組參數原封不動地傳遞給另一個函數。這裏不僅需要參數的值不變,而且需要參數的類型屬性(左值/右值)保持不變,這叫做 Perfect Forwarding(完美轉發)。

 

C++11 開始,完美轉發由 std::forward 函數實現。底層實現和引用reference 密切相關:remove_reference 、右值引用、引用聚合。

 

16. future 和 thread 瞭解嗎?

與併發(Concurrency)編程相關,future 是一個更高的抽象,同樣的結構,代碼將大大簡化,變量減少到只有一個未來量。

 

調用 async 可以獲得一個未來量。在未來量上調用 get 成員函數可以獲得結果。

 

17. tcp/ip 四層模型和 osi 七層模型具體是哪些?

 

18. 進程和線程的區別?

操作系統的設計,因此可以歸結爲三點:

(1)以多進程形式,允許多個任務同時運行;

(2)以多線程形式,允許單個任務分成不同的部分運行;

(3)提供協調機制,一方面防止進程之間和線程之間產生衝突,另一方面允許進程之間和線程之間共享資源。

 

a.地址空間和其它資源:進程間擁有獨立內存,進程是資源分配的基本單位;線程隸屬於某一進程,且同一進程的各線程間共享內存(資源),線程是cpu調度的基本單位。

 

b.通信:進程間相互獨立,通信困難,常用的方法有:管道,信號,套接字,共享內存,消息隊列等;線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。

 

c.調度和切換:線程上下文切換比進程上下文切換要快。進程間切換要保存上下文,加載另一個進程;而線程則共享了進程的上下文環境,切換更快。

04算法題

1. KMP 算法?

KMP 算法和核心在於求解模式串的 next 數組,使得模式串與主串在匹配過程中出現失配情況時,大大降低沒有必要的回退。

2. 知道二叉樹有哪些遍歷的方式嗎?

先序/中序/後序/層序遍歷。非遞歸:先序/中序/後序需要使用棧(Stack)輔助,層序遍歷需要使用隊列(Queue)輔助。

線索後遍歷(線性結構),但是改變了樹的指針結構。

 

4. 快速排序的思想?

快速排序是一種基於分治法的重要的排序算法,歸併排序也是基於分治技術。快速排序的核心在於切分(Partition)操作。根據切分的不同,可以分爲普通快排(單路切分)、雙路快排(雙路切分)、三路快排(大於、等於、小於三路)。

5. 冒泡排序

 

6. 手撕 strcpy?

 

注:如果 dest 事先未分配足夠的一片連續空間,則 `dest++` 可能會覆蓋其他的數據。

 

05理解應用題

1. 一個結構體,能夠用 memcpy 判斷兩個結構體存的東西是不一樣的嗎?

我不知道能不能?但是可以瞭解一下 memset 和 memcpy 函數。在對結構體變量都使用 memset 初始化的前提下,使用 memcmp 函數進行一個一個字節的比較,相同則返回 0。

memset 按字節賦值,對每個字節賦值同樣的值。0 的二進制代碼爲全 0,-1 的二進制補碼全爲 1。所以一般賦初值要賦爲 0 或 -1。如果對一個整型賦值爲 1,int 佔 4 個字節,最後結果爲十進制的 16843009。

  •  
0000 0001|0000 0001|0000 0001|0000 0001
  •  
  •  
  int a;  memset(&a, 1, sizeof(a)); // a is 16843009

 

 

如果有結構體成員有指針的情況,則還要再考慮,就如同深拷貝的淺拷貝問題。

 

memcpy sizeof(char) 等於 1,逐個字節的拷貝。

 

2. 知道哈希表嗎?怎麼解決衝突?如果只有 32 個槽,怎麼存放幾千個數據?

 

hashtable,將關鍵字 K,通過散列函數 F(K) 映射到表中一個位置上。設計優秀的哈希函數,哈希表可以實現接近於 O(1) 的查詢速度。當 F(K1) == F(K2) 時,稱爲衝突。

 

解決 Hash 衝突的方法有:線性探測法、二次探出法、開鏈法、再散列法。

 

如果只有 30 個槽,說明存在大量的衝突,平均每個槽會產生上百的衝突,此時第一級的哈希函數已經沒有多大的優化的空間了,要麼進行再 hash。我覺得采用開鏈法也是可取的,可以在每個槽後接上樹結構衝突發生時,往樹中添加即可,

採用樹結構可以保證查詢的時間複雜度爲 O(logn)。

 

3. 用過 shell 腳本嗎?

用過一點點,基於 Github Page 搭建了個人博客,本地編寫博客後,執行一個腳本,自動提交和部署到原廠倉庫。有一定的腳本語言基礎:Python、Lua。

 

4. 學習了哪些數據結構?

  • 線性數據結構

動態數組:Vector 擴容/縮容

鏈表(LinkedList):頭節點、尾指針、循環鏈表

棧(Stack):鏈表棧、動態數組棧

隊列(Queue):鏈表隊列、動態數組隊列、循環隊列

 

  • 樹結構

堆(Heap):優先隊列,最大/最小堆

二分搜索樹(BST)

平衡二叉樹(AVL)

紅黑樹(RB-Tree)

B 樹和 B+ 樹(B-Tree)

字典樹(Trie)

 

  • 圖結構

圖的表示:鄰接矩陣、鄰接表。

 

  • 其他數據結構

並查集(Union Find)

哈希表(Hash-table)

集合(Set):HashSet、TreeSet

映射(Map):HashMap、TreeMap

 

5. 你對 mfc 的印象?

微軟基礎類庫(Microsoft Foundation Classes,簡稱 MFC)。

MFC 可以用面向對象的方法來調用 Windows API,以及能夠更加敏捷的開發應用程序。

 

MFC 出現的時間比較早(1992),而 C++ 的第一個標準是 1998 年(C++98)。C++ 的發展比 MFC 更加迅猛發展,結果 MFC 的一些好的東西就成了它的陋習。

 

身邊也沒有同學學過 MFC。我對桌面程序開發有過一點點的經驗,我採用的方案的 C++ 和 Qt,做的是一個可視化的排序的窗口。

 

6. 用過哪些 UI 界面?

圖形化方面瞭解一點點的 Qt 和 OpenCV。但是時間比較早了,大三的時候。

 

7. 用過 qt 嗎?

排序算法的可視化,採用的是 C++ 和 Qt:QPainter、自動重繪函數。

 

8. 如何判斷大小端,手寫代碼?

在 Socket 網絡編程時,會涉及到大端序和小端序的轉換。

 

  • 大端序(Big Endian):高位在前,低位在後。這是人類讀寫數值的方式。

  • 小端序(Little Endian):地位字節在前,高位字節在後。

 

計算機正常的內存增長方式是從低到高(棧不是),取數據方式是從基地址根據偏移找到它們的位置。小端序第一個字節是它的低位,符號位在最後一位,在做運算時,從地位開始,最終刷新符號位,運算較爲高效。

 

大端序第一個字節就是高位,很容易知道它是正數還是負數,對於數值判斷會很迅速。除了計算機內部處理,其他場合幾乎都是大端序,比如網絡傳輸和文件存儲。

 

 

9. 做了哪些面向對象編程的工作?

  • 大學對課程設計

  • 動手實現 C++ STL 的部分容器類,遵循 OOP 思想,類的設計,方法接口設計。

05參考

1. What is the difference between new/delete and malloc/free? - Stack Overflow

https://stackoverflow.com/questions/240212/what-is-the-difference-between-new-delete-and-malloc-free

 

2. 二分搜索樹(BST)的前/中/後序遍歷-遞歸+非遞歸 - 藍本本

https://zhuanlan.zhihu.com/p/100926726

 

3. 十大排序算法 - 藍本本

https://zhuanlan.zhihu.com/p/100435276

 


 

PS:關注微信公衆號「藍本本」,和我一起學習、進修和放縱好奇心。

 

題圖:Photo by Mohamed Khaled from Pexels.

 


 

 

Landon

——

學習、進修和放縱好奇心!

@  藍本本  

——

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章