千萬不要錯過的後端[純乾貨]面試知識點整理 I

小魔童哪吒發表於2021-06-08

img

語言相關基礎題

物件複用的瞭解,零拷貝的瞭解

物件複用

指得是設計模式,物件可以採用不同的設計模式達到複用的目的,最常見的就是繼承和組合模式了。

零拷貝:

零拷貝主要的任務就是避免CPU將資料從一塊儲存拷貝到另外一塊儲存,主要就是利用各種零拷貝技術,避免讓CPU做大量的資料拷貝任務,減少不必要的拷貝,或者讓別的元件來做這一類簡單的資料傳輸任務,讓CPU解脫出來專注於別的任務。這樣就可以讓系統資源的利用更加有效。

零拷貝技術常見linux中,例如使用者空間到核心空間的拷貝,這個是沒有必要的,我們可以採用零拷貝技術,這個技術就是通過mmap直接將核心空間的資料通過對映的方法對映到使用者空間上,即物理上共用這段資料

介紹C++所有的建構函式

預設建構函式、一般建構函式、拷貝建構函式

  • 預設建構函式(無引數):如果建立一個類你沒有寫任何建構函式,則系統會自動生成預設的建構函式,或者寫了一個不帶任何形參的建構函式
  • 一般建構函式:一般建構函式可以有各種引數形式,一個類可以有多個一般建構函式,前提是引數的個數或者型別不同(基於c++的過載函式原理)
  • 拷貝建構函式引數為類物件本身的引用,用於根據一個已存在的物件複製出一個新的該類的物件,一般在函式中會將已存在物件的資料成員的值複製一份到新建立的物件中。引數(物件的引用)是不可變的(const型別)。此函式經常用在函式呼叫時使用者定義型別的值傳遞及返回。

為什麼要記憶體對齊?

  • 平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。

  • 效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。

成員初始化列表的概念,為什麼用成員初始化列表會快一些

型別一 : 內建資料型別,複合型別(指標,引用)

型別二 : 使用者定義型別(類型別)

對於型別一,在成員初始化列表和建構函式體內進行,在效能和結果上都是一樣的

對於型別二,結果上相同,但是效能上存在很大的差別

因為類型別的資料成員物件在進入函式體是已經構造完成,也就是說在成員初始化列表處進行構造物件的工作,這是呼叫一個建構函式,

在進入函式體之後,進行的是 對已經構造好的類物件的賦值,又呼叫個拷貝賦值操作符才能完成(如果並未提供,則使用編譯器提供的預設按成員賦值行為)

簡單的來說:

對於使用者定義型別:

  • 如果使用類初始化列表,直接呼叫對應的建構函式即完成初始化

  • 如果在建構函式中初始化,那麼首先呼叫預設的建構函式,然後呼叫指定的建構函式

所以對於使用者定義型別,使用列表初始化可以減少一次預設建構函式呼叫過程

c/c++ 程式除錯方法

  • printf 大法(日誌)

自己封裝巨集函式,進行列印出錯位置的檔案,行號,函式

通過gcc -DDEBUG_EN 開啟除錯資訊輸出

#ifdefine DEBUG_EN
#define DEBUG(fmt, args...) \
do { \
printf("DEBUG:%s-%d-%s "fmt, __FILE__, __LINE__, __FUNCTION__, ##args);\
}while(0)

#define ERROR(fmt, args...) \
do { \
printf("ERROR:%s-%d-%s "fmt, __FILE__, __LINE__, __FUNCTION__, ##args);\
}while(0)
#else
#define DEBUG(fmt, args) do{}while(0)
#define ERROR(fmt, args) do{}while(0)
#endif
  • core-dump/map 除錯

當程式執行的過程中異常終止或崩潰,作業系統會將程式當時的記憶體狀態記錄下來,儲存在一個檔案中,這種行為就叫做Core Dump.

MAP 檔案是程式的全域性符號、原始檔和程式碼行號資訊的唯一的文字表示方法,是整個程式工程資訊的靜態文字,通常由linker生成。

  • gdb

通過執行程式,打斷點、單步、檢視變數的值等方式在執行時定位bug。

file <檔名> 載入被除錯的可執行程式檔案
b <行號>/<函式名稱> 在第幾行或者某個函式第一行程式碼前設定斷點
r 執行
s 單步執行一行程式碼
n 執行一行程式碼,執行函式呼叫(如果有)
c 繼續執行程式至下一個斷點或者結束
p<變數名稱> 檢視變數值
q 退出

引用是否能實現動態繫結,為什麼引用可以實現

因為物件的型別是確定的,在編譯期就確定了,指標或引用是在執行期根據他們繫結的具體物件確定。

在什麼情況下系統會呼叫拷貝建構函式:(三種情況)

(1)用類的一個物件去初始化另一個物件時

(2)當函式的形參是類的物件時(也就是值傳遞時),如果是引用傳遞則不會呼叫

(3)當函式的返回值是類的物件或引用時

左值和右值

左值:可以對錶達式取地址,有名字的的值就是左值,一般指表示式結束後依然存在的持久物件

右值:不能對錶達式取地址,沒有名字的值,就是右值,一般指表示式結束後不再存在的臨時物件

  • 純右值 — 用於識別臨時變數和一些不跟物件關聯的值
  • 將亡值 — 具有轉移語義的物件

右值引用可以實現轉移語義和完美轉發的新特性

c++的訪問限定符

  • public
  • protected
  • private

在類的內部,不受限定符號的約束,可以隨意訪問,在類的外部,只能訪問類中public的成員

struct 和 class 的區別

struct 和class 都可以定義類,struct連的成員許可權都是public的

map

底層實現:紅黑樹

  • map – 無序map

key 和 value , key 是不可以重複的,所有元素的值都會自動排序,key不允許重複

  • unordered map – 有序map

key 和 value , key是可以重複的,所有元素的值都會自動排序,key不允許重複

vector

連續儲存的容器,記憶體分配在堆上面,動態陣列

底層實現:陣列

兩倍容量增長:vector一次性分配好記憶體, 在增加新元素的時候,如果沒有超過當前的容量,那麼直接新增,然後調整迭代器,如果超過了當前的容量, 則vector會重新配置原陣列的記憶體的2倍空間,將原空間元素記憶體拷貝到新空間,釋放掉原空間,且此時迭代器會失效

效能:

  • 查詢訪問的時候:O(1)
  • 插入的時候:

插入末尾:空間不夠,則需要申請記憶體,和釋放原空間,對資料進行拷貝

​ 空間夠,則直接插入,速度很快

插入中間:空間不夠,則需要申請記憶體,和釋放原空間,對資料進行拷貝

​ 空間夠,記憶體拷貝

  • 刪除資料的時候: 刪除中間的資料,需要記憶體拷貝

    刪除尾巴的資料,很快

  • 適用場景:經常隨機方案,且不對非尾部節點進行插入和刪除

list

動態連結串列,記憶體分配在堆上,每增加一個資料,則會開闢一個資料的空間,刪除一個資料,則會釋放掉一個資料的空間

底層實現:雙向連結串列

  • 訪問:效能很差,只能快速訪問頭尾節點

  • 插入:很快,常數的時間

  • 刪除:很快,常數的時間

  • 適用場景:大量增刪的場景

set

集合,所有元素都會根據元素的值進行排序,且不允許重複

底層實現:紅黑樹(一種平衡二叉樹)

適用場景:有序不重複集合

迭代器

迭代器是類模版,表現的像指標。封裝了指標的一些行為,過載了指標的++/--/->/*等操作符號,相當於一種智慧指標。可以根據不同的資料結構,來實現 ++ 和 – 操作

  • terator模式是運用於一種聚合物件的模式,把不同集合內的訪問邏輯抽象出來,使得不暴露物件的內部結構而達到遍歷集合的效果
  • 運用範圍:底層聚合支援類,如vector,stack,list及ostream_iterator的擴充套件

迭代器時如何刪除元素的

  • 對於vector,deque序列容器來說,記憶體是連續分配的,使用erase(iteraotor)後,後邊的迭代器都會失效,刪除一個元素,會導致後面的元素全部向前移動一個位置,但是 erase方法會返回下一個有效的iterator,如
vector<int> val = { 1,2,3,4,5,6 };  
vector<int>::iterator iter;  
for (iter = val.begin(); iter != val.end(); )  
{  
     if (3 == *iter)  
          iter = val.erase(iter);     //返回下一個有效的迭代器,無需+1  
     else  
          ++iter;  
}  
  • 對於關聯容器如:map,multimap,set,multiset,記憶體是隨機分配的,刪除當前的iterator,僅僅是當前的iterator失效而已,只要在erase的時候,iterator遞增即可。因為map之類的容器,底層實現是紅黑樹,插入和刪除一個節點,對其他節點沒有影響,如
set<int> valset = { 1,2,3,4,5,6 };  
set<int>::iterator iter;  
for (iter = valset.begin(); iter != valset.end(); )  
{  
     if (3 == *iter)  
          valset.erase(iter++);  
     else  
          ++iter;  
}  
  • 對於list容器來說,是不連續分配的記憶體,且list呼叫erase方法,是可以返回下一個有效的iterator,因此可以使用方法1 和 方法2

epoll原理

  • 呼叫epoll_create方法,建立epoll物件

  • 再使用epoll_ctrl方法,操作epoll物件,把需要操作的檔案描述符新增進去進行監控,這些檔案描述符會以epoll_event結構體的形式組成一顆紅黑樹,阻塞epoll_wait

  • 當某個fd有事件發生時,核心就會把該fd事件結構體放到連結串列中,返回發生事件的連結串列

resize 和 reverse

resize 是改變容器內含有元素的數量,它會建立元素,且會將值預設為0,如果resize後需要追加資料,則是在尾部追加

reverse 是改變容器的最大容量,它不會建立元素

編譯與底層 c++原始檔到可執行檔案經歷的過程

預處理階段:將原始碼檔案中標頭檔案,巨集定義進行分析和替換,生成預編譯檔案

編譯階段:將預編譯檔案轉換成特定的彙編程式碼,生成彙編檔案

彙編階段:將編譯階段的彙編檔案轉換成機器碼,生成可重定位目標檔案

連結階段:將多個目標檔案及所需的庫連結成最終的可執行檔案

編譯過程及記憶體管理

“”和<>的區別

“” :

  • 先從當前標頭檔案目錄中找

  • 編譯器設定的標頭檔案 (可以顯式的 是用 -I來指定)

  • 在系統變數的CPLUS_INCUCLUDE_PATH/C_INCLUDE_PATH中指定的標頭檔案路徑

<>:

  • 編譯器設定的標頭檔案 (可以顯式的 是用 -I來指定)

  • 在系統變數的CPLUS_INCUCLUDE_PATH/C_INCLUDE_PATH中指定的標頭檔案路徑

malloc原理

向記憶體申請一塊連續可用的空間,並返回指向這塊空間的指標

  • 如果開闢成功,則返回一個指向開闢好空間的指標

  • 如果開闢失敗,則返回一個NULL指標,因此malloc的返回值一定要做檢查

  • 返回值的型別是void* ,所以malloc函式並不知道開闢空間的型別,具體在使用的時候使用者自己來決定

  • 如果引數 size 為0,malloc的行為是標準未定義的,取決於編譯器

  • 標頭檔案均為#include <stdlib.h>

calloc

向記憶體申請一塊連續可用的空間,並返回指向這塊空間的指標

void* calloc(size_t num, size_t size);

  • 功能是為 num 個大小為 size 的元素開闢一塊空間,並且把空間的每個位元組初始化為0

  • 與函式 malloc 的區別只在於 calloc 會在返回地址之前把申請的空間的每個位元組初始化為全0

realloc

向記憶體申請一塊續可用的空間,並返回指向這塊空間的指標

void* realloc(void* ptr, size_t size);

  • ptr 是要調整的記憶體地址

  • size 是調整之後新大小

  • 返回值為調整之後的記憶體起始位置

  • 這個函式在調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到的空間

  • realloc在調整記憶體空間的時候存在兩種情況

情況1:原有空間之後有足夠大的空間

情況2:原有空間之後沒有足夠大的空間

當是情況1的時候,要擴充套件記憶體就直接在原有記憶體之後直接追加空間,原來空間的資料不發生變化。

當是情況2的時候,原有空間之後沒有足夠多的空間時,擴充套件的方法是:在堆空間上另找一個合適大小的連續空間來使用。這樣函式返回的是一個新的記憶體地址。

free

用來釋放動態開闢的記憶體

void free(void* ptr);

  • 如果引數 ptr 指向的空間不是動態開闢的,那free函式的行為是未定義的

  • 如果引數 ptr 是NULL指標,則函式什麼事都不做

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,*下一次 後端純乾貨面試題整理 I I *

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章