C++面試復習總結


C++面試

本人20年3到4月內面了近十家公司,整理一下C++客戶端問的多的基礎問題

另:操作系統面試總結OpenGL面試總結計算機網絡面試總結

代碼到可執行程序

  1. 預處理:條件編譯,頭文件包含,宏替換的處理,生成.i文件。
  2. 編譯:將預處理后的文件轉換成匯編語言,生成.s文件
  3. 匯編:匯編變為目標代碼(機器代碼)生成.o的文件
  4. 鏈接:連接目標代碼,生成可執行程序

C++內存結構

  1. 棧區: 由編譯器自動分配釋放,像局部變量,函數參數,都是在棧區。會隨着作用於退出而釋放空間。
  2. 堆區:程序員分配並釋放的區域,像malloc(c),new(c++)
  3. 全局數據區(靜態區):全局變量和靜態量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束釋放。
  4. 代碼區

靜態庫和動態庫

靜態庫和動態庫最本質的區別就是:該庫是否被編譯進目標(程序)內部

  • 靜態庫
    一般擴展名為.a.lib,在編譯的時候會直接整合到目標程序中,所以利用靜態函數庫編譯成的文件會比較大,最大的優點就是編譯成功的可執行文件可以獨立運行,而不再需要向外部要求讀取函數庫的內容;但是從升級難易度來看明顯沒有優勢,如果函數庫更新,需要重新編譯
  • 動態庫
    動態函數庫的擴展名一般為.so.dll,這類函數庫通常名為libxxx.so或xxx.dll 。
    與靜態函數庫被整個捕捉到程序中不同,動態函數庫在編譯的時候,在程序里只有一個“指向”的位置而已,也就是說當可執行文件需要使用到函數庫的機制時,程序才會去讀取函數庫來使用;也就是說可執行文件無法單獨運行。這樣從產品功能升級角度方便升級,只要替換對應動態庫即可,不必重新編譯整個可執行文件。

dll加載

隱式加載(靜態調用

在程序編譯的時候就將dll編譯到可執行文件中,這種加載方式調用方便,程序發布的時候可以不用帶着dll
缺點是程序會很大

顯示加載(動態調用

在程序運行過程中,需要用到dll里的函數時,再動態加載dll到內存中
這種加載方式因為是在程序運行后再加載的,所以可以讓程序啟動更快,而且dll的維護更容易,使得程序如果需要更新,很多時候直接更新dll,而不用重新安裝程序
這種加載方式,函數調用稍微復雜一點

C++多態

靜態多態

靜態多態:也稱為編譯期間的多態

  • 函數重載:包括普通函數的重載和成員函數的重載
  • 函數模板的使用

函數簽名

為什么C語言中沒有重載呢?
C編譯器的函數簽名不會記錄參數類型和順序,
C++中的函數簽名(function signature):包含了一個函數的信息——包括函數名、參數類型、參數個數、順序以及它所在的類和命名空間,普通函數簽名並不包含函數返回值部分。所以對於不同函數簽名的函數,即使函數名相同,編譯器和鏈接器都認為它們是不同的函數。

調用協議

stdcall是Pascal方式清理C方式壓棧,通常用於Win32 Api中,參數入棧規則是從右到左自己在退出時清空堆棧
cdecl是C和C++程序的缺省調用方式,參數入棧規則也是從右至左由調用者把參數彈出棧。對於傳送參數的內存棧是由調用者來維護的。每一個調用它的函數都包含清空堆棧的代碼,所以產生的可執行文件大小會比調用stdcall函數的大。
fastcall調用的主要特點就是快,通過寄存器來傳送參數,從左開始不大於4字節的參數放入CPU的ECX和EDX寄存器,其余參數從右向左入棧

C語言:

__stdcall 編譯后,函數名被修飾為“_functionname@number”
__cdecl 編譯后,函數名被修飾為“_functionname”
__fastcall 編譯后,函數名給修飾為“@functionname@nmuber”

C++:

__stdcall 編譯后,函數名被修飾為“?functionname@@YG******@Z”
__cdecl 編譯后,函數名被修飾為“?functionname@@YA******@Z”
__fastcall 編譯后,函數名給修飾為“?functionname@@YI******@Z”

函數實現和函數定義時如果使用了不同的函數調用協議,則無法實現函數調用。C語言和C++語言間如果不進行特殊處理,也無法實現函數的互相調用。

動態多態

虛函數表

當一個類在實現時,如果存在一個或以上的虛函數,這個類便會包含一張虛函數表。而當一個子類繼承了基類,子類也會有自己的一張虛函數表。

當我們在設計類的時候,如果把某個函數設置成虛函數時,也就表明我們希望子類在繼承的時候能夠有自己的實現方式;如果我們明確這個類不會被繼承,那么就不應該有虛函數的出現。

對於虛函數的調用是通過查虛函數表來進行的,每個虛函數在虛函數表中都存放着自己的一個地址。這張虛函數表是在編譯時產生的,否則這個類的結構信息中也不會插入虛指針的地址信息。

每個類使用一個虛函數表,每個類對象用一個虛表指針

多重繼承的派生類有多個虛函數表,就像一個二維數組

img

虛析構

如果析構函數不被聲明成虛函數,則編譯器采用的綁定方式是靜態綁定,在刪除基類指針時,只會調用基類析構函數,而不調用派生類析構函數,這樣就會導致基類指針指向的派生類對象析構不完全。

虛構造

實例化一個對象時,首先會分配對象內存空間,然后調用構造函數來初始化對象。
vptr變量是在構造函數中進行初始化的。又因為執行虛函數需要通過vptr指針來調用。如果可以定義構造函數為虛函數,那么就會陷入先有雞還是先有蛋的循環討論中。

重載,重寫,重定義

重載(overload):函數名相同,參數列表不同,override只是在類的內部存在

重寫(override):也叫覆蓋。子類重新定義父類中有相同名稱和參數的虛函數(virtual)。

  1. 被重寫的函數不能是static的,且必須是virtual的
  2. 重寫函數必須有相同的類型,名稱和參數列表
  3. 重寫函數的訪問修飾符可以不同。盡管父類的virtual方法是private的,派生類中重寫改寫為public,protected也是可以的。這是因為被virtual修飾的成員函數,無論他們是private/protect/public的,都會被統一放置到虛函數表中。

對父類進行派生時,子類會繼承到擁有相同偏移地址的虛函數標(相同偏移地址指的是各虛函數先谷底與VPTR指針的偏移),因此就允許子類對這些虛函數進行重寫

重定義(redefining),也叫隱藏。子類重新定義父類有相同名稱的非虛函數(參數列表可以不同)。

子類若有和父類相同的函數,那么,這個類將會隱藏其父類的方法。除非你在調用的時候,強制轉換成父類類型。在子類和父類之間嘗試做類似重載的調用時不能成功的。

菱形繼承

虛繼承解決菱形繼承二義性問題,虛繼承只影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。

在實際開發中,位於中間層次的基類將其繼承聲明為虛繼承一般不會帶來什么問題。通常情況下,使用虛繼承的類層次是由一個人或者一個項目組一次性設計完成的。對於一個獨立開發的類來說,很少需要基類中的某一個類是虛基類,況且新類的開發者也無法改變已經存在的類體系。

C++標准庫中的 iostream 類就是一個虛繼承的實際應用案例。iostream 從 istream 和 ostream 直接繼承而來,而 istream 和 ostream 又都繼承自一個共同的名為 base_ios 的類,是典型的菱形繼承。此時 istream 和 ostream 必須采用虛繼承,否則將導致 iostream 類中保留兩份 base_ios 類的成員。

img

函數調用的過程

int func(int param1 ,int param2,int param3){
    int var1 = param1;
    int var2 = param2;
    int var3 = param3;
    ...
}

int result = func(1,2,3);

在堆棧中變量分布是從高地址到低地址分布
EBP是指向棧底的指針,在過程調用中不變,又稱為幀指針。
ESP指向棧頂,程序執行時移動,ESP減小分配空間,ESP增大釋放空間,ESP又稱為棧指針。

函數執行時,參數從右向左逐步壓入棧中(stdcall),最后壓入RET返回地址。
通過跳轉指令進入函數后(func(1,2,3);),函數地址入棧,EBP入棧,然后把當前ESP的值給EBP,對應的匯編指令

push ebp
mov ebp esp

此時棧頂和棧底指向同一位置

之后再將調用的函數的臨時變量壓入,最后通過EAX寄存器保存函數的返回值
調用執行函數完畢,局部變量var3,var2,var1一次出棧,EBP恢復原值,返回地址出棧,找到原執行地址,param1,param2,param3依次出棧,函數調用執行完畢。

調用執行函數完畢,局部變量var3,var2,var1一次出棧,EBP恢復原值,返回地址出棧,找到原執行地址,param1,param2,param3依次出棧,函數調用執行完畢。圖略

類的類型大小

static

靜態局部變量:變量屬於函數本身,僅受函數的控制。保存在全局數據區,而不是在棧中,每次的值保持到下一次調用,直到下次賦新值。
靜態全局變量:定義在函數體外,用於修飾全局變量,表示該變量只在本文件可見,不能被其它文件所用(全局變量可以)

靜態函數:靜態函數不能被其它文件所用,其它文件中可以定義相同名字的函數,不會發生沖突

靜態數據成員:靜態數據成員的生存期大於 class 的實例(靜態數據成員是每個 class 有一份,普通數據成員是每個 instance 有一份)

  1. 靜態成員之間可以相互訪問,包括靜態成員函數訪問靜態數據成員和訪問靜態成員函數;
  2. 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員;
  3. 靜態成員函數不能訪問非靜態成員函數和非靜態數據成員;
  4. 調用靜態成員函數,可以用成員訪問操作符(.)和(->)為一個類的對象或指向類對象的指針調用靜態成員函數,也可以用類名::函數名調用(因為他本來就是屬於類的,用類名調用很正常)

const

修飾普通類型的變量

修飾指針:頂層const(指針本身是個常量)和底層cosnt(指針指向對象是一個常量)

修飾函數參數:

  1. 函數參數為值傳遞:值傳遞(pass-by-value)是傳遞一份參數的拷貝給函數,因此不論函數體代碼如何運行,也只會修改拷貝而無法修改原始對象,這種情況不需要將參數聲明為const
  2. 函數參數為指針:指針傳遞(pass-by-pointer)只會進行淺拷貝,拷貝一份指針給函數,而不會拷貝一份原始對象。因此,給指針參數加上頂層const可以防止指針指向被篡改,加上底層const可以防止指向對象被篡改。
  3. 函數參數為引用:引用傳遞(pass-by-reference)有一個很重要的作用,由於引用就是對象的一個別名,因此不需要拷貝對象,減小了開銷。這同時也導致可以通過修改引用直接修改原始對象(畢竟引用和原始對象其實是同一個東西),因此,大多數時候,推薦函數參數設置為pass-by-reference-to-const。給引用加上底層const,既可以減小拷貝開銷,又可以防止修改底層所引用的對象。

修飾函數返回值:令函數返回一個常量,可以有效防止因用戶錯誤造成的意外,比如 “=” 。

const成員函數:const成員函數不可修改類對象的內容(指針類型的數據成員只能保證不修改該指針指向)。原理是cost成員函數的this指針是底層const指針,不能用於改變其所指對象的值。
當成員函數的 const 和 non-const 版本同時存在時:const object 只能調用 const 版本,non-const object 只能調用 non-const 版本。

const object(data members不得變動) non-const objectdata members可變動)
const member function(保證不更改data members)
non-const member function(不保證) 不可

extern "C"

extern "C"的主要作用就是為了能夠正確實現C++代碼調用其他C語言代碼
加上extern "C"后,會指示編譯器這部分代碼按C語言(而不是C++)的方式進行編譯。由於C++支持函數重載,因此編譯器編譯函數的過程中會將函數的參數類型也加到編譯后的代碼中,而不僅僅是函數名;而C語言並不支持函數重載,因此編譯C語言代碼的函數時不會帶上函數的參數類型,一般只包括函數名。

在C++出現以前,很多代碼都是C語言寫的,而且很底層的庫也是C語言寫的,為了更好的支持原來的C代碼和已經寫好的C語言庫,需要在C++中盡可能的支持C,而extern "C"就是其中的一個策略。

  1. C++代碼調用C語言代碼
  2. 在C++的頭文件中使用
  3. 在多個人協同開發時,可能有的人比較擅長C語言,而有的人擅長C++

inline

類內定義成員函數默認為inline

優點:

  1. inline 定義的類的內聯函數,函數的代碼被放入符號表中,在使用時直接進行替換,(像宏一樣展開),沒有了調用的開銷,效率也很高。
  2. 內聯函數也是一個真正的函數,編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。然后進行一系列的相關檢查,就像對待任何一個真正的函數一樣。這樣就消除了它的隱患和局限性。(宏替換不會檢查參數類型,安全隱患較大)
  3. inline函數可以作為一個類的成員函數,與類的普通成員函數作用相同,可以訪問一個類的私有成員和保護成員。內聯函數可以用於替代一般的宏定義,最重要的應用在於類的存取函數的定義上面。

缺點:

  1. 內聯函數具有一定的局限性,內聯函數的函數體一般來說不能太大,如果內聯函數的函數體過大,一般的編譯器會放棄內聯方式,而采用普通的方式調用函數。(換句話說就是,你使用內聯函數,只不過是向編譯器提出一個申請,編譯器可以拒絕你的申請)這樣,內聯函數就和普通函數執行效率一樣了。
  2. inline說明對編譯器來說只是一種建議,編譯器可以選擇忽略這個建議。比如,你將一個長達1000多行的函數指定為inline,編譯器就會忽略這個inline,將這個函數還原成普通函數,因此並不是說把一個函數定義為inline函數就一定會被編譯器識別為內聯函數,具體取決於編譯器的實現和函數體的大小。

和宏定義的區別

宏是由預處理器對宏進行替代,而且內聯函數是真正的函數,只是在需要用到的時候,內聯函數像宏一樣的展開,所以取消了函數的參數壓棧,減少了調用的開銷。你可以象調用函數一樣來調用內聯函數,而不必擔心會產生於處理宏的一些問題。內聯函數與帶參數的宏定義進行下比較,它們的代碼效率是一樣,但是內聯歡函數要優於宏定義,因為內聯函數遵循的類型和作用域規則,它與一般函數更相近,在一些編譯器中,一旦關聯上內聯擴展,將與一般函數一樣進行調用,比較方便。

stl容器

序列容器

vector

動態數組,在堆中分配內存,元素連續存放,有保留內存,如果減少大小后,內存也不會釋放;如果新值大於當前大小時才會重新分配內存。

push_back()pop_back()insert(),訪問時間是O1,erase()時間是On,查找On

擴容方式:倍數開辟二倍的內存,舊的數據開辟到新內存,釋放舊的內存,指向新內存。

  • 對頭部和中間進行添加刪除元素操作需要移動內存,如果元素是結構或類,那么移動的同時還會進行構造和析構操作
  • 對任何元素的訪問時間都是O1,常用來保存需要經常進行隨機訪問的內容,並且不需要經常對中間元素進行添加刪除操作
  • 屬性與string差不多,同樣可以使用capacity看當前保留的內存,使用swap來減少它使用的內存,如push_back 1000個元素,capacity返回值為16384
  • 對最后元素操作最快(在后面添加刪除元素最快),此時一般不需要移動內存,只有保留內存不夠時才需要

list

雙向循環鏈表

元素存放在堆中,每個元素都是放在一塊內存中,他的內存空間可以是不連續的,通過指針來進行數據的訪問,這個特點使得它的隨機存取變得非常沒有效率,因此它沒有提供[]操作符的重載。但是由於鏈表的特點,它可以很有效率的支持任意地方的刪除和插入操作。

增刪erase()都是O1,訪問On,查找頭O1,其余查找On。

deque

雙端隊列 deque 是對 vector 和list 優缺點的結合的一種容器。

由於分段數組的大小是固定的,並且他們的首地址被連續存放在索引數組中,因此可以對其進行隨機訪問,但是效率比 vector 低很多。向兩端加入新元素時,如果這一端的分段數組未滿,則可以直接加入,如果這一端的分段數組已滿,只需要創建新的分段數組,並把該分段數組的地址加入到索引數組中即可,這樣就不需要對已有的元素進行移動,因此在雙端隊列的兩端加入新的元素都具有較高的效率

insert()erase()時間是On,查找On,其余O1。

vector:快速的隨機儲存,快速的在最后插入刪除元素
需要高效隨機儲存,需要高效的在末尾插入刪除元素,不需要高效的在其他地方插入和刪除元素

list:快速在任意位置插入刪除元素,快速訪問頭尾元素
需要大量插入刪除操作,不關心隨機儲存的場景

deque:比較快速的隨機存儲(比vector的慢),快速的在頭尾插入刪除元素
需要隨機儲存,需要高效的在頭尾插入刪除元素的場景

deque可以看作是vector和list的折中方案

關聯容器

底層都是紅黑樹

set

元素有序,無重復元素,插入刪除操作的效率比序列容器高,因為對於關聯容器來說,不需要做內存的拷貝和內存的移動。

增刪改查Ologn

multiset

multiset和set相同,只不過它允許重復元素,也就是說multiset可包括多個數值相同的元素。

map

map由紅黑樹實現,其元素都是鍵值對,每個元素的鍵是排序的准則,每個鍵只能出現一次,不允許重復

增刪改查Ologn

multimap

multimap和map相同,但允許重復元素,也就是說multimap可包含多個鍵值(key)相同的元素。

unordered_mapmap

map內部實現了一個紅黑樹(所有元素都是有序的),unordered_map內部實現了一個哈希表(元素的排列是無序的)

map
優點:有序性,這是map結構最大的優點,其元素的有序性在很多應用中都會簡化很多的操作;內部實現一個紅黑書使得map的很多操作在lgn的時間復雜度下就可以實現,因此效率非常的高
缺點:空間占用率高,因為map內部實現了紅黑樹,雖然提高了運行效率,但是因為每一個節點都需要額外保存父節點、孩子節點和紅/黑性質,使得每一個節點都占用大量的空間
適用:對於那些有順序要求的問題,用map會更高效一些

unordered_map
優點:因為內部實現了哈希表,因此其查找速度非常的快
缺點:哈希表的建立比較耗費時間
適用:對於查找問題,unordered_map會更加高效一些

智能指針

將基本類型指針封裝為類對象指針,並在析構函數里編寫delete語句刪除指針指向的內存空間。

auto_ptr<string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; vocaticn = ps;

如果 ps 和 vocation 是常規指針,則兩個指針將指向同一個 string 對象。這是不能接受的,因為程序將試圖刪除同一個對象兩次,一次是 ps 過期時,另一次是 vocation 過期時。要避免這種問題,方法有多種:

  1. 定義陚值運算符,使之執行深復制。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未采用此方案。
  2. 建立所有權(ownership)概念。對於特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的析構函數會刪除該對象。然后讓賦值操作轉讓所有權。這就是用於 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更嚴格。
  3. 創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數。例如,賦值時,計數將加 1,而指針過期時,計數將減 1,當減為 0 時才調用 delete。這是 shared_ptr 采用的策略。

所有的智能指針類都有一個explicit構造函數,以指針作為參數。因此不能自動將指針轉換為智能指針對象,必須顯式調用:

shared_ptr<double> pd; 
double *p_reg = new double;
pd = p_reg;                               // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)

對全部三種智能指針都應避免的一點:

string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No

pvac過期時,程序將把delete運算符用於非堆內存,這是錯誤的。

auto_ptr

在 auto_ptr 對象銷毀時,他所管理的對象也會自動被 delete 掉。
auto_ptr 采用 copy 語義來轉移指針資源,轉移指針資源的所有權的同時將原指針置為 NULL,拷貝后原對象變得無效,再次訪問原對象時會導致程序崩潰。

unique_ptr

由 C++11 引入,旨在替代不安全的 auto_ptr。

unique_ptr 則禁止了拷貝語義,但提供了移動語義,即可以使用std::move() 進行控制權限的轉移,如下代碼所示:

它持有對對象的獨有權——兩個 unique_ptr 不能指向一個對象,即 unique_ptr 不共享它所管理的對象。
內存資源所有權可以轉移到另一個 unique_ptr,並且原始 unique_ptr 不再擁有此資源。實際使用中,建議將對象限制為由一個所有者所有,因為多個所有權會使程序邏輯變得復雜。因此,當需要智能指針用於存 C++ 對象時,可使用 unique_ptr,構造 unique_ptr 時,可使用 make_unique 函數。

//智能指針的創建  
unique_ptr<int> u_i; 	//創建空智能指針
u_i.reset(new int(3)); 	//綁定動態對象  
unique_ptr<int> u_i2(new int(4));//創建時指定動態對象
unique_ptr<T,D> u(d);	//創建空 unique_ptr,執行類型為 T 的對象,用類型為 D 的對象 d 來替代默認的刪除器 delete

//所有權的變化  
int *p_i = u_i2.release();	//釋放所有權  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有權轉移(通過移動語義),u_s所有權轉移后,變成“空指針” 
u_s2.reset(u_s.release());	//所有權轉移
u_s2=nullptr;//顯式銷毀所指對象,同時智能指針變為空指針。與u_s2.reset()等價

當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這么做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因為它調用 unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 pu3 后就會被銷毀。這種隨情況而已的行為表明,unique_ptr 優於允許兩種賦值的auto_ptr 。

使用move后,原來的指針仍轉讓所有權變成空指針,可以對其重新賦值。

shared_ptr

shared_ptr 是為了解決 auto_ptr 在對象所有權上的局限性(auto_ptr 是獨占的),在使用引用計數的機制上提供了可以共享所有權的智能指針,當然這需要額外的開銷:

  1. shared_ptr 對象除了包括一個所擁有對象的指針外,還必須包括一個引用計數代理對象的指針;
  2. 時間上的開銷主要在初始化和拷貝操作上, * 和 -> 操作符重載的開銷跟 auto_ptr 是一樣;
  3. 開銷並不是我們不使用 shared_ptr 的理由,,永遠不要進行不成熟的優化,直到性能分析器告訴你這一點。

環形引用:智能指針互相指向了對方,導致自己的引用計數一直為1,所以沒有進行析構,這就造成了內存泄漏。

weak_ptr

weak_ptr 只對 shared_ptr 進行引用,而不改變其引用計數,當被觀察的 shared_ptr 失效后,相應的 weak_ptr 也相應失效。

內存泄漏

內存泄漏是指應用程序分配某段內存后,失去了對該段內存的控制,因而造成了內存的浪費。

  1. 在類的構造函數和析構函數中沒有匹配的調用new和delete函數
  2. 沒有正確地清除嵌套的對象指針
  3. 在釋放對象數組時在中沒有使用delete[]
  4. 指向對象的指針數組不等同於對象數組
  5. 缺少拷貝構造函數重載賦值運算符:隱式的指針復制結果就是兩個對象擁有指向同一個動態分配的內存空間的指針。
  6. 沒有將基類的析構函數定義為虛函數

野指針:指向被釋放的或者訪問受限內存的指針。

造成野指針的原因:

  1. 指針變量沒有被初始化(如果值不定,可以初始化為NULL)
  2. 指針被free或者delete后,沒有置為NULL,free和delete只是把指針所指向的內存給釋放掉,並沒有把指針本身干掉,此時指針指向的是“垃圾”內存。釋放后的指針應該被置為NULL.
  3. 指針操作超越了變量的作用范圍,比如返回指向棧內存的指針就是野指針。

避免內存泄漏

RAII 資源獲取即初始化,是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。智能指針即RAII最具代表的實現

RTTI

即通過運行時類型識別,程序能夠使用基類的指針或引用來檢查着這些指針或引用所指的對象的實際派生類型。

RTTI典型的應用需求
1、類型的識別,即能在運行時判斷出某對象、表達式等的類型,能判斷它們是基本類型(int、string),還是對象,以及它們區別於其它類型的標識;
2、對象的繼承關系的運行時判斷;
3、在出錯處理、內存診斷等處理時的輸出信息;
4、基於字符型名稱的運行時對象訪問、方法調用;
5、對象的自動保存和讀入;
6、基於ID或名稱的對象自動生成;
7、環境配置的保存和讀入;
8、程序自動生成;

指針和引用

指針是存放內存地址的一種變量,指針的大小不會像其他變量一樣變化。聲明時可以暫時不初始化
指針在聲明周期內隨時可能會為Null,所以使用時一定要做檢查,防止出現空指針、野指針的情況

引用的本質是“變量的別名”,就一定要有本體。聲明時就必須初始化
C++中的引用本質上是一種被限制的指針,引用是占據內存的

new和malloc

malloc實際是返還一段內存區域的中間地址,我們可用的內存只可以是這個地址之后的,地址前面的內存是根據系統固定規則存放着該內存區域的信息,這樣free就可以知道malloc出來的地址前面的內容,從而確定整個內存區域的大小。

1.類型安全

new操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符。而malloc內存分配成功則是返回void * ,需要通過強制類型轉換將void*指針轉換成我們需要的類型。
類型安全很大程度上可以等價於內存安全,類型安全的代碼不會試圖方法自己沒被授權的內存區域。關於C++的類型安全性可說的又有很多了。

2.分配失敗返回

new內存分配失敗時,會拋出bac_alloc異常,它不會返回NULL;malloc分配內存失敗時返回NULL。
在使用C語言時,我們習慣在malloc分配內存后判斷分配是否成功:

3.調用ctor

new的過程

使用new操作符來分配對象內存時會經歷三個步驟:

  1. 調用operator new 函數(對於數組是operator new[])分配一塊足夠大的,原始的,未命名的內存空間以便存儲特定類型的對象。
  2. 編譯器運行相應的構造函數以構造對象,並為其傳入初值。
  3. 對象構造完成后,返回一個指向該對象的指針。

使用delete操作符來釋放對象內存時會經歷兩個步驟:

  1. 調用對象的析構函數。
  2. 編譯器調用operator delete(或operator delete[])函數釋放內存空間。

總之來說,new/delete會調用對象的構造函數/析構函數以完成對象的構造/析構,而malloc則不會。

placement new 本質上是對 operator new 的重載,定義於#include 中,它不分配內存,調用合適的構造函數在 ptr 所指的地方構造一個對象,之后返回實參指針ptr

重載

opeartor new /operator delete可以被重載,而malloc/free並不允許重載

總結

特征 new/delete malloc/free
分配內存的位置 自由存儲區
分配成功 返回完整類型指針 返回void*
分配失敗 默認拋出異常 返回NULL
分配內存的大小 由編譯器根據類型計算得出 必須顯式指定字節數
處理數組 有處理數組的new版本new[] 需要用戶計算數組的大小后進行內存分配
已分配內存的擴充 無法直觀地處理 使用realloc簡單完成
是否相互調用 可以,看具體的operator new/delete實現 不可調用new
分配內存時內存不足 客戶能夠指定處理函數或重新制定分配器 無法通過用戶代碼進行處理
函數重載 允許 不允許
構造函數與析構函數 調用 不調用

strcpy和memcpy

  1. 復制的內容不同。strcpy只能復制字符串,而memcpy可以復制任意內容,例如字符數組、整型、結構體、類等。企業中使用memcpy很平常,因為需要拷貝大量的結構體參數。memcpy通常與memset函數配合使用。
  2. 復制的方法不同。strcpy不需要指定長度,它遇到被復制字符的串結束符"\0"才結束,所以容易溢出。memcpy則是根據其第3個參數決定復制的長度。因此strcpy會復制字符串的結束符“\0”,而memcpy則不會復制。

C++11,C++14

11:初始化列表,auto Type,foreach,nullptr,enum class,委托構造,禁止重寫final,顯示重寫override,成員初始值,默認構造函數default,刪除構造函數delete,常量表達式constexpr,Lambda函數

14:auto返回類型,泛型lambda

排序

堆排序、快速排序、希爾排序、直接選擇排序是不穩定的排序算法,

而基數排序、冒泡排序、直接插入排序、折半插入排序、歸並排序是穩定的排序算法。

歸並

  1. 如果給的數組只有一個元素的話,直接返回(也就是遞歸到最底層的一個情況)
  2. 把整個數組分為盡可能相等的兩個部分(分)
  3. 對於兩個被分開的兩個部分進行整個歸並排序(治)
  4. 把兩個被分開且排好序的數組拼接在一起

img

void merge(int arr[], int l, int m, int r)
{
    int i, j, k;
    int n1 = m - l + 1;
    int n2 = r - m;
    vector<int> L(n1);
    vector<int> R(n2);
    for (i = 0; i < n1; i++)
        L[i] = arr[l + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[m + 1 + j];
    i = 0;
    j = 0;
    k = l;
    while (i < n1 && j < n2){
        if (L[i] <= R[j]){
            arr[k] = L[i];
            i++;
        }
        else{
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    while (i < n1){
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2){
        arr[k] = R[j];
        j++;
        k++;
    }
}

void mergeSort(int arr[], int l, int r)
{
    if (l < r)
    {
        int m = l + (r - l) / 2;
        mergeSort(arr, l, m);
        mergeSort(arr, m + 1, r);
        merge(arr, l, m, r);
    }
}

堆排

基本思想:把待排序的元素按照大小在二叉樹位置上排列,排序好的元素要滿足:父節點的元素要大於等於其子節點;這個過程叫做堆化過程。
根據這個特性(大根堆根最大,小根堆根最小),就可以把根節點拿出來,然后再堆化下,再把根節點拿出來,循環到最后一個節點,就排序好了。

  • 最大堆中的最大元素值出現在根結點(堆頂)
  • 堆中每個父節點的元素值都大於等於其孩子結點

下圖是最小堆:

img

建立堆函數:

void heapify(int arr[], int n, int i) 
{ 
    int largest = i; // 將最大元素設置為堆頂元素
    int l = 2*i + 1; // left = 2*i + 1 
    int r = 2*i + 2; // right = 2*i + 2 
  
    // 如果 left 比 root 大的話
    if (l < n && arr[l] > arr[largest]) 
        largest = l; 
  
    // I如果 right 比 root 大的話
    if (r < n && arr[r] > arr[largest]) 
        largest = r; 
  
    if (largest != i) 
    { 
        swap(arr[i], arr[largest]); 
  
        // 遞歸地定義子堆
        heapify(arr, n, largest); 
    } 
} 

堆排序的方法如下,把最大堆堆頂的最大數取出,將剩余的堆繼續調整為最大堆,再次將堆頂的最大數取出,這個過程持續到剩余數只有一個時結束。

堆排序函數:

void heapSort(int arr[], int n) 
{ 
    // 建立堆
    for (int i = n / 2 - 1; i >= 0; i--) 
        heapify(arr, n, i); 
  
    // 一個個從堆頂取出元素
    for (int i=n-1; i>=0; i--) 
    { 
        swap(arr[0], arr[i]);  
        heapify(arr, i, 0); 
    } 
} 

快排

void quickSort(int a[],int left,int right)
{
  int i=left;
  int j=right;
  int temp=a[left];
  if(left>=right)
    return;
  while(i!=j)
  {
    while(i<j&&a[j]>=temp) 
    	j--;
    if(j>i)
      a[i]=a[j];//a[i]已經賦值給temp,所以直接將a[j]賦值給a[i],賦值完之后a[j],有空位
    while(i<j&&a[i]<=temp)
    i++;
    if(i<j)
      a[j]=a[i];
  }
  a[i]=temp;//把基准插入,此時i與j已經相等R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys
  quickSort(a,left,i-1);/*遞歸左邊*/
  quickSort(a,i+1,right);/*遞歸右邊*/
}

哈希沖突

https://blog.csdn.net/u011109881/article/details/80379505

圖的聯通

https://blog.csdn.net/weixin_44646116/article/details/95523884

紅黑樹和AVL

平衡二叉樹類型 平衡度 調整頻率 適用場景
AVL樹 查詢多,增/刪少
紅黑樹 增/刪頻繁

設計模式

單例模式

保證一個類只有一個實例,並提供一個訪問它的全局訪問點,使得系統中只有唯一的一個對象實例。
常用於管理資源,如日志、線程池

工廠模式

工廠模式的主要作用是封裝對象的創建,分離對象的創建和操作過程,用於批量管理對象的創建過程,便於程序的維護和擴展。


免責聲明!

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



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